libeufin

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

commit 9acd362f3c3dba2bb15be1c9df08f4308a567175
parent 995bed3a7128a9d9b31b62c398213ed67113a192
Author: Antoine A <>
Date:   Wed, 27 May 2026 19:11:29 +0200

common: replace Kotlin implementation with Rust implementation

Diffstat:
MCargo.toml | 4++--
MMakefile | 118+++++++++++++------------------------------------------------------------------
MREADME | 2+-
Mcontrib/bump-version | 17+++++++++++++++--
Mcontrib/ci/Containerfile | 36+++++++++++++++---------------------
Mcontrib/ci/ci.sh | 3++-
Mcontrib/ci/deb-test.sh | 2+-
Mcontrib/ci/jobs/0-codespell/dictionary.txt | 18++++--------------
Mcontrib/ci/jobs/0-codespell/job.sh | 11+++++------
Mcontrib/ci/jobs/2-test/job.sh | 2+-
Mcontrib/ci/jobs/3-docs/job.sh | 3+--
Acontrib/ci/jobs/4-deb-amd64/config.ini | 6++++++
Acontrib/ci/jobs/4-deb-amd64/job.sh | 21+++++++++++++++++++++
Dcontrib/ci/jobs/4-deb/job.sh | 21---------------------
Dcontrib/ci/jobs/4-deb/version.sh | 6------
Acontrib/ci/jobs/5-deb-arm64/config.ini | 6++++++
Acontrib/ci/jobs/5-deb-arm64/job.sh | 2++
Rcontrib/ci/jobs/5-deploy/config.ini -> contrib/ci/jobs/6-deploy/config.ini | 0
Rcontrib/ci/jobs/5-deploy/job.sh -> contrib/ci/jobs/6-deploy/job.sh | 0
Mcontrib/ci/version.sh | 6+++---
Mcontrib/docker-launcher/Dockerfile | 4++--
Dcrates/libeufin-bank/src/api.rs | 934-------------------------------------------------------------------------------
Dcrates/libeufin-bank/src/api/account.rs | 1929-------------------------------------------------------------------------------
Dcrates/libeufin-bank/src/api/cashout.rs | 457-------------------------------------------------------------------------------
Dcrates/libeufin-bank/src/api/conversion.rs | 1015-------------------------------------------------------------------------------
Dcrates/libeufin-bank/src/api/wire.rs | 719-------------------------------------------------------------------------------
Dcrates/libeufin-bank/src/lib.rs | 560-------------------------------------------------------------------------------
Dcrates/libeufin-ebics/src/ebics/order.rs | 270-------------------------------------------------------------------------------
Dcrates/libeufin-ebics/src/iso20022.rs | 183-------------------------------------------------------------------------------
Dcrates/libeufin-ebics/src/iso20022/camt.rs | 1249-------------------------------------------------------------------------------
Dcrates/libeufin-ebics/src/iso20022/hac.rs | 198-------------------------------------------------------------------------------
Dcrates/libeufin-ebics/src/iso20022/pain001.rs | 246-------------------------------------------------------------------------------
Dcrates/libeufin-ebics/src/iso20022/pain002.rs | 270-------------------------------------------------------------------------------
Dcrates/libeufin-nexus/src/db/initiated.rs | 915-------------------------------------------------------------------------------
Dcrates/libeufin-nexus/src/manual.rs | 164-------------------------------------------------------------------------------
Dcrates/libeufin-nexus/src/test.rs | 490-------------------------------------------------------------------------------
Dcrates/testbench/Cargo.toml | 23-----------------------
Dcrates/testbench/src/main.rs | 500-------------------------------------------------------------------------------
Mdebian/control | 29++++++++++++-----------------
Mdebian/libeufin-bank.install | 7+++----
Mdebian/libeufin-ebisync.install | 4+---
Mdebian/libeufin-nexus.install | 5+----
Mdebian/rules | 46++++++++++++----------------------------------
Dgradle.properties | 7-------
Dgradlew.bat | 82-------------------------------------------------------------------------------
Rcrates/libeufin-bank/Cargo.toml -> libeufin-bank/Cargo.toml | 0
Dlibeufin-bank/build.gradle | 71-----------------------------------------------------------------------
Mlibeufin-bank/conf/test.conf | 2+-
Mlibeufin-bank/conf/test_bonus.conf | 4++--
Mlibeufin-bank/conf/test_no_conversion.conf | 4++--
Mlibeufin-bank/conf/test_no_password_check.conf | 4++--
Mlibeufin-bank/conf/test_restrict.conf | 4++--
Mlibeufin-bank/conf/test_tan_err.conf | 2+-
Mlibeufin-bank/conf/test_with_fees.conf | 2+-
Mlibeufin-bank/conf/test_x_taler_bank.conf | 2+-
Alibeufin-bank/src/api.rs | 934+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alibeufin-bank/src/api/account.rs | 1929+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alibeufin-bank/src/api/cashout.rs | 457+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alibeufin-bank/src/api/conversion.rs | 1015+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rcrates/libeufin-bank/src/api/prepared.rs -> libeufin-bank/src/api/prepared.rs | 0
Rcrates/libeufin-bank/src/api/revenue.rs -> libeufin-bank/src/api/revenue.rs | 0
Rcrates/libeufin-bank/src/api/tan.rs -> libeufin-bank/src/api/tan.rs | 0
Rcrates/libeufin-bank/src/api/token.rs -> libeufin-bank/src/api/token.rs | 0
Rcrates/libeufin-bank/src/api/tx.rs -> libeufin-bank/src/api/tx.rs | 0
Alibeufin-bank/src/api/wire.rs | 719+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rcrates/libeufin-bank/src/api/withdrawal.rs -> libeufin-bank/src/api/withdrawal.rs | 0
Rcrates/libeufin-bank/src/auth.rs -> libeufin-bank/src/auth.rs | 0
Rcrates/libeufin-bank/src/config.rs -> libeufin-bank/src/config.rs | 0
Rcrates/libeufin-bank/src/db.rs -> libeufin-bank/src/db.rs | 0
Rcrates/libeufin-bank/src/db/account.rs -> libeufin-bank/src/db/account.rs | 0
Rcrates/libeufin-bank/src/db/cashout.rs -> libeufin-bank/src/db/cashout.rs | 0
Rcrates/libeufin-bank/src/db/conversion.rs -> libeufin-bank/src/db/conversion.rs | 0
Rcrates/libeufin-bank/src/db/exchange.rs -> libeufin-bank/src/db/exchange.rs | 0
Rcrates/libeufin-bank/src/db/gc.rs -> libeufin-bank/src/db/gc.rs | 0
Rcrates/libeufin-bank/src/db/prepared.rs -> libeufin-bank/src/db/prepared.rs | 0
Rcrates/libeufin-bank/src/db/tan.rs -> libeufin-bank/src/db/tan.rs | 0
Rcrates/libeufin-bank/src/db/token.rs -> libeufin-bank/src/db/token.rs | 0
Rcrates/libeufin-bank/src/db/tx.rs -> libeufin-bank/src/db/tx.rs | 0
Rcrates/libeufin-bank/src/db/withdrawal.rs -> libeufin-bank/src/db/withdrawal.rs | 0
Alibeufin-bank/src/lib.rs | 560+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rcrates/libeufin-bank/src/main.rs -> libeufin-bank/src/main.rs | 0
Dlibeufin-bank/src/main/kotlin/tech/libeufin/bank/Config.kt | 222-------------------------------------------------------------------------------
Dlibeufin-bank/src/main/kotlin/tech/libeufin/bank/Constants.kt | 44--------------------------------------------
Dlibeufin-bank/src/main/kotlin/tech/libeufin/bank/Error.kt | 74--------------------------------------------------------------------------
Dlibeufin-bank/src/main/kotlin/tech/libeufin/bank/Main.kt | 86-------------------------------------------------------------------------------
Dlibeufin-bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt | 1045-------------------------------------------------------------------------------
Dlibeufin-bank/src/main/kotlin/tech/libeufin/bank/api/BankIntegrationApi.kt | 188-------------------------------------------------------------------------------
Dlibeufin-bank/src/main/kotlin/tech/libeufin/bank/api/ConversionApi.kt | 409-------------------------------------------------------------------------------
Dlibeufin-bank/src/main/kotlin/tech/libeufin/bank/api/CoreBankApi.kt | 1451-------------------------------------------------------------------------------
Dlibeufin-bank/src/main/kotlin/tech/libeufin/bank/api/ObservabilityApi.kt | 86-------------------------------------------------------------------------------
Dlibeufin-bank/src/main/kotlin/tech/libeufin/bank/api/PreparedTransferApi.kt | 141-------------------------------------------------------------------------------
Dlibeufin-bank/src/main/kotlin/tech/libeufin/bank/api/RevenueApi.kt | 90-------------------------------------------------------------------------------
Dlibeufin-bank/src/main/kotlin/tech/libeufin/bank/api/WireGatewayApi.kt | 420-------------------------------------------------------------------------------
Dlibeufin-bank/src/main/kotlin/tech/libeufin/bank/auth/auth.kt | 275-------------------------------------------------------------------------------
Dlibeufin-bank/src/main/kotlin/tech/libeufin/bank/auth/mfa.kt | 184-------------------------------------------------------------------------------
Dlibeufin-bank/src/main/kotlin/tech/libeufin/bank/cli/BenchPwh.kt | 62--------------------------------------------------------------
Dlibeufin-bank/src/main/kotlin/tech/libeufin/bank/cli/ChangePw.kt | 76----------------------------------------------------------------------------
Dlibeufin-bank/src/main/kotlin/tech/libeufin/bank/cli/CreateAccount.kt | 117-------------------------------------------------------------------------------
Dlibeufin-bank/src/main/kotlin/tech/libeufin/bank/cli/CreateToken.kt | 97-------------------------------------------------------------------------------
Dlibeufin-bank/src/main/kotlin/tech/libeufin/bank/cli/DbInit.kt | 64----------------------------------------------------------------
Dlibeufin-bank/src/main/kotlin/tech/libeufin/bank/cli/EditAccount.kt | 103-------------------------------------------------------------------------------
Dlibeufin-bank/src/main/kotlin/tech/libeufin/bank/cli/Gc.kt | 41-----------------------------------------
Dlibeufin-bank/src/main/kotlin/tech/libeufin/bank/cli/LibeufinBank.kt | 37-------------------------------------
Dlibeufin-bank/src/main/kotlin/tech/libeufin/bank/cli/Serve.kt | 70----------------------------------------------------------------------
Dlibeufin-bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt | 807-------------------------------------------------------------------------------
Dlibeufin-bank/src/main/kotlin/tech/libeufin/bank/db/CashoutDAO.kt | 157-------------------------------------------------------------------------------
Dlibeufin-bank/src/main/kotlin/tech/libeufin/bank/db/ConversionDAO.kt | 463-------------------------------------------------------------------------------
Dlibeufin-bank/src/main/kotlin/tech/libeufin/bank/db/Database.kt | 174-------------------------------------------------------------------------------
Dlibeufin-bank/src/main/kotlin/tech/libeufin/bank/db/ExchangeDAO.kt | 321-------------------------------------------------------------------------------
Dlibeufin-bank/src/main/kotlin/tech/libeufin/bank/db/GcDAO.kt | 87-------------------------------------------------------------------------------
Dlibeufin-bank/src/main/kotlin/tech/libeufin/bank/db/TanDAO.kt | 249-------------------------------------------------------------------------------
Dlibeufin-bank/src/main/kotlin/tech/libeufin/bank/db/TokenDAO.kt | 253-------------------------------------------------------------------------------
Dlibeufin-bank/src/main/kotlin/tech/libeufin/bank/db/TransactionDAO.kt | 211-------------------------------------------------------------------------------
Dlibeufin-bank/src/main/kotlin/tech/libeufin/bank/db/TransferDAO.kt | 89-------------------------------------------------------------------------------
Dlibeufin-bank/src/main/kotlin/tech/libeufin/bank/db/WithdrawalDAO.kt | 409-------------------------------------------------------------------------------
Dlibeufin-bank/src/main/kotlin/tech/libeufin/bank/helpers.kt | 131-------------------------------------------------------------------------------
Dlibeufin-bank/src/main/kotlin/tech/libeufin/bank/params.kt | 146-------------------------------------------------------------------------------
Rcrates/libeufin-bank/src/mfa.rs -> libeufin-bank/src/mfa.rs | 0
Rcrates/libeufin-bank/src/payto.rs -> libeufin-bank/src/payto.rs | 0
Rcrates/libeufin-bank/src/pw.rs -> libeufin-bank/src/pw.rs | 0
Dlibeufin-bank/src/test/kotlin/AmountTest.kt | 404-------------------------------------------------------------------------------
Dlibeufin-bank/src/test/kotlin/BankIntegrationApiTest.kt | 294-------------------------------------------------------------------------------
Dlibeufin-bank/src/test/kotlin/CommonApiTest.kt | 34----------------------------------
Dlibeufin-bank/src/test/kotlin/ConversionApiTest.kt | 204-------------------------------------------------------------------------------
Dlibeufin-bank/src/test/kotlin/CoreBankApiTest.kt | 2584-------------------------------------------------------------------------------
Dlibeufin-bank/src/test/kotlin/DatabaseTest.kt | 187-------------------------------------------------------------------------------
Dlibeufin-bank/src/test/kotlin/GcTest.kt | 170-------------------------------------------------------------------------------
Dlibeufin-bank/src/test/kotlin/JsonTest.kt | 85-------------------------------------------------------------------------------
Dlibeufin-bank/src/test/kotlin/ObservabilityTest.kt | 55-------------------------------------------------------
Dlibeufin-bank/src/test/kotlin/OpenApiTest.kt | 59-----------------------------------------------------------
Dlibeufin-bank/src/test/kotlin/PaytoTest.kt | 90-------------------------------------------------------------------------------
Dlibeufin-bank/src/test/kotlin/PreparedTransferApiTest.kt | 288-------------------------------------------------------------------------------
Dlibeufin-bank/src/test/kotlin/RevenueApiTest.kt | 61-------------------------------------------------------------
Dlibeufin-bank/src/test/kotlin/SecurityTest.kt | 94-------------------------------------------------------------------------------
Dlibeufin-bank/src/test/kotlin/StatsTest.kt | 216-------------------------------------------------------------------------------
Dlibeufin-bank/src/test/kotlin/WireGatewayApiTest.kt | 538-------------------------------------------------------------------------------
Dlibeufin-bank/src/test/kotlin/bench.kt | 440-------------------------------------------------------------------------------
Dlibeufin-bank/src/test/kotlin/helpers.kt | 413-------------------------------------------------------------------------------
Dlibeufin-bank/src/test/kotlin/routines.kt | 177-------------------------------------------------------------------------------
Dlibeufin-common/build.gradle | 84-------------------------------------------------------------------------------
Dlibeufin-common/src/main/kotlin/AnsiColor.kt | 76----------------------------------------------------------------------------
Dlibeufin-common/src/main/kotlin/ApiError.kt | 162-------------------------------------------------------------------------------
Dlibeufin-common/src/main/kotlin/Backoff.kt | 42------------------------------------------
Dlibeufin-common/src/main/kotlin/Cli.kt | 124-------------------------------------------------------------------------------
Dlibeufin-common/src/main/kotlin/Constants.kt | 42------------------------------------------
Dlibeufin-common/src/main/kotlin/Encoding.kt | 106-------------------------------------------------------------------------------
Dlibeufin-common/src/main/kotlin/Subject.kt | 271-------------------------------------------------------------------------------
Dlibeufin-common/src/main/kotlin/Table.kt | 87-------------------------------------------------------------------------------
Dlibeufin-common/src/main/kotlin/TalerConfig.kt | 600-------------------------------------------------------------------------------
Dlibeufin-common/src/main/kotlin/TalerErrorCode.kt | 2191-------------------------------------------------------------------------------
Dlibeufin-common/src/main/kotlin/TalerMessage.kt | 508-------------------------------------------------------------------------------
Dlibeufin-common/src/main/kotlin/api/auth.kt | 59-----------------------------------------------------------
Dlibeufin-common/src/main/kotlin/api/route.kt | 41-----------------------------------------
Dlibeufin-common/src/main/kotlin/api/server.kt | 372-------------------------------------------------------------------------------
Dlibeufin-common/src/main/kotlin/client.kt | 130-------------------------------------------------------------------------------
Dlibeufin-common/src/main/kotlin/config.kt | 66------------------------------------------------------------------
Dlibeufin-common/src/main/kotlin/crypto/CryptoUtil.kt | 343-------------------------------------------------------------------------------
Dlibeufin-common/src/main/kotlin/crypto/PwCrypto.kt | 107-------------------------------------------------------------------------------
Dlibeufin-common/src/main/kotlin/db/DbPool.kt | 82-------------------------------------------------------------------------------
Dlibeufin-common/src/main/kotlin/db/config.kt | 86-------------------------------------------------------------------------------
Dlibeufin-common/src/main/kotlin/db/helpers.kt | 166-------------------------------------------------------------------------------
Dlibeufin-common/src/main/kotlin/db/notifications.kt | 101-------------------------------------------------------------------------------
Dlibeufin-common/src/main/kotlin/db/schema.kt | 117-------------------------------------------------------------------------------
Dlibeufin-common/src/main/kotlin/db/statement.kt | 237-------------------------------------------------------------------------------
Dlibeufin-common/src/main/kotlin/db/transaction.kt | 85-------------------------------------------------------------------------------
Dlibeufin-common/src/main/kotlin/db/types.kt | 135-------------------------------------------------------------------------------
Dlibeufin-common/src/main/kotlin/helpers.kt | 200-------------------------------------------------------------------------------
Dlibeufin-common/src/main/kotlin/iban.kt | 89-------------------------------------------------------------------------------
Dlibeufin-common/src/main/kotlin/log.kt | 114-------------------------------------------------------------------------------
Dlibeufin-common/src/main/kotlin/params.kt | 150-------------------------------------------------------------------------------
Dlibeufin-common/src/main/kotlin/registry.kt | 216-------------------------------------------------------------------------------
Dlibeufin-common/src/main/kotlin/security.kt | 30------------------------------
Dlibeufin-common/src/main/kotlin/test/bench.kt | 101-------------------------------------------------------------------------------
Dlibeufin-common/src/main/kotlin/test/cli.kt | 33---------------------------------
Dlibeufin-common/src/main/kotlin/test/helpers.kt | 177-------------------------------------------------------------------------------
Dlibeufin-common/src/main/kotlin/test/routines.kt | 141-------------------------------------------------------------------------------
Dlibeufin-common/src/main/kotlin/time.kt | 60------------------------------------------------------------
Dlibeufin-common/src/main/resources/META-INF/services/org.slf4j.spi.SLF4JServiceProvider | 2--
Dlibeufin-common/src/main/resources/xsd/camt.052.001.02.xsd | 1299-------------------------------------------------------------------------------
Dlibeufin-common/src/main/resources/xsd/camt.053.001.02.xsd | 1299-------------------------------------------------------------------------------
Dlibeufin-common/src/main/resources/xsd/camt.054.001.02.xsd | 1240-------------------------------------------------------------------------------
Dlibeufin-common/src/main/resources/xsd/ebics_H004.xsd | 11-----------
Dlibeufin-common/src/main/resources/xsd/ebics_H005.xsd | 11-----------
Dlibeufin-common/src/main/resources/xsd/ebics_hev.xsd | 135-------------------------------------------------------------------------------
Dlibeufin-common/src/main/resources/xsd/ebics_keymgmt_request_H004.xsd | 543-------------------------------------------------------------------------------
Dlibeufin-common/src/main/resources/xsd/ebics_keymgmt_request_H005.xsd | 523-------------------------------------------------------------------------------
Dlibeufin-common/src/main/resources/xsd/ebics_keymgmt_response_H004.xsd | 137-------------------------------------------------------------------------------
Dlibeufin-common/src/main/resources/xsd/ebics_keymgmt_response_H005.xsd | 137-------------------------------------------------------------------------------
Dlibeufin-common/src/main/resources/xsd/ebics_orders_H004.xsd | 1892-------------------------------------------------------------------------------
Dlibeufin-common/src/main/resources/xsd/ebics_orders_H005.xsd | 2094-------------------------------------------------------------------------------
Dlibeufin-common/src/main/resources/xsd/ebics_request_H004.xsd | 355-------------------------------------------------------------------------------
Dlibeufin-common/src/main/resources/xsd/ebics_request_H005.xsd | 349-------------------------------------------------------------------------------
Dlibeufin-common/src/main/resources/xsd/ebics_response_H004.xsd | 166-------------------------------------------------------------------------------
Dlibeufin-common/src/main/resources/xsd/ebics_response_H005.xsd | 167-------------------------------------------------------------------------------
Dlibeufin-common/src/main/resources/xsd/ebics_signature_S002.xsd | 177-------------------------------------------------------------------------------
Dlibeufin-common/src/main/resources/xsd/ebics_signatures.xsd | 217-------------------------------------------------------------------------------
Dlibeufin-common/src/main/resources/xsd/ebics_types_H004.xsd | 2426-------------------------------------------------------------------------------
Dlibeufin-common/src/main/resources/xsd/ebics_types_H005.xsd | 1885-------------------------------------------------------------------------------
Dlibeufin-common/src/main/resources/xsd/pain.001.001.03.ch.02.xsd | 1212-------------------------------------------------------------------------------
Dlibeufin-common/src/main/resources/xsd/pain.001.001.03.xsd | 923-------------------------------------------------------------------------------
Dlibeufin-common/src/main/resources/xsd/pain.001.001.09.ch.03.xsd | 1733-------------------------------------------------------------------------------
Dlibeufin-common/src/main/resources/xsd/pain.002.001.13.xsd | 1253-------------------------------------------------------------------------------
Dlibeufin-common/src/main/resources/xsd/xmldsig-core-schema.xsd | 318-------------------------------------------------------------------------------
Dlibeufin-common/src/test/kotlin/AmountTest.kt | 71-----------------------------------------------------------------------
Dlibeufin-common/src/test/kotlin/BaseUrlTest.kt | 53-----------------------------------------------------
Dlibeufin-common/src/test/kotlin/ConfigTest.kt | 321-------------------------------------------------------------------------------
Dlibeufin-common/src/test/kotlin/CryptoUtilTest.kt | 130-------------------------------------------------------------------------------
Dlibeufin-common/src/test/kotlin/EncodingTest.kt | 120-------------------------------------------------------------------------------
Dlibeufin-common/src/test/kotlin/IbanTest.kt | 43-------------------------------------------
Dlibeufin-common/src/test/kotlin/ParamsTest.kt | 61-------------------------------------------------------------
Dlibeufin-common/src/test/kotlin/PaytoTest.kt | 89-------------------------------------------------------------------------------
Dlibeufin-common/src/test/kotlin/SubjectTest.kt | 200-------------------------------------------------------------------------------
Dlibeufin-common/src/test/kotlin/TlsTest.kt | 56--------------------------------------------------------
Dlibeufin-common/src/test/resources/ebics_hev.xml | 4----
Dlibeufin-common/src/test/resources/ebics_ini_inner_key.xml | 15---------------
Dlibeufin-common/src/test/resources/ebics_ini_request_sample.xml | 26--------------------------
Dlibeufin-common/src/test/resources/hia_request.xml | 26--------------------------
Dlibeufin-common/src/test/resources/hia_request_order_data.xml | 24------------------------
Dlibeufin-common/src/test/resources/hpb_request.xml | 43-------------------------------------------
Dlibeufin-common/src/test/resources/signature1/doc.xml | 10----------
Dlibeufin-common/src/test/resources/signature1/public_key.txt | 2--
Dlibeufin-common/tmp/test-conf.conf | 4----
Dlibeufin-common/tmp/test-second-conf.conf | 0
Rcrates/libeufin-ebics/Cargo.toml -> libeufin-ebics/Cargo.toml | 0
Rcrates/libeufin-ebics/bin/iso20022-codegen.rs -> libeufin-ebics/bin/iso20022-codegen.rs | 0
Dlibeufin-ebics/build.gradle | 45---------------------------------------------
Rlibeufin-nexus/sample/platform/gls_camt052.xml -> libeufin-ebics/sample/platform/gls_camt052.xml | 0
Rlibeufin-nexus/sample/platform/gls_camt053.xml -> libeufin-ebics/sample/platform/gls_camt053.xml | 0
Rlibeufin-nexus/sample/platform/gls_camt054.xml -> libeufin-ebics/sample/platform/gls_camt054.xml | 0
Rlibeufin-nexus/sample/platform/gls_pain001.xml -> libeufin-ebics/sample/platform/gls_pain001.xml | 0
Rlibeufin-nexus/sample/platform/hac.xml -> libeufin-ebics/sample/platform/hac.xml | 0
Rlibeufin-nexus/sample/platform/maerki_baumann_camt053.xml -> libeufin-ebics/sample/platform/maerki_baumann_camt053.xml | 0
Rlibeufin-nexus/sample/platform/maerki_baumann_pain001.xml -> libeufin-ebics/sample/platform/maerki_baumann_pain001.xml | 0
Rlibeufin-nexus/sample/platform/pain002_accp.xml -> libeufin-ebics/sample/platform/pain002_accp.xml | 0
Rlibeufin-nexus/sample/platform/pain002_part.xml -> libeufin-ebics/sample/platform/pain002_part.xml | 0
Rlibeufin-nexus/sample/platform/postfinance_camt053.xml -> libeufin-ebics/sample/platform/postfinance_camt053.xml | 0
Rlibeufin-nexus/sample/platform/postfinance_camt054.xml -> libeufin-ebics/sample/platform/postfinance_camt054.xml | 0
Rlibeufin-nexus/sample/platform/postfinance_pain001.xml -> libeufin-ebics/sample/platform/postfinance_pain001.xml | 0
Rlibeufin-nexus/sample/platform/raiffeisen_camt053.xml -> libeufin-ebics/sample/platform/raiffeisen_camt053.xml | 0
Rlibeufin-nexus/sample/platform/raiffeisen_pain001.xml -> libeufin-ebics/sample/platform/raiffeisen_pain001.xml | 0
Rlibeufin-nexus/sample/platform/valiant_camt052.xml -> libeufin-ebics/sample/platform/valiant_camt052.xml | 0
Rlibeufin-nexus/sample/platform/valiant_pain001.xml -> libeufin-ebics/sample/platform/valiant_pain001.xml | 0
Rcrates/libeufin-ebics/src/cli.rs -> libeufin-ebics/src/cli.rs | 0
Rcrates/libeufin-ebics/src/config.rs -> libeufin-ebics/src/config.rs | 0
Rcrates/libeufin-ebics/src/crypto.rs -> libeufin-ebics/src/crypto.rs | 0
Rcrates/libeufin-ebics/src/db.rs -> libeufin-ebics/src/db.rs | 0
Rcrates/libeufin-ebics/src/dialect.rs -> libeufin-ebics/src/dialect.rs | 0
Rcrates/libeufin-ebics/src/ebics.rs -> libeufin-ebics/src/ebics.rs | 0
Rcrates/libeufin-ebics/src/ebics/administrative.rs -> libeufin-ebics/src/ebics/administrative.rs | 0
Rcrates/libeufin-ebics/src/ebics/bts.rs -> libeufin-ebics/src/ebics/bts.rs | 0
Rcrates/libeufin-ebics/src/ebics/ebics_code.rs -> libeufin-ebics/src/ebics/ebics_code.rs | 0
Rcrates/libeufin-ebics/src/ebics/key_management.rs -> libeufin-ebics/src/ebics/key_management.rs | 0
Rcrates/libeufin-ebics/src/ebics/logger.rs -> libeufin-ebics/src/ebics/logger.rs | 0
Alibeufin-ebics/src/ebics/order.rs | 270+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alibeufin-ebics/src/iso20022.rs | 183+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rcrates/libeufin-ebics/src/iso20022/bank_tx_code.rs -> libeufin-ebics/src/iso20022/bank_tx_code.rs | 0
Alibeufin-ebics/src/iso20022/camt.rs | 1249+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alibeufin-ebics/src/iso20022/hac.rs | 198+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rcrates/libeufin-ebics/src/iso20022/model.rs -> libeufin-ebics/src/iso20022/model.rs | 0
Alibeufin-ebics/src/iso20022/pain001.rs | 246+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alibeufin-ebics/src/iso20022/pain002.rs | 270+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rcrates/libeufin-ebics/src/iso20022/status_code.rs -> libeufin-ebics/src/iso20022/status_code.rs | 0
Rcrates/libeufin-ebics/src/keys.rs -> libeufin-ebics/src/keys.rs | 0
Rcrates/libeufin-ebics/src/lib.rs -> libeufin-ebics/src/lib.rs | 0
Dlibeufin-ebics/src/main/kotlin/tech/libeufin/ebics/EBicsLogger.kt | 143-------------------------------------------------------------------------------
Dlibeufin-ebics/src/main/kotlin/tech/libeufin/ebics/EbicsAdministrative.kt | 169-------------------------------------------------------------------------------
Dlibeufin-ebics/src/main/kotlin/tech/libeufin/ebics/EbicsBTS.kt | 341-------------------------------------------------------------------------------
Dlibeufin-ebics/src/main/kotlin/tech/libeufin/ebics/EbicsCommon.kt | 437-------------------------------------------------------------------------------
Dlibeufin-ebics/src/main/kotlin/tech/libeufin/ebics/EbicsConstants.kt | 114-------------------------------------------------------------------------------
Dlibeufin-ebics/src/main/kotlin/tech/libeufin/ebics/EbicsDAO.kt | 49-------------------------------------------------
Dlibeufin-ebics/src/main/kotlin/tech/libeufin/ebics/EbicsKeyMng.kt | 202-------------------------------------------------------------------------------
Dlibeufin-ebics/src/main/kotlin/tech/libeufin/ebics/cli.kt | 31-------------------------------
Dlibeufin-ebics/src/main/kotlin/tech/libeufin/ebics/config.kt | 40----------------------------------------
Dlibeufin-ebics/src/main/kotlin/tech/libeufin/ebics/db/Database.kt | 39---------------------------------------
Dlibeufin-ebics/src/main/kotlin/tech/libeufin/ebics/http.kt | 78------------------------------------------------------------------------------
Dlibeufin-ebics/src/main/kotlin/tech/libeufin/ebics/keys.kt | 223-------------------------------------------------------------------------------
Dlibeufin-ebics/src/main/kotlin/tech/libeufin/ebics/order.kt | 161-------------------------------------------------------------------------------
Dlibeufin-ebics/src/main/kotlin/tech/libeufin/ebics/pdf.kt | 115-------------------------------------------------------------------------------
Dlibeufin-ebics/src/main/kotlin/tech/libeufin/ebics/setup.kt | 234-------------------------------------------------------------------------------
Dlibeufin-ebics/src/main/kotlin/tech/libeufin/ebics/test/TxCheck.kt | 92-------------------------------------------------------------------------------
Dlibeufin-ebics/src/main/kotlin/tech/libeufin/ebics/test/ebics.kt | 486-------------------------------------------------------------------------------
Dlibeufin-ebics/src/main/kotlin/tech/libeufin/ebics/ws.kt | 207-------------------------------------------------------------------------------
Dlibeufin-ebics/src/main/kotlin/tech/libeufin/ebics/xml.kt | 376-------------------------------------------------------------------------------
Rcrates/libeufin-ebics/src/pdf.rs -> libeufin-ebics/src/pdf.rs | 0
Rcrates/libeufin-ebics/src/setup.rs -> libeufin-ebics/src/setup.rs | 0
Rcrates/libeufin-ebics/src/test.rs -> libeufin-ebics/src/test.rs | 0
Dlibeufin-ebics/src/test/kotlin/EbicsTest.kt | 87-------------------------------------------------------------------------------
Dlibeufin-ebics/src/test/kotlin/Keys.kt | 100-------------------------------------------------------------------------------
Dlibeufin-ebics/src/test/kotlin/MySerializers.kt | 48------------------------------------------------
Dlibeufin-ebics/src/test/kotlin/WsTest.kt | 187-------------------------------------------------------------------------------
Dlibeufin-ebics/src/test/kotlin/XmlTest.kt | 153-------------------------------------------------------------------------------
Dlibeufin-ebics/src/test/resources/signature1/doc.xml | 10----------
Dlibeufin-ebics/src/test/resources/signature1/public_key.txt | 2--
Rcrates/libeufin-ebics/src/utils.rs -> libeufin-ebics/src/utils.rs | 0
Rcrates/libeufin-ebics/src/ws.rs -> libeufin-ebics/src/ws.rs | 0
Rcrates/libeufin-ebics/src/xml.rs -> libeufin-ebics/src/xml.rs | 0
Rcrates/libeufin-ebics/src/xml_sign.rs -> libeufin-ebics/src/xml_sign.rs | 0
Dlibeufin-ebisync/build.gradle | 74--------------------------------------------------------------------------
Dlibeufin-ebisync/conf/test.conf | 15---------------
Dlibeufin-ebisync/ebisync.conf | 96-------------------------------------------------------------------------------
Dlibeufin-ebisync/src/main/kotlin/tech/libeufin/ebisync/Main.kt | 70----------------------------------------------------------------------
Dlibeufin-ebisync/src/main/kotlin/tech/libeufin/ebisync/api/SyncApi.kt | 125-------------------------------------------------------------------------------
Dlibeufin-ebisync/src/main/kotlin/tech/libeufin/ebisync/azure.kt | 193-------------------------------------------------------------------------------
Dlibeufin-ebisync/src/main/kotlin/tech/libeufin/ebisync/cli/DbInit.kt | 45---------------------------------------------
Dlibeufin-ebisync/src/main/kotlin/tech/libeufin/ebisync/cli/Fetch.kt | 242-------------------------------------------------------------------------------
Dlibeufin-ebisync/src/main/kotlin/tech/libeufin/ebisync/cli/LibeufinEbisync.kt | 54------------------------------------------------------
Dlibeufin-ebisync/src/main/kotlin/tech/libeufin/ebisync/cli/Serve.kt | 81-------------------------------------------------------------------------------
Dlibeufin-ebisync/src/main/kotlin/tech/libeufin/ebisync/cli/Setup.kt | 136-------------------------------------------------------------------------------
Dlibeufin-ebisync/src/main/kotlin/tech/libeufin/ebisync/config.kt | 139-------------------------------------------------------------------------------
Dlibeufin-ebisync/src/main/kotlin/tech/libeufin/ebisync/constants.kt | 26--------------------------
Dlibeufin-ebisync/src/main/kotlin/tech/libeufin/ebisync/db/Database.kt | 38--------------------------------------
Dlibeufin-ebisync/src/main/kotlin/tech/libeufin/ebisync/db/KvDAO.kt | 87-------------------------------------------------------------------------------
Dlibeufin-ebisync/src/spa/index.html | 512-------------------------------------------------------------------------------
Dlibeufin-ebisync/src/test/kotlin/DatabaseTest.kt | 47-----------------------------------------------
Dlibeufin-ebisync/src/test/kotlin/EbicsTest.kt | 68--------------------------------------------------------------------
Dlibeufin-ebisync/src/test/kotlin/SyncApiTest.kt | 101-------------------------------------------------------------------------------
Dlibeufin-ebisync/src/test/kotlin/helpers.kt | 107-------------------------------------------------------------------------------
Rcrates/libeufin-nexus/Cargo.toml -> libeufin-nexus/Cargo.toml | 0
Dlibeufin-nexus/codegen.py | 158-------------------------------------------------------------------------------
Mlibeufin-nexus/conf/auth.conf | 2+-
Mlibeufin-nexus/conf/fetch.conf | 4++--
Mlibeufin-nexus/conf/gls.conf | 4++--
Mlibeufin-nexus/conf/maerki_baumann.conf | 4++--
Mlibeufin-nexus/conf/mini.conf | 4++--
Mlibeufin-nexus/conf/skip.conf | 2+-
Mlibeufin-nexus/conf/test.conf | 2+-
Mlibeufin-nexus/conf/valiant.conf | 4++--
Rcrates/libeufin-nexus/src/api.rs -> libeufin-nexus/src/api.rs | 0
Rcrates/libeufin-nexus/src/bench.rs -> libeufin-nexus/src/bench.rs | 0
Rcrates/libeufin-nexus/src/config.rs -> libeufin-nexus/src/config.rs | 0
Rcrates/libeufin-nexus/src/db.rs -> libeufin-nexus/src/db.rs | 0
Rcrates/libeufin-nexus/src/db/exchange.rs -> libeufin-nexus/src/db/exchange.rs | 0
Alibeufin-nexus/src/db/initiated.rs | 915+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rcrates/libeufin-nexus/src/db/list.rs -> libeufin-nexus/src/db/list.rs | 0
Rcrates/libeufin-nexus/src/db/payment.rs -> libeufin-nexus/src/db/payment.rs | 0
Rcrates/libeufin-nexus/src/db/transfer.rs -> libeufin-nexus/src/db/transfer.rs | 0
Rcrates/libeufin-nexus/src/fetch.rs -> libeufin-nexus/src/fetch.rs | 0
Rcrates/libeufin-nexus/src/lib.rs -> libeufin-nexus/src/lib.rs | 0
Rcrates/libeufin-nexus/src/list.rs -> libeufin-nexus/src/list.rs | 0
Rcrates/libeufin-nexus/src/main.rs -> libeufin-nexus/src/main.rs | 0
Dlibeufin-nexus/src/main/kotlin/tech/libeufin/nexus/Config.kt | 199-------------------------------------------------------------------------------
Dlibeufin-nexus/src/main/kotlin/tech/libeufin/nexus/Constants.kt | 25-------------------------
Dlibeufin-nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt | 92-------------------------------------------------------------------------------
Dlibeufin-nexus/src/main/kotlin/tech/libeufin/nexus/api/ObservabilityApi.kt | 202-------------------------------------------------------------------------------
Dlibeufin-nexus/src/main/kotlin/tech/libeufin/nexus/api/PreparedTransferApi.kt | 149-------------------------------------------------------------------------------
Dlibeufin-nexus/src/main/kotlin/tech/libeufin/nexus/api/RevenueApi.kt | 76----------------------------------------------------------------------------
Dlibeufin-nexus/src/main/kotlin/tech/libeufin/nexus/api/WireGatewayApi.kt | 307-------------------------------------------------------------------------------
Dlibeufin-nexus/src/main/kotlin/tech/libeufin/nexus/api/helpers.kt | 47-----------------------------------------------
Dlibeufin-nexus/src/main/kotlin/tech/libeufin/nexus/cli/DbInit.kt | 44--------------------------------------------
Dlibeufin-nexus/src/main/kotlin/tech/libeufin/nexus/cli/EbicsFetch.kt | 569-------------------------------------------------------------------------------
Dlibeufin-nexus/src/main/kotlin/tech/libeufin/nexus/cli/EbicsSetup.kt | 200-------------------------------------------------------------------------------
Dlibeufin-nexus/src/main/kotlin/tech/libeufin/nexus/cli/EbicsSubmit.kt | 161-------------------------------------------------------------------------------
Dlibeufin-nexus/src/main/kotlin/tech/libeufin/nexus/cli/InitiatePayment.kt | 78------------------------------------------------------------------------------
Dlibeufin-nexus/src/main/kotlin/tech/libeufin/nexus/cli/LibeufinNexus.kt | 59-----------------------------------------------------------
Dlibeufin-nexus/src/main/kotlin/tech/libeufin/nexus/cli/List.kt | 172-------------------------------------------------------------------------------
Dlibeufin-nexus/src/main/kotlin/tech/libeufin/nexus/cli/Manual.kt | 162-------------------------------------------------------------------------------
Dlibeufin-nexus/src/main/kotlin/tech/libeufin/nexus/cli/Serve.kt | 73-------------------------------------------------------------------------
Dlibeufin-nexus/src/main/kotlin/tech/libeufin/nexus/db/Database.kt | 136-------------------------------------------------------------------------------
Dlibeufin-nexus/src/main/kotlin/tech/libeufin/nexus/db/ExchangeDAO.kt | 237-------------------------------------------------------------------------------
Dlibeufin-nexus/src/main/kotlin/tech/libeufin/nexus/db/InitiatedDAO.kt | 393-------------------------------------------------------------------------------
Dlibeufin-nexus/src/main/kotlin/tech/libeufin/nexus/db/KvDAO.kt | 89-------------------------------------------------------------------------------
Dlibeufin-nexus/src/main/kotlin/tech/libeufin/nexus/db/ListDAO.kt | 245-------------------------------------------------------------------------------
Dlibeufin-nexus/src/main/kotlin/tech/libeufin/nexus/db/PaymentDAO.kt | 280-------------------------------------------------------------------------------
Dlibeufin-nexus/src/main/kotlin/tech/libeufin/nexus/db/TransferDAO.kt | 85-------------------------------------------------------------------------------
Dlibeufin-nexus/src/main/kotlin/tech/libeufin/nexus/dialect.kt | 103-------------------------------------------------------------------------------
Dlibeufin-nexus/src/main/kotlin/tech/libeufin/nexus/helpers.kt | 30------------------------------
Dlibeufin-nexus/src/main/kotlin/tech/libeufin/nexus/iso20022/BankTransactionCode.kt | 386-------------------------------------------------------------------------------
Dlibeufin-nexus/src/main/kotlin/tech/libeufin/nexus/iso20022/Constants.kt | 45---------------------------------------------
Dlibeufin-nexus/src/main/kotlin/tech/libeufin/nexus/iso20022/ExternalCodeSets.kt | 478-------------------------------------------------------------------------------
Dlibeufin-nexus/src/main/kotlin/tech/libeufin/nexus/iso20022/camt.kt | 713-------------------------------------------------------------------------------
Dlibeufin-nexus/src/main/kotlin/tech/libeufin/nexus/iso20022/hac.kt | 74--------------------------------------------------------------------------
Dlibeufin-nexus/src/main/kotlin/tech/libeufin/nexus/iso20022/pain001.kt | 142-------------------------------------------------------------------------------
Dlibeufin-nexus/src/main/kotlin/tech/libeufin/nexus/iso20022/pain002.kt | 153-------------------------------------------------------------------------------
Alibeufin-nexus/src/manual.rs | 164+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rcrates/libeufin-nexus/src/model.rs -> libeufin-nexus/src/model.rs | 0
Alibeufin-nexus/src/test.rs | 490+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dlibeufin-nexus/src/test/kotlin/CliTest.kt | 95-------------------------------------------------------------------------------
Dlibeufin-nexus/src/test/kotlin/DatabaseTest.kt | 1327-------------------------------------------------------------------------------
Dlibeufin-nexus/src/test/kotlin/EbicsTest.kt | 200-------------------------------------------------------------------------------
Dlibeufin-nexus/src/test/kotlin/Iso20022Test.kt | 688-------------------------------------------------------------------------------
Dlibeufin-nexus/src/test/kotlin/ObservabilityTest.kt | 40----------------------------------------
Dlibeufin-nexus/src/test/kotlin/OpenApiTest.kt | 59-----------------------------------------------------------
Dlibeufin-nexus/src/test/kotlin/PreparedTransferApiTest.kt | 204-------------------------------------------------------------------------------
Dlibeufin-nexus/src/test/kotlin/RegistrationTest.kt | 767-------------------------------------------------------------------------------
Dlibeufin-nexus/src/test/kotlin/RevenueApiTest.kt | 75---------------------------------------------------------------------------
Dlibeufin-nexus/src/test/kotlin/WireGatewayApiTest.kt | 493-------------------------------------------------------------------------------
Dlibeufin-nexus/src/test/kotlin/bench.kt | 240-------------------------------------------------------------------------------
Dlibeufin-nexus/src/test/kotlin/helpers.kt | 310-------------------------------------------------------------------------------
Dlibeufin-nexus/src/test/kotlin/routines.kt | 84-------------------------------------------------------------------------------
Rcrates/libeufin-nexus/src/testing.rs -> libeufin-nexus/src/testing.rs | 0
Dsettings.gradle | 8--------
Atestbench/Cargo.toml | 25+++++++++++++++++++++++++
Mtestbench/conf/cli.conf | 6+++---
Mtestbench/conf/integration.conf | 4++--
Mtestbench/conf/mini.conf | 8++++----
Mtestbench/conf/test.conf | 2+-
Atestbench/src/main.rs | 506+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dtestbench/src/main/kotlin/Main.kt | 397-------------------------------------------------------------------------------
Dtestbench/src/test/kotlin/CliTest.kt | 137-------------------------------------------------------------------------------
Dtestbench/src/test/kotlin/IntegrationTest.kt | 448-------------------------------------------------------------------------------
Dtestbench/src/test/kotlin/Iso20022Test.kt | 130-------------------------------------------------------------------------------
Dtestbench/src/test/kotlin/MigrationTest.kt | 230-------------------------------------------------------------------------------
402 files changed, 10300 insertions(+), 74085 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "3" -members = ["crates/*"] +members = ["libeufin-bank", "libeufin-ebics", "libeufin-nexus", "testbench"] [workspace.package] version = "1.5.0" @@ -35,7 +35,7 @@ sqlx = { version = "0.8", default-features = false, features = [ aws-lc-rs = "1.15" compact_str = { version = "0.9.0", features = ["serde", "sqlx-postgres"] } reqwest = "0.13.2" -libeufin-ebics = { path = "crates/libeufin-ebics" } +libeufin-ebics = { path = "libeufin-ebics" } jiff = { version = "0.2", default-features = false, features = ["tz-system"] } clap = { version = "4.5", features = ["derive"] } uuid = { version = "1.0", features = ["v4", "fast-rng", "serde"] } diff --git a/Makefile b/Makefile @@ -1,38 +1,19 @@ # This Makefile has been placed under the public domain - --include .config.mk - -# Default target, must be at the top. -# Should be changed with care to not break (Debian) packaging. -all: build - -archive = ./build-system/archive.py +-include build-system/config.mk # Absolute DESTDIR or empty string if DESTDIR unset/empty abs_destdir=$(abspath $(DESTDIR)) -share_dir=$(abs_destdir)$(prefix)/share -man_dir=$(share_dir)/man bin_dir=$(abs_destdir)$(prefix)/bin lib_dir=$(abs_destdir)$(prefix)/lib +share_dir=$(abs_destdir)$(prefix)/share +man_dir=$(share_dir)/man +all: build -# While the gradle command sounds like it's installing something, -# it's like a destdir install that only touches the source tree. .PHONY: build build: - ./gradlew libeufin-bank:installShadowDist libeufin-nexus:installShadowDist libeufin-ebisync:installShadowDist - - -.PHONY: dist -dist: - mkdir -p build/distributions - version="$$(./gradlew -q libeufinVersion)" && \ - $(archive) --include ./configure "build/distributions/libeufin-$$version-sources.tar.gz" - -.PHONY: deb -deb: - dpkg-buildpackage -rfakeroot -b -uc -us + cargo build --release --bin libeufin-bank --bin libeufin-nexus .PHONY: install-nobuild-files install-nobuild-files: @@ -43,102 +24,40 @@ install-nobuild-files: install -m 644 -D -t $(share_dir)/libeufin/sql database-versioning/libeufin-bank*.sql install -m 644 -D -t $(share_dir)/libeufin/sql database-versioning/libeufin-nexus*.sql install -m 644 -D -t $(share_dir)/libeufin/sql database-versioning/libeufin-conversion*.sql - install -m 644 -D -t $(share_dir)/libeufin-ebisync/config.d libeufin-ebisync/ebisync.conf - install -m 644 -D -t $(share_dir)/libeufin-ebisync/sql database-versioning/versioning.sql - install -m 644 -D -t $(share_dir)/libeufin-ebisync/sql database-versioning/libeufin-ebisync*.sql install -D -t $(bin_dir) contrib/libeufin-dbconfig install -D -t $(bin_dir) contrib/libeufin-ebisync-dbconfig install -D -t $(bin_dir) contrib/libeufin-tan-*.sh install -d $(share_dir)/libeufin/spa - cp contrib/spa/* $(share_dir)/libeufin/spa/ - install -d $(share_dir)/libeufin-ebisync/spa - cp libeufin-ebisync/src/spa/* $(share_dir)/libeufin-ebisync/spa/ + cp contrib/wallet-core/bank/* $(share_dir)/libeufin/spa/ .PHONY: install install: build install-nobuild-files # Install libeufin-bank - install -D -t $(bin_dir) libeufin-bank/build/install/libeufin-bank-shadow/bin/libeufin-bank + install -D -t $(bin_dir) target/release/libeufin-bank install -m 644 -D -t $(man_dir)/man1 doc/prebuilt/man/libeufin-bank.1 install -m 644 -D -t $(man_dir)/man5 doc/prebuilt/man/libeufin-bank.conf.5 - install -m 644 -D -t $(lib_dir) libeufin-bank/build/install/libeufin-bank-shadow/lib/libeufin-bank-all.jar # Install libeufin-nexus - install -D -t $(bin_dir) libeufin-nexus/build/install/libeufin-nexus-shadow/bin/libeufin-nexus + install -D -t $(bin_dir) target/release/libeufin-nexus install -m 644 -D -t $(man_dir)/man1 doc/prebuilt/man/libeufin-nexus.1 install -m 644 -D -t $(man_dir)/man5 doc/prebuilt/man/libeufin-nexus.conf.5 - install -m 644 -D -t $(lib_dir) libeufin-nexus/build/install/libeufin-nexus-shadow/lib/libeufin-nexus-all.jar -# Install libeufin-ebisync - install -D -t $(bin_dir) libeufin-ebisync/build/install/libeufin-ebisync-shadow/bin/libeufin-ebisync - install -m 644 -D -t $(lib_dir) libeufin-ebisync/build/install/libeufin-ebisync-shadow/lib/libeufin-ebisync-all.jar - -.PHONY: assemble -assemble: - ./gradlew assemble .PHONY: check check: install-nobuild-files - ./gradlew check - -.PHONY: bank-test -bank-test: install-nobuild-files - ./gradlew :libeufin-bank:test --tests $(test) -i - -.PHONY: nexus-test -nexus-test: install-nobuild-files - ./gradlew :libeufin-nexus:test --tests $(test) -i - -.PHONY: common-test -common-test: install-nobuild-files - ./gradlew :libeufin-common:test --tests $(test) -i - -.PHONY: ebics-test -ebics-test: install-nobuild-files - ./gradlew :libeufin-ebics:test --tests $(test) -i - -.PHONY: testbench-test -testbench-test: install-nobuild-files - ./gradlew :testbench:test --tests $(test) -i - -.PHONY: ebisync-test -ebisync-test: install-nobuild-files - ./gradlew :libeufin-ebisync:test --tests $(test) -i - -.PHONY: nexus-testbench -nexus-testbench: install-nobuild-files - ./gradlew :testbench:install && \ - cd testbench && \ - ./build/install/libeufin-testbench-test/bin/libeufin-testbench-test nexus $(platform) - -.PHONY: ebisync-testbench -ebisync-testbench: install-nobuild-files - ./gradlew :testbench:install && \ - cd testbench && \ - ./build/install/libeufin-testbench-test/bin/libeufin-testbench-test ebisync $(platform) + cargo clippy --all-targets + cargo test .PHONY: doc doc: - ./gradlew :dokkaGeneratePublicationHtml - echo "Open build/dokka/html/index.html" + cargo doc + +.PHONY: deb +deb: + dpkg-buildpackage -b -uc -us .PHONY: ci ci: contrib/ci/run-all-jobs.sh -.PHONY: bank-bench-db -bank-bench-db: install-nobuild-files - ./gradlew cleanTest :libeufin-bank:test --tests Bench.benchDb -i --no-build-cache - -.PHONY: nexus-bench-db -nexus-bench-db: install-nobuild-files - ./gradlew cleanTest :libeufin-nexus:test --tests Bench.benchDb -i --no-build-cache - -.PHONY: rust-install -rust-install: install-nobuild-files - cargo build --release --bin libeufin-bank --bin libeufin-nexus - install -D -t $(bin_dir) target/release/libeufin-bank - install -D -t $(bin_dir) target/release/libeufin-nexus - -.PHONY: rust-check -rust-check: install-nobuild-files - cargo clippy --all-targets - cargo test - +.PHONY: deb +deb: + dpkg-buildpackage -rfakeroot -b -uc -us +\ No newline at end of file diff --git a/README b/README @@ -25,7 +25,7 @@ $PFX/bin as well. Running tests ============= -Tests need a PostgreSQL database called "libeufincheck". +Tests need a PostgreSQL database called "taler_rust_check". If the database setup is correct and LibEuFin is installed, the following command runs all the test cases: diff --git a/contrib/bump-version b/contrib/bump-version @@ -58,5 +58,18 @@ if not dry and deb_current_version != version: with open("debian/changelog", "w") as f: f.write(new_changelog) -# The program version is derived from Git tags at build time; there is no -# Gradle version literal to bump. +# Bump version in Cargo.toml + +with open("Cargo.toml") as f: + contents = f.read() +cargo_pat = r'version.*=.*"(.*)"' +m = re.search(cargo_pat, contents) +cargo_current_version = m.group(1) + +new_contents = re.sub(cargo_pat, f'version = "{new_version}"', contents, count=1) +cargo_bump = " [!]" if cargo_current_version != version else "" +print(f"Cargo.toml: {cargo_current_version} -> {version}{cargo_bump}") + +if not dry: + with open("Cargo.toml", "w") as f: + f.write(new_contents) diff --git a/contrib/ci/Containerfile b/contrib/ci/Containerfile @@ -1,30 +1,24 @@ FROM docker.io/library/debian:trixie -ENV DEBIAN_FRONTEND=noninteractive \ - # Persistent gradle cache - GRADLE_USER_HOME=/workdir/.gradle +ENV DEBIAN_FRONTEND=noninteractiveA + # Apt sync RUN apt-get update -yq && \ apt-get upgrade -yq && \ + # Install build & test dependencies apt-get install -yq \ - unzip \ - default-jdk-headless \ - make \ - po-debconf \ - build-essential \ - debhelper-compat \ - devscripts \ - python3 \ - git-buildpackage \ - postgresql \ - sudo \ - curl - -# Install dependencies provided by other Taler repositories. -RUN curl -sSL https://taler.net/taler-systems.gpg \ - -o /etc/apt/keyrings/taler-systems.gpg && \ - echo 'deb [signed-by=/etc/apt/keyrings/taler-systems.gpg] https://deb.taler.net/apt/debian trixie-testing main' \ - > /etc/apt/sources.list.d/taler.list + rustup \ + git \ + python3 \ + sudo \ + man \ + make \ + postgresql \ + debhelper \ + build-essential \ + git-buildpackage && \ + # Install rustup toolchain + rustup default stable WORKDIR /workdir diff --git a/contrib/ci/ci.sh b/contrib/ci/ci.sh @@ -26,7 +26,8 @@ fi --arch "${JOB_ARCH}" \ --env CI_COMMIT_REF="$(git rev-parse HEAD)" \ --volume "${PWD}":/workdir \ - --volume "${HOME}/.gradle/caches:/root/.gradle/caches" \ + --volume "${HOME}/.cargo/git:/root/.cargo/git" \ + --volume "${HOME}/.cargo/registry:/root/.cargo/registry" \ --workdir /workdir \ "${JOB_CONTAINER}" \ contrib/ci/jobs/"${JOB_NAME}"/job.sh diff --git a/contrib/ci/deb-test.sh b/contrib/ci/deb-test.sh @@ -32,7 +32,7 @@ libeufin-ebisync-dbconfig -r -c testbench/conf/mini.conf for USER in $ITEMS; do step "Check $USER db access" - sudo -u $USER psql -d libeufincheck -c "SELECT 1;" &> /dev/null + sudo -u $USER psql -d taler_rust_check -c "SELECT 1;" &> /dev/null done step "Check man pages" diff --git a/contrib/ci/jobs/0-codespell/dictionary.txt b/contrib/ci/jobs/0-codespell/dictionary.txt @@ -5,17 +5,7 @@ # 'foo' and you add 'Foo' _here_, codespell will continue to complain # about 'Foo'. # -ifset -bu -fIDN -ECT -complet -ges -UE -Te -optin -claus -pres +regio haa -registerIn -checkIn -\ No newline at end of file +hve +ser +\ No newline at end of file diff --git a/contrib/ci/jobs/0-codespell/job.sh b/contrib/ci/jobs/0-codespell/job.sh @@ -12,16 +12,15 @@ configure~ */debian/.debhelper/* */doc/prebuilt/* */.git/* -*/.gradle/* +*/target/* */contrib/ci/* -*/frontend/* -*/build/* +*/contrib/wallet-core/* */*.xsd */*.xml -*/libeufin-nexus/src/main/kotlin/tech/libeufin/nexus/iso20022/ExternalCodeSets.kt -*/libeufin-nexus/src/main/kotlin/tech/libeufin/nexus/iso20022/BankTransactionCode.kt -*/libeufin-common/src/main/kotlin/registry.kt */testbench/test/* +./libeufin-ebics/src/iso20022/bank_tx_code.rs +./libeufin-ebics/src/iso20022/status_code.rs +Cargo.lock EOF ); diff --git a/contrib/ci/jobs/2-test/job.sh b/contrib/ci/jobs/2-test/job.sh @@ -11,7 +11,7 @@ apt-get upgrade -yq # Setup postgres cluster sudo -u postgres pg_ctlcluster 17 main start sudo -u postgres createuser root --superuser -sudo -u postgres createdb -O root libeufincheck +sudo -u postgres createdb -O root taler_rust_check\ check_command() { diff --git a/contrib/ci/jobs/3-docs/job.sh b/contrib/ci/jobs/3-docs/job.sh @@ -6,5 +6,4 @@ apt-get update -yq apt-get upgrade -yq # Build documentation -# Why is doc always failling -# make doc +# make doc TODO understand why it crash diff --git a/contrib/ci/jobs/4-deb-amd64/config.ini b/contrib/ci/jobs/4-deb-amd64/config.ini @@ -0,0 +1,6 @@ +[build] +HALT_ON_FAILURE = True +WARN_ON_FAILURE = True +CONTAINER_BUILD = True +CONTAINER_NAME = localhost/libeufin:amd64 +CONTAINER_ARCH = amd64 diff --git a/contrib/ci/jobs/4-deb-amd64/job.sh b/contrib/ci/jobs/4-deb-amd64/job.sh @@ -0,0 +1,20 @@ +#!/bin/bash +set -exuo pipefail + +# Update system +apt-get update -yq +apt-get upgrade -yq + +# Build package +export VERSION="$(./contrib/ci/version.sh)" +echo "Building package version ${VERSION}" +EMAIL=none gbp dch --dch-opt=-b --ignore-branch --debian-tag="%(version)s" --git-author --new-version="${VERSION}" +make deb + +# Test package +sudo ./contrib/ci/deb-test.sh + +# Move to artifact +ls -alh ../*.deb +mkdir -p /artifacts/libeufin/${CI_COMMIT_REF} +mv ../*.deb /artifacts/libeufin/${CI_COMMIT_REF} +\ No newline at end of file diff --git a/contrib/ci/jobs/4-deb/job.sh b/contrib/ci/jobs/4-deb/job.sh @@ -1,21 +0,0 @@ -#!/bin/bash -set -exuo pipefail -# Update system -apt-get update -yq -apt-get upgrade -yq - -# Build package -VERSION="$(./contrib/ci/version.sh)" -export VERSION -: "${VERSION:?version generation returned an empty version}" -echo "Building package version ${VERSION}" -EMAIL=none gbp dch --dch-opt=-b --ignore-branch --debian-tag="%(version)s" --git-author --new-version="${VERSION}" -make deb - -# Test package -sudo ./contrib/ci/deb-test.sh - -# Move to artifact -ls -alh ../*.deb -mkdir -p /artifacts/libeufin/${CI_COMMIT_REF} # Variable comes from CI environment -mv ../*.deb /artifacts/libeufin/${CI_COMMIT_REF}/ diff --git a/contrib/ci/jobs/4-deb/version.sh b/contrib/ci/jobs/4-deb/version.sh @@ -1,6 +0,0 @@ -#!/bin/sh -# This file is in the public domain. -# Compatibility entry point, also usable in generated job directories. -set -eu -repo_dir=$(git rev-parse --show-toplevel) -exec "$repo_dir/contrib/ci/version.sh" "$@" diff --git a/contrib/ci/jobs/5-deb-arm64/config.ini b/contrib/ci/jobs/5-deb-arm64/config.ini @@ -0,0 +1,6 @@ +[build] +HALT_ON_FAILURE = True +WARN_ON_FAILURE = True +CONTAINER_BUILD = True +CONTAINER_NAME = localhost/libeufin:arm64 +CONTAINER_ARCH = arm64 diff --git a/contrib/ci/jobs/5-deb-arm64/job.sh b/contrib/ci/jobs/5-deb-arm64/job.sh @@ -0,0 +1 @@ +../4-deb-amd64/job.sh +\ No newline at end of file diff --git a/contrib/ci/jobs/5-deploy/config.ini b/contrib/ci/jobs/6-deploy/config.ini diff --git a/contrib/ci/jobs/5-deploy/job.sh b/contrib/ci/jobs/6-deploy/job.sh diff --git a/contrib/ci/version.sh b/contrib/ci/version.sh @@ -1,6 +1,6 @@ #!/bin/sh # This file is in the public domain. -# Print the Debian CI version; diagnostics go to stderr. +# Compatibility entry point, also usable in generated job directories. set -eu -script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) -exec python3 "$script_dir/debian-version.py" "$@" +repo_dir=$(git rev-parse --show-toplevel) +exec "$repo_dir/contrib/ci/version.sh" "$@" diff --git a/contrib/docker-launcher/Dockerfile b/contrib/docker-launcher/Dockerfile @@ -14,8 +14,8 @@ COPY launch-bank.sh /launch-bank.sh COPY libeufin-bank.conf /libeufin-bank.conf RUN apt-get install -y sudo RUN grep -v ^host.*all /etc/postgresql/15/main/pg_hba.conf > /tmp/pg_hba_buf.txt -RUN echo "host libeufincheck all 127.0.0.1/32 trust" >> /tmp/pg_hba_buf.txt -RUN echo "host libeufincheck all ::1/128 trust" >> /tmp/pg_hba_buf.txt +RUN echo "host taler_rust_check all 127.0.0.1/32 trust" >> /tmp/pg_hba_buf.txt +RUN echo "host taler_rust_check all ::1/128 trust" >> /tmp/pg_hba_buf.txt RUN cp /tmp/pg_hba_buf.txt /etc/postgresql/15/main/pg_hba.conf ENTRYPOINT ["/launch-bank.sh"] diff --git a/crates/libeufin-bank/src/api.rs b/crates/libeufin-bank/src/api.rs @@ -1,934 +0,0 @@ -/* -* This file is part of LibEuFin. -* Copyright (C) 2026 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/> -*/ - -use std::sync::Arc; - -use axum::{ - Json, Router, - extract::State, - response::{IntoResponse, Redirect, Response}, - routing::get, -}; -use serde::{Deserialize, Serialize}; -use sqlx::PgPool; -use taler_api::{ - api::TalerRouter as _, error::ApiResult, extract::Query, notification::NotificationChannel, -}; -use taler_common::{ - api::LibtoolVersion, - types::amount::{Amount, Currency}, -}; -use taler_macros::{EnumMeta, api_config}; -use tower_http::services::ServeDir; -use uuid::Uuid; - -use crate::{ - TanChannel, - api::{ - account::account_api, - cashout::cashout_api, - conversion::conversion_api, - prepared::prepared_api, - revenue::revenue_api, - tan::tan_api, - token::token_api, - tx::tx_api, - wire::wire_api, - withdrawal::{WithdrawalStatus, withdrawal_api}, - }, - auth::AdminRAuth, - config::{BankCfg, CurrencySpecification}, - db::{monitor, notification_listener}, -}; - -pub mod account; -pub mod cashout; -pub mod conversion; -pub mod prepared; -pub mod revenue; -pub mod tan; -pub mod token; -pub mod tx; -pub mod wire; -pub mod withdrawal; - -const IMPLEMENTATION: &str = "urn:net:taler:specs:libeufin-bank:taler-rust"; -const COREBANK_API_VERSION: LibtoolVersion = LibtoolVersion::new(12, 1, 0); -const CONVERSION_API_VERSION: LibtoolVersion = LibtoolVersion::new(2, 1, 1); -const INTEGRATION_API_VERSION: LibtoolVersion = LibtoolVersion::new(5, 1, 5); - -pub struct BankState { - pub db: PgPool, - pub cfg: BankCfg, - pub tx_channel: NotificationChannel<u64, i64>, - pub taler_out_channel: NotificationChannel<u64, i64>, - pub taler_in_channel: NotificationChannel<u64, i64>, - pub revenue_channel: NotificationChannel<u64, i64>, - pub withdrawal_channel: NotificationChannel<Uuid, WithdrawalStatus>, -} - -impl BankState { - pub async fn start(pool: PgPool, cfg: BankCfg) -> Self { - let tx_channel = NotificationChannel::new(); - let taler_in_channel = NotificationChannel::new(); - let taler_out_channel = NotificationChannel::new(); - let revenue_channel = NotificationChannel::new(); - let withdrawal_channel = NotificationChannel::new(); - tokio::spawn(notification_listener( - pool.clone(), - tx_channel.clone(), - taler_out_channel.clone(), - taler_in_channel.clone(), - revenue_channel.clone(), - withdrawal_channel.clone(), - )); - Self { - cfg, - db: pool, - tx_channel, - taler_in_channel, - taler_out_channel, - revenue_channel, - withdrawal_channel, - } - } -} - -#[api_config("taler-corebank")] -#[derive(Debug, Clone, Serialize)] -pub struct Config<'a> { - pub currency: Currency, - pub currency_specification: &'a CurrencySpecification, - pub base_url: &'a str, - pub bank_name: &'a str, - pub allow_conversion: bool, - pub allow_registrations: bool, - pub allow_deletions: bool, - pub allow_edit_name: bool, - pub allow_edit_cashout_payto_uri: bool, - pub default_debit_threshold: Amount, - pub supported_tan_channels: Vec<TanChannel>, - pub wire_type: &'a str, - pub wire_transfer_fees: Amount, - pub min_wire_transfer_amount: Amount, - pub max_wire_transfer_amount: Amount, -} - -#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, sqlx::Type, EnumMeta)] -#[sqlx(type_name = "stat_timeframe_enum")] -#[enum_meta(Str)] -#[allow(non_camel_case_types)] -pub enum Timeframe { - #[default] - hour, - day, - month, - year, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MonitorParams { - #[serde(default)] - pub timeframe: Timeframe, - pub date_s: Option<u64>, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(tag = "type")] -pub enum MonitorResponse { - #[serde(rename = "no-conversions", rename_all = "camelCase")] - Simple { - taler_in_count: u64, - taler_in_volume: Amount, - taler_out_count: u64, - taler_out_volume: Amount, - }, - #[serde(rename = "with-conversions", rename_all = "camelCase")] - Conversion { - cashin_count: u64, - cashin_regional_volume: Amount, - cashin_fiat_volume: Amount, - cashout_count: u64, - cashout_regional_volume: Amount, - cashout_fiat_volume: Amount, - taler_in_count: u64, - taler_in_volume: Amount, - taler_out_count: u64, - taler_out_volume: Amount, - }, -} - -async fn config(State(state): State<Arc<BankState>>) -> Response { - Json(Config { - name: (), - version: COREBANK_API_VERSION, - implementation: Some(IMPLEMENTATION), - currency: state.cfg.regional_currency, - currency_specification: &state.cfg.regional_currency_spec, - base_url: state.cfg.base_url.as_str(), - bank_name: &state.cfg.name, - allow_conversion: state.cfg.fiat.is_some(), - allow_registrations: state.cfg.allow_registration, - allow_deletions: state.cfg.allow_account_deletion, - allow_edit_name: state.cfg.allow_edit_name, - allow_edit_cashout_payto_uri: state.cfg.allow_edit_cashout, - default_debit_threshold: state.cfg.default_debt_limit, - supported_tan_channels: state.cfg.tan_channels.keys().copied().collect::<Vec<_>>(), - wire_type: state.cfg.wire_method.as_ref(), - wire_transfer_fees: state.cfg.wire_transfer_fees, - min_wire_transfer_amount: state.cfg.min_amount, - max_wire_transfer_amount: state.cfg.max_amount, - }) - .into_response() -} - -pub fn bank_api(state: Arc<BankState>) -> Router { - let router = Router::new().route("/config", get(config)); // TODO monitor - if let Some(path) = &state.cfg.spa_path { - router - .nest_service("/webui/", ServeDir::new(path)) - .route("/", get(async || Redirect::permanent("/webui/"))) - } else { - router - } - .route( - "/monitor", - get( - async |Query(params): Query<MonitorParams>, - _: AdminRAuth, - State(state): State<Arc<BankState>>| { - ApiResult::Ok(Json( - monitor( - &state.db, - &state.cfg.regional_currency, - state.cfg.fiat_currency(), - &params, - ) - .await?, - )) - }, - ), - ) - .merge(token_api()) - .merge(account_api()) - .merge(tx_api()) - .merge(withdrawal_api()) - .merge(cashout_api(state.clone())) - .merge(tan_api()) - .merge(conversion_api(state.clone())) - .merge(wire_api()) - .merge(prepared_api()) - .merge(revenue_api()) - .with_state(state) - .finalize() -} - -#[cfg(test)] -pub mod test { - use std::{collections::BTreeMap, fmt::Display, ops::Deref, sync::Arc}; - - use axum::{ - Router, - extract::{Request, State}, - http::{HeaderValue, Method, StatusCode, header::AUTHORIZATION}, - middleware::{self, Next}, - }; - use compact_str::CompactString; - use jiff::Timestamp; - use rand::{random_range, seq::IndexedRandom}; - use taler_api::db::BindHelper; - use taler_common::{ - api::{EddsaPublicKey, HashCode, ShortHashCode}, - config::Config, - db::dbinit, - error_code::ErrorCode, - types::{ - amount::{Amount, Decimal, decimal}, - payto::PaytoURI, - }, - }; - use taler_test_utils::{ - db::db_test_setup_manual, - json, - server::{TestRequest, TestResponse, TestServer as _}, - }; - use uuid::Uuid; - - use crate::{ - CONFIG_SOURCE, - api::{ - BankState, MonitorResponse, - account::{ - AccountData, Balance, CreditDebitInfo, create_admin_account, rand_iban_payto, - }, - bank_api, - conversion::{ConversionRateClassResponse, ConversionResponse}, - tan::ChallengeResponse, - withdrawal::BankAccountCreateWithdrawalResponse, - }, - config::BankCfg, - db::{self, account::CreationResult}, - mfa::TALER_CHALLENGE_IDS, - payto::BankPayto, - }; - - pub enum Auth { - Admin, - Optional, - UserOrAdmin, - UserOnly, - Token, - } - - pub struct BankTestCtx { - pub merchant_payto: BankPayto, - pub exchange_payto: BankPayto, - pub customer_payto: BankPayto, - pub unknown_payto: BankPayto, - pub tmp_payto: BankPayto, - pub admin_payto: BankPayto, - pub server: Router, - pub state: Arc<BankState>, - tokens: BTreeMap<CompactString, String>, - } - - impl BankTestCtx { - pub async fn new(conf: &str) -> Self { - let cfg = Config::from_file( - CONFIG_SOURCE, - Some(format!("../../libeufin-bank/conf/{conf}")), - ) - .unwrap(); - let cfg = BankCfg::parse(cfg).unwrap(); - let dir = cfg.db_cfg.sql_dir.as_ref(); - let (mut conn, db) = db_test_setup_manual(dir, "libeufin-bank").await; - dbinit(&mut conn, dir, "libeufin-bank", true).await.unwrap(); - dbinit(&mut conn, dir, "libeufin-nexus", true) - .await - .unwrap(); - let procedure = - std::fs::read_to_string(dir.join("libeufin-conversion-setup.sql")).unwrap(); - sqlx::raw_sql(&procedure).execute(&mut *conn).await.unwrap(); - drop(conn); - - let state = Arc::new(BankState::start(db.clone(), cfg).await); - let server = bank_api(state.clone()); - - let merchant_payto = db::account::create( - &db, - &state.cfg.ctx, - &state.cfg.pw_crypto, - "merchant", - "merchant-password", - "Merchant", - None, - None, - None, - &rand_iban_payto().into_inner().into(), - false, - false, - Decimal::new(10, 0).to_amount(&state.cfg.regional_currency), - Amount::zero(&state.cfg.regional_currency), - &[], - false, - None, - ) - .await - .unwrap() - .assert_success(); - let exchange_payto = db::account::create( - &db, - &state.cfg.ctx, - &state.cfg.pw_crypto, - "exchange", - "exchange-password", - "Exchange", - None, - None, - None, - &rand_iban_payto().into_inner().into(), - false, - true, - Decimal::new(10, 0).to_amount(&state.cfg.regional_currency), - Amount::zero(&state.cfg.regional_currency), - &[], - false, - None, - ) - .await - .unwrap() - .assert_success(); - let customer_payto = db::account::create( - &db, - &state.cfg.ctx, - &state.cfg.pw_crypto, - "customer", - "customer-password", - "Customer", - None, - None, - None, - &rand_iban_payto().into_inner().into(), - false, - false, - Decimal::new(10, 0).to_amount(&state.cfg.regional_currency), - Amount::zero(&state.cfg.regional_currency), - &[], - false, - None, - ) - .await - .unwrap() - .assert_success(); - - let res = create_admin_account(&db, &state.cfg, Some("admin-password")) - .await - .unwrap(); - - let admin_payto = match res { - CreationResult::Success(payto) => payto, - _ => unreachable!(), - }; - - let mut ctx = BankTestCtx { - merchant_payto: merchant_payto.into(), - exchange_payto: exchange_payto.into(), - customer_payto: customer_payto.into(), - unknown_payto: rand_iban_payto().convert(), - tmp_payto: rand_iban_payto().convert(), - admin_payto: admin_payto.into(), - server, - state, - tokens: BTreeMap::new(), - }; - ctx.cache_tokens(&["admin", "merchant", "exchange", "customer"]) - .await; - - if ctx.state.cfg.fiat.is_some() { - // Set conversion rates - ctx.post_admin("/conversion-info/conversion-rate") - .json(json!({ - "cashin_ratio" :"0.8", - "cashin_fee" :"KUDOS:0.02", - "cashin_tiny_amount" :"KUDOS:0.01", - "cashin_rounding_mode" :"nearest", - "cashin_min_amount" :"EUR:0", - "cashout_ratio" :"1.26", - "cashout_fee" :"EUR:0.003", - "cashout_tiny_amount" :"EUR:0.01", - "cashout_rounding_mode" :"zero", - "cashout_min_amount" :"KUDOS:0.1" - })) - .await - .assert_no_content(); - ctx.create_conversion_rate_class().await; - } - - ctx - } - - pub async fn swap_cfg(mut self, conf: &str) -> Self { - let cfg = Config::from_file( - CONFIG_SOURCE, - Some(format!("../../libeufin-bank/conf/{conf}")), - ) - .unwrap(); - - let state = Arc::new( - BankState::start(self.state.db.clone(), BankCfg::parse(cfg).unwrap()).await, - ); - self.server = bank_api(state.clone()); - self.state = state; - self - } - - fn pw_auth(req: TestRequest, username: Option<&str>) -> TestRequest { - let username: CompactString = username - .unwrap_or_else(|| Self::extract_username(req.url.path())) - .into(); - req.basic_auth(&username, &format!("{username}-password")) - } - - fn extract_username(path: &str) -> &str { - if path.contains("admin") { - "admin" - } else { - path.split('/').nth(2).unwrap() - } - } - - pub async fn cache_tokens(&mut self, usernames: &[&'static str]) { - let tasks = usernames - .iter() - .map(|username| { - let username = CompactString::from(*username); - async { - let res = Self::pw_auth( - self.server.post(format!("/accounts/{username}/token")), - Some("admin"), - ) - .json(json!({ - "scope": "readwrite", - "duration": { - "d_us": "forever" - } - })) - .await - .assert_ok_json::<serde_json::Value>(); - let token = res["access_token"].as_str().unwrap(); - (username, format!("Bearer {token}")) - } - }) - .collect::<Vec<_>>(); - for (username, token) in futures::future::join_all(tasks).await { - self.tokens.insert(username, token); - } - } - - pub async fn admin_router(&self) -> Router { - self.server.clone().layer(middleware::from_fn_with_state( - Arc::new(self.tokens.clone()), - async |State(tokens): State<Arc<BTreeMap<CompactString, String>>>, - mut req: Request, - next: Next| { - let path = req.uri().path(); - if path.starts_with("/accounts") && !path.contains("admin") { - let username = Self::extract_username(req.uri().path()); - let header = HeaderValue::from_str(&tokens[username]).unwrap(); - req.headers_mut().insert(AUTHORIZATION, header); - } else { - req.headers_mut().insert( - AUTHORIZATION, - HeaderValue::from_str(&tokens["admin"]).unwrap(), - ); - } - next.run(req).await - }, - )) - } - - fn requesta( - &self, - method: Method, - path: impl AsRef<str>, - username: Option<&str>, - ) -> TestRequest { - let path = path.as_ref(); - let username = username.unwrap_or_else(|| Self::extract_username(path)); - let token = &self.tokens[username]; - self.server - .request(method, path) - .header(AUTHORIZATION, token) - } - - pub fn postpw(&self, path: impl AsRef<str>) -> TestRequest { - Self::pw_auth(self.server.request(Method::POST, path), None) - } - - pub fn geta(&self, path: impl AsRef<str>) -> TestRequest { - self.requesta(Method::GET, path, None) - } - - pub fn posta(&self, path: impl AsRef<str>) -> TestRequest { - self.requesta(Method::POST, path, None) - } - - pub fn patcha(&self, path: impl AsRef<str>) -> TestRequest { - self.requesta(Method::PATCH, path, None) - } - - pub fn deletea(&self, path: impl AsRef<str>) -> TestRequest { - self.requesta(Method::DELETE, path, None) - } - - pub fn get_admin(&self, path: impl AsRef<str>) -> TestRequest { - self.requesta(Method::GET, path, Some("admin")) - } - - pub fn post_admin(&self, path: impl AsRef<str>) -> TestRequest { - self.requesta(Method::POST, path, Some("admin")) - } - - pub fn patch_admin(&self, path: impl AsRef<str>) -> TestRequest { - self.requesta(Method::PATCH, path, Some("admin")) - } - - pub fn delete_admin(&self, path: impl AsRef<str>) -> TestRequest { - self.requesta(Method::DELETE, path, Some("admin")) - } - - pub async fn auth_routine(&self, method: Method, path: impl AsRef<str>, auth: Auth) { - let path = path.as_ref(); - // Bad header - self.request(method.clone(), path) - .header(AUTHORIZATION, "WTF") - .await - .assert_error(ErrorCode::GENERIC_UNAUTHORIZED); - - if !matches!(auth, Auth::Token) { - if !matches!(auth, Auth::Optional) { - // No header - self.request(method.clone(), path) - .await - .assert_error_status( - ErrorCode::GENERIC_PARAMETER_MISSING, - StatusCode::UNAUTHORIZED, - ); - } - - // Other account - self.requesta(method.clone(), path, Some("customer")) - .await - .assert_error(ErrorCode::GENERIC_FORBIDDEN); - } - - match auth { - Auth::Admin => { - self.requesta(method.clone(), path, Some("merchant")) - .await - .assert_error(ErrorCode::GENERIC_FORBIDDEN); - } - Auth::UserOrAdmin | Auth::Token | Auth::Optional => {} - Auth::UserOnly => { - self.requesta(method.clone(), path, Some("admin")) - .await - .assert_error(ErrorCode::GENERIC_FORBIDDEN); - } - } - } - - pub async fn fill_tan_info(&self, username: &str) { - self.patch_admin(format!("/accounts/{username}")) - .json(json!({ - "contact_data": { - "phone": format!("+{}", random_range(0..10000)) - }, - "tan_channel": "sms" - })) - .await - .assert_no_content(); - } - - pub async fn fill_cashout_info(&self, username: &str) { - self.patch_admin(format!("/accounts/{username}")) - .json(json!({ - "cashout_payto_uri": self.unknown_payto, - })) - .await - .assert_no_content(); - } - - pub async fn tmp_payto(&mut self) -> PaytoURI { - self.tmp_payto = rand_iban_payto().convert(); - self.tmp_payto.as_uri() - } - - pub async fn tx_s(&self, from: &str, amount: &str, to: &str, subject: impl Display) { - let payto = match to { - "admin" => &self.admin_payto, - "merchant" => &self.merchant_payto, - "customer" => &self.customer_payto, - "exchange" => &self.exchange_payto, - _ => &self.tmp_payto, - }; - self.posta(format!("/accounts/{from}/transactions")) - .json(json!({ - "payto_uri": format!("{payto}?message={subject}"), - "amount": format!("{}:{amount}", self.state.cfg.regional_currency), - })) - .await - .maybe_challenge(self) - .await - .assert_ok(); - } - - pub async fn tx(&self, from: &str, amount: &str, to: &str) { - self.tx_s(from, amount, to, "payout").await - } - - pub async fn transfer( - &self, - amount: &str, - payto: &BankPayto, - metadata: Option<CompactString>, - ) { - self.posta("/accounts/exchange/taler-wire-gateway/transfer") - .json({ - json!({ - "request_uid": HashCode::rand(), - "amount": format!("{}:{amount}", self.state.cfg.regional_currency), - "exchange_base_url": "http://exchange.example.com/", - "wtid": ShortHashCode::rand(), - "credit_account": payto, - "metadata": metadata, - }) - }) - .await - .assert_ok() - } - - /** Perform a taler incoming transaction of [amount] from merchant to exchange */ - pub async fn add_incoming(&self, amount: &str) { - self.post_admin("/accounts/exchange/taler-wire-gateway/admin/add-incoming") - .json({ - json!({ - "amount": format!("{}:{amount}", self.state.cfg.regional_currency), - "reserve_pub": EddsaPublicKey::rand(), - "debit_account": self.merchant_payto - }) - }) - .await - .assert_ok() - } - - pub async fn add_kyc(&self, amount: &str) { - self.post_admin("/accounts/exchange/taler-wire-gateway/admin/add-kycauth") - .json({ - json!({ - "amount": format!("{}:{amount}", self.state.cfg.regional_currency), - "account_pub": EddsaPublicKey::rand(), - "debit_account": self.merchant_payto - }) - }) - .await - .assert_ok() - } - - pub async fn cashout(&self, amount: &str) { - let amount = format!("{}:{amount}", self.state.cfg.regional_currency); - self.posta("/accounts/customer/cashouts") - .json({ - json!({ - "request_uid": ShortHashCode::rand(), - "amount_debit": amount, - "amount_credit": self.convert(&amount).await - }) - }) - .await - .assert_ok() - } - - pub async fn cashin(&self, amount: &str) { - sqlx::query("SELECT 0 FROM cashin($1, $2, $3, $4)") - .bind_timestamp(&Timestamp::now()) - .bind(ShortHashCode::rand()) - .bind(decimal(amount)) - .bind("") - .fetch_one(&self.state.db) - .await - .unwrap(); - } - - pub async fn withdrawal(&self, amount: &str) { - let uuid = self - .posta("/accounts/merchant/withdrawals") - .json({ - json!({ - "amount": format!("{}:{amount}", self.state.cfg.regional_currency) - }) - }) - .await - .assert_ok_json::<BankAccountCreateWithdrawalResponse>() - .withdrawal_id; - self.withdraw_select(uuid).await; - self.posta(format!("/accounts/merchant/withdrawals/{uuid}/confirm")) - .json(json!({})) - .await - .assert_no_content(); - } - - pub async fn withdraw_select(&self, uuid: Uuid) -> EddsaPublicKey { - let key = EddsaPublicKey::rand(); - self.post(format!("/taler-integration/withdrawal-operation/{uuid}")) - .json(json!({ - "reserve_pub": key, - "selected_exchange": self.exchange_payto - })) - .await - .assert_ok(); - key - } - - pub async fn create_conversion_rate_class(&self) -> u64 { - self.post_admin("/conversion-rate-classes") - .json(json!({ - "name": format!("Gen class {}", Timestamp::now()) - })) - .await - .assert_ok_json::<ConversionRateClassResponse>() - .conversion_rate_class_id - } - - pub async fn convert(&self, amount: &str) -> Amount { - self.get(format!( - "/conversion-info/cashout-rate?amount_debit={amount}" - )) - .await - .assert_ok_json::<ConversionResponse>() - .amount_credit - } - - /** Set [account] debit threshold to [maxDebt] amount */ - pub async fn set_max_debt(&self, username: &str, amount: &str) { - self.patch_admin(format!("/accounts/{username}")) - .json(json!({ - "debit_threshold": format!("{}:{amount}", self.state.cfg.regional_currency) - })) - .await - .assert_no_content(); - } - - /** Check [account] balance is [amount], [amount] is prefixed with + for credit and - for debit */ - pub async fn assert_balance(&self, username: &str, expected: &str) { - let res: AccountData = self - .get_admin(format!("/accounts/{username}")) - .await - .assert_ok_json(); - let Balance { - amount, - credit_debit_indicator, - } = res.balance; - let prefix = match credit_debit_indicator { - CreditDebitInfo::credit => "", - CreditDebitInfo::debit => "-", - }; - pretty_assertions::assert_eq!(format!("{prefix}{}", amount.decimal()), expected); - } - } - - impl Deref for BankTestCtx { - type Target = Router; - - fn deref(&self) -> &Self::Target { - &self.server - } - } - - pub async fn bank_setup() -> BankTestCtx { - bank_setup_conf("test.conf").await - } - - pub async fn bank_setup_conf(conf: &str) -> BankTestCtx { - BankTestCtx::new(conf).await - } - - pub fn tan_code(info: &str) -> Option<CompactString> { - let path = format!("/tmp/tan-{}.txt", info); - let code = match std::fs::read_to_string(&path) { - Ok(f) => f, - Err(e) if e.kind() == std::io::ErrorKind::NotFound => return None, - Err(e) => panic!("{:?}", e), - }; - std::fs::remove_file(path).unwrap(); - Some(code.split(' ').next().unwrap().into()) - } - - pub trait MfaRequest { - fn assert_challenge_check( - &self, - ctx: &BankTestCtx, - check: impl AsyncFnOnce(&ChallengeResponse), - ) -> impl std::future::Future<Output = Self>; - - fn assert_challenge(&self, ctx: &BankTestCtx) -> impl std::future::Future<Output = Self> { - self.assert_challenge_check(ctx, async |_| {}) - } - - fn maybe_challenge(self, ctx: &BankTestCtx) -> impl std::future::Future<Output = Self>; - } - - impl MfaRequest for TestResponse { - async fn assert_challenge_check( - &self, - ctx: &BankTestCtx, - check: impl AsyncFnOnce(&ChallengeResponse), - ) -> TestResponse { - let res: ChallengeResponse = self.assert_accepted_json(); - let username = self.uri.path().split('/').nth(2).unwrap(); - - let challenges = if res.combi_and { - &res.challenges - } else { - std::slice::from_ref(res.challenges.choose(&mut rand::rng()).unwrap()) - }; - - for challenge in challenges { - ctx.posta(format!( - "/accounts/{username}/challenge/{}", - challenge.challenge_id - )) - .await - .assert_ok(); - } - check(&res).await; - - for challenge in challenges { - let code = tan_code(&challenge.tan_info).unwrap(); - ctx.posta(format!( - "/accounts/{username}/challenge/{}/confirm", - challenge.challenge_id - )) - .json(json!({ "tan": code })) - .await - .assert_no_content(); - } - // Recover body from request - let ids = res - .challenges - .into_iter() - .map(|it| it.challenge_id) - .collect::<Vec<_>>() - .join(", "); - ctx.requesta(self.method.clone(), self.uri.path(), Some(username)) - .header(TALER_CHALLENGE_IDS, ids) - .raw_json(self.req_body.clone()) - .await - } - - async fn maybe_challenge(self, ctx: &BankTestCtx) -> Self { - if self.status == StatusCode::ACCEPTED { - self.assert_challenge(ctx).await - } else { - self - } - } - } - - #[tokio::test] - async fn corebank() { - let ctx = bank_setup().await; - - ctx.auth_routine(Method::GET, "/monitor", Auth::Admin).await; - - ctx.get("/config").await.assert_ok(); - - ctx.get_admin("/monitor?timeframe=day&which=25") - .await - .assert_ok_json::<MonitorResponse>(); - ctx.get_admin("/monitor?timeframe=day=which=25") - .await - .assert_error(ErrorCode::GENERIC_PARAMETER_MALFORMED); - - ctx.fill_cashout_info("customer").await; - ctx.cashout("1").await; - ctx.get_admin("/monitor") - .await - .assert_ok_json::<MonitorResponse>(); - } -} diff --git a/crates/libeufin-bank/src/api/account.rs b/crates/libeufin-bank/src/api/account.rs @@ -1,1929 +0,0 @@ -/* -* This file is part of LibEuFin. -* Copyright (C) 2026 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/> -*/ - -use std::{ - fmt::Debug, - str::FromStr, - sync::{Arc, LazyLock}, -}; - -use axum::{ - Json, Router, - extract::State, - http::StatusCode, - response::{IntoResponse, NoContent}, - routing::{get, patch, post}, -}; -use compact_str::{CompactString, format_compact}; -use regex::Regex; -use serde::{Deserialize, Serialize}; -use sqlx::{Database, PgPool}; -use taler_api::{ - error::{ApiResult, bad_request, failure, failure_code, failure_status}, - extract::{Query, Req}, -}; -use taler_common::{ - api::params::{Page, PageParams, ParamsErr}, - error_code::ErrorCode::{self}, - types::{ - amount::Amount, - base32::Base32, - iban::{Country, IBAN}, - payto::{BankID, IbanPayto, Payto, PaytoURI}, - }, -}; -use taler_macros::EnumMeta; - -use crate::{ - TanChannel, - api::{BankState, conversion::ConversionRate}, - auth::{AdminRAuth, RegistrationAuth, UserAuth, UserRAuth, require_admin}, - config::{BankCfg, WireMethod}, - db::{ - self, - account::{ - CreationResult, DeletionResult, PatchAuthResult, PatchResult, by_username, page_admin, - page_public, reconfig, reconfig_password, - }, - }, - mfa::{AccountDeletionOp, AccountPasswordOp, AccountReconfigOp, MfaReq, Tans}, - payto::{FullBankPayto, LibeufinId, XTalerBank}, - pw::checkpw, -}; - -#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct ChallengeContactData { - #[serde(default)] - pub email: Maybe<CompactString>, - #[serde(default)] - pub phone: Maybe<CompactString>, -} - -impl ChallengeContactData { - pub fn validate(&self) -> ApiResult<()> { - static EMAIL_PATTERN: LazyLock<Regex> = LazyLock::new(|| { - Regex::new("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,4}$").unwrap() - }); - static PHONE_PATTERN: LazyLock<Regex> = - LazyLock::new(|| Regex::new("^\\+?[0-9]+$").unwrap()); - - if let Maybe::Some(email) = &self.email - && !EMAIL_PATTERN.is_match(email) - { - return Err(bad_request(format_args!( - "email contact data '{email}' is malformed" - ))); - } - if let Maybe::Some(phone) = &self.phone - && !PHONE_PATTERN.is_match(phone) - { - return Err(bad_request(format_args!( - "phone contact data '{phone}' is malformed" - ))); - } - Ok(()) - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct RegisterAccountRequest { - pub username: CompactString, - pub password: CompactString, - pub name: CompactString, - #[serde(default)] - pub is_public: bool, - #[serde(default)] - pub is_taler_exchange: bool, - #[serde(default)] - pub contact_data: ChallengeContactData, - pub cashout_payto_uri: Option<IbanPayto>, - pub payto_uri: Option<Payto<LibeufinId>>, - pub debit_threshold: Option<Amount>, - pub tan_channel: Option<TanChannel>, - pub tan_channels: Option<Vec<TanChannel>>, - pub conversion_rate_class_id: Option<u64>, -} - -impl RegisterAccountRequest { - pub fn validate(&self) -> ApiResult<()> { - static USERNAME_REGEX: LazyLock<Regex> = - LazyLock::new(|| Regex::new("^[a-zA-Z0-9-._~]{1,126}$").unwrap()); - if !USERNAME_REGEX.is_match(&self.username) { - return Err(bad_request(format_args!( - "username '{}' is malformed, must match [a-zA-Z0-9-._~]{{1,126}}", - self.username - ))); - } - if self.tan_channel.is_some() && self.tan_channels.is_some() { - return Err(bad_request( - "you must only use either tan_channel or tan_channels", - )); - } - self.contact_data.validate()?; - Ok(()) - } - - pub fn channels(&self) -> &[TanChannel] { - if let Some(many) = &self.tan_channels { - many - } else if let Some(one) = &self.tan_channel { - std::slice::from_ref(one) - } else { - &[] - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct RegisterAccountResponse { - pub internal_payto_uri: FullBankPayto, -} - -#[derive(Debug, Clone, PartialEq, Eq, Default)] -pub enum Maybe<T> { - #[default] - Missing, - Null, - Some(T), -} - -impl<T> Maybe<T> { - pub fn opt(&self) -> Option<&T> { - match self { - Maybe::Missing | Maybe::Null => None, - Maybe::Some(v) => Some(v), - } - } - - pub fn is_some(&self) -> bool { - matches!(self, Maybe::Some(_)) - } - - pub fn inner(&self) -> Option<Option<&T>> { - match self { - Maybe::Missing => None, - Maybe::Null => Some(None), - Maybe::Some(v) => Some(Some(v)), - } - } -} - -impl<T: FromStr> FromStr for Maybe<T> { - type Err = T::Err; - - fn from_str(s: &str) -> Result<Self, Self::Err> { - if s.is_empty() { - Ok(Self::Null) - } else { - Ok(Self::Some(s.parse()?)) - } - } -} - -impl<'de, T: Deserialize<'de>> Deserialize<'de> for Maybe<T> { - fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> - where - D: serde::Deserializer<'de>, - { - Ok(match Option::<T>::deserialize(deserializer)? { - None => Maybe::Null, - Some(v) => Maybe::Some(v), - }) - } -} - -impl<DB: Database, T: sqlx::Type<DB>> sqlx::Type<DB> for Maybe<T> { - fn type_info() -> DB::TypeInfo { - Option::<T>::type_info() - } -} - -impl<'r, DB: Database, T: sqlx::Decode<'r, DB>> sqlx::Decode<'r, DB> for Maybe<T> { - fn decode(value: <DB as Database>::ValueRef<'r>) -> Result<Self, sqlx::error::BoxDynError> { - Ok(match Option::<T>::decode(value)? { - None => Maybe::Null, - Some(v) => Maybe::Some(v), - }) - } -} - -impl<T: Serialize> Serialize for Maybe<T> { - fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> { - self.opt().serialize(serializer) - } -} - -impl<T> From<Option<T>> for Maybe<T> { - fn from(value: Option<T>) -> Self { - match value { - None => Maybe::Null, - Some(v) => Maybe::Some(v), - } - } -} - -impl<T> From<Option<Maybe<T>>> for Maybe<T> { - fn from(value: Option<Maybe<T>>) -> Self { - match value { - None => Maybe::Missing, - Some(v) => v, - } - } -} - -pub trait TanInfo: Debug { - fn phone(&self) -> Option<&str>; - fn email(&self) -> Option<&str>; - fn channels(&self) -> &[TanChannel]; - fn mfa(&self) -> Tans { - self.channels() - .iter() - .filter_map(|it| { - match it { - TanChannel::email => self.email(), - TanChannel::sms => self.phone(), - } - .map(|info| (*it, info.into())) - }) - .collect() - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct AccountReconfiguration { - #[serde(default)] - pub contact_data: ChallengeContactData, - #[serde(default)] - pub cashout_payto_uri: Maybe<IbanPayto>, - pub name: Option<CompactString>, - pub is_public: Option<bool>, - pub debit_threshold: Option<Amount>, - #[serde(default)] - pub tan_channel: Maybe<TanChannel>, - #[serde(default)] - pub tan_channels: Option<Vec<TanChannel>>, - pub is_taler_exchange: Option<bool>, - #[serde(default)] - pub conversion_rate_class_id: Maybe<u64>, -} - -impl TanInfo for AccountReconfiguration { - fn phone(&self) -> Option<&str> { - self.contact_data.phone.opt().map(|it| it.as_str()) - } - - fn email(&self) -> Option<&str> { - self.contact_data.email.opt().map(|it| it.as_str()) - } - - fn channels(&self) -> &[TanChannel] { - if let Some(many) = &self.tan_channels { - many - } else if let Some(one) = self.tan_channel.opt() { - std::slice::from_ref(one) - } else { - &[] - } - } -} - -impl AccountReconfiguration { - pub fn check(&self) -> ApiResult<()> { - if self.tan_channel.is_some() && self.tan_channels.is_some() { - return Err(bad_request( - "you must only use either tan_channel or tan_channels", - )); - } - - Ok(()) - } - - pub fn channels(&self) -> Option<&[TanChannel]> { - if let Some(many) = &self.tan_channels { - Some(many) - } else if let Some(one) = self.tan_channel.opt() { - Some(std::slice::from_ref(one)) - } else { - None - } - } - - pub fn required_validation(&self, current: &impl TanInfo) -> ApiResult<Tans> { - // Tan channels are either the new ones or the current one - let channels = self.channels().unwrap_or_else(|| current.channels()); - let validated = current.mfa(); - - channels - .iter() - .filter_map(|channel| { - // Info are either the new one or the current ones - let info = match channel { - TanChannel::sms => self.phone().or(current.phone()), - TanChannel::email => self.email().or(current.email()), - }; - - let Some(info) = info else { - return Some(Err(failure( - ErrorCode::BANK_MISSING_TAN_INFO, - format_args!("missing info for tan channel {channel}"), - ))); - }; - - let tan = (*channel, info.into()); - - // Check if tan is already used and therefore already validated - (!(validated.contains(&tan))).then_some(Ok(tan)) - }) - .collect::<Result<Vec<_>, _>>() - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, EnumMeta)] -#[enum_meta(Str)] -#[allow(non_camel_case_types)] -pub enum CreditDebitInfo { - credit, - debit, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct Balance { - pub amount: Amount, - pub credit_debit_indicator: CreditDebitInfo, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, EnumMeta, sqlx::Type)] -#[enum_meta(Str)] -#[sqlx(type_name = "TEXT")] -#[allow(non_camel_case_types)] -pub enum AccountStatus { - active, - locked, - deleted, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct AccountData { - pub name: CompactString, - pub payto_uri: FullBankPayto, - pub balance: Balance, - pub debit_threshold: Amount, - pub contact_data: ChallengeContactData, - pub cashout_payto_uri: Option<PaytoURI>, - pub tan_channel: Option<TanChannel>, - pub tan_channels: Vec<TanChannel>, - pub is_public: bool, - pub is_taler_exchange: bool, - pub is_locked: bool, - pub status: AccountStatus, - pub conversion_rate_class_id: Option<u64>, - pub conversion_rate: Option<ConversionRate>, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct AccountMinimalData { - pub username: CompactString, - pub name: CompactString, - pub payto_uri: FullBankPayto, - pub balance: Balance, - pub debit_threshold: Amount, - pub is_public: bool, - pub is_taler_exchange: bool, - pub is_locked: bool, - pub row_id: u64, - pub status: AccountStatus, - pub conversion_rate_class_id: Option<u64>, - pub conversion_rate: Option<ConversionRate>, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct ListBankAccountsResponse { - pub accounts: Vec<AccountMinimalData>, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct PublicAccount { - pub username: CompactString, - pub payto_uri: FullBankPayto, - pub balance: Balance, - pub is_taler_exchange: bool, - pub row_id: u64, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct PublicAccountsResponse { - pub public_accounts: Vec<PublicAccount>, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct AccountPasswordChange { - pub new_password: CompactString, - pub old_password: Option<CompactString>, -} - -#[derive(Debug, Clone, Deserialize)] -pub struct AccountParams { - #[serde(flatten)] - pub page: PageParams, - pub filter_name: Option<CompactString>, - pub conversion_rate_class_id: Option<u64>, -} - -impl AccountParams { - pub fn check(self) -> Result<Account, ParamsErr> { - Ok(Account { - page: self.page.check()?, - filter_name: self.filter_name.map(|it| format_compact!("%{it}%")), - conversion_rate_class_id: self.conversion_rate_class_id, - }) - } -} - -#[derive(Debug)] -pub struct Account { - pub page: Page, - pub filter_name: Option<CompactString>, - pub conversion_rate_class_id: Option<u64>, -} - -pub fn account_api() -> Router<Arc<BankState>> { - Router::new() - .route( - "/accounts", - post( - async |RegistrationAuth { is_admin }: RegistrationAuth, - State(state): State<Arc<BankState>>, - Req(req): Req<RegisterAccountRequest>| - -> ApiResult<Json<RegisterAccountResponse>> { - match create_account(&state.db, &state.cfg, &req, is_admin).await? { - CreationResult::BonusBalanceInsufficient => { - Err(failure_code(ErrorCode::BANK_UNALLOWED_DEBIT)) - } - CreationResult::UsernameReuse => { - Err(failure_code(ErrorCode::BANK_REGISTER_USERNAME_REUSE)) - } - CreationResult::PayToReuse => { - Err(failure_code(ErrorCode::BANK_REGISTER_PAYTO_URI_REUSE)) - } - CreationResult::UnknownConversionClass => { - Err(failure_code(ErrorCode::BANK_CONVERSION_RATE_CLASS_UNKNOWN)) - } - CreationResult::Success(payto) => Ok(Json(RegisterAccountResponse { - internal_payto_uri: payto, - })), - } - }, - ) - .get( - async |State(state): State<Arc<BankState>>, - _: AdminRAuth, - Query(params): Query<AccountParams>| { - let params = params.check()?; - let accounts = page_admin( - &state.db, - &state.cfg.ctx, - &state.cfg.regional_currency, - state.cfg.fiat_currency(), - &params, - ) - .await?; - if accounts.is_empty() { - ApiResult::Ok(NoContent.into_response()) - } else { - Ok(Json(ListBankAccountsResponse { accounts }).into_response()) - } - }, - ), - ) - .route( - "/public-accounts", - get( - async |State(state): State<Arc<BankState>>, Query(params): Query<AccountParams>| { - let params = params.check()?; - let accounts = page_public( - &state.db, - &state.cfg.ctx, - &state.cfg.regional_currency, - &params, - ) - .await?; - if accounts.is_empty() { - ApiResult::Ok(NoContent.into_response()) - } else { - Ok(Json(PublicAccountsResponse { - public_accounts: accounts, - }) - .into_response()) - } - }, - ), - ) - .route( - "/accounts/{username}", - patch( - async |State(state): State<Arc<BankState>>, - MfaReq { mut auth, req, mfa }: MfaReq<AccountReconfigOp>| { - if let Some(tans) = mfa.pending_mfa() { - return mfa.response_validation(&auth, &state.db, tans).await; - } - match patch_account( - &state.db, - &state.cfg, - &req, - &auth.username, - auth.is_admin(), - mfa.is_2fa(), - ) - .await? - { - PatchResult::Success => Ok(NoContent.into_response()), - PatchResult::MissingTanInfo(e) => Err(e), - PatchResult::Challenges(tans) => { - if tans.is_empty() { - mfa.response_mfa(&mut auth, &state.db, &state.cfg.ctx).await - } else { - mfa.response_validation(&auth, &state.db, &tans).await - } - } - PatchResult::UnknownAccount => todo!(), - PatchResult::NonAdminName => { - Err(failure_code(ErrorCode::BANK_NON_ADMIN_PATCH_LEGAL_NAME)) - } - PatchResult::NonAdminCashout => { - Err(failure_code(ErrorCode::BANK_NON_ADMIN_PATCH_CASHOUT)) - } - PatchResult::NonAdminDebtLimit => { - Err(failure_code(ErrorCode::BANK_NON_ADMIN_PATCH_DEBT_LIMIT)) - } - PatchResult::NonAdminConversionRateClass => Err(failure_code( - ErrorCode::BANK_NON_ADMIN_SET_CONVERSION_RATE_CLASS, - )), - PatchResult::UnknownConversionClass => { - Err(failure_code(ErrorCode::BANK_CONVERSION_RATE_CLASS_UNKNOWN)) - } - } - }, - ) - .delete( - async |State(state): State<Arc<BankState>>, - MfaReq { mut auth, mfa, .. }: MfaReq<AccountDeletionOp>| { - if !state.cfg.allow_account_deletion && !auth.is_admin() { - return Err(require_admin()); - } - // Not deleting reserved names - if matches!(auth.username.as_str(), "admin" | "bank") { - return Err(failure( - ErrorCode::BANK_RESERVED_USERNAME_CONFLICT, - "Cannot delete reserved account", - )); - } else if auth.username == "exchange" && state.cfg.fiat.is_some() { - return Err(failure( - ErrorCode::BANK_RESERVED_USERNAME_CONFLICT, - "Cannot delete 'exchange' accounts when conversion is enabled", - )); - } - match db::account::delete(&state.db, &auth.username, mfa.is_2fa()).await? { - DeletionResult::Success => Ok(NoContent.into_response()), - DeletionResult::UnknownAccount => { - Err(failure_code(ErrorCode::BANK_UNKNOWN_ACCOUNT)) - } - DeletionResult::BalanceNotZero => { - Err(failure_code(ErrorCode::BANK_ACCOUNT_BALANCE_NOT_ZERO)) - } - DeletionResult::TanRequired => Ok(mfa - .response_mfa(&mut auth, &state.db, &state.cfg.ctx) - .await - .into_response()), - } - }, - ) - .get( - async |State(state): State<Arc<BankState>>, - UserAuth { username, .. }: UserRAuth| { - match by_username( - &state.db, - &state.cfg.ctx, - &state.cfg.regional_currency, - state.cfg.fiat_currency(), - &username, - ) - .await? - { - Some(acc) => Ok(Json(acc)), - None => Err(failure_code(ErrorCode::BANK_UNKNOWN_ACCOUNT)), - } - }, - ), - ) - .route( - "/accounts/{username}/auth", - patch( - async |State(state): State<Arc<BankState>>, - MfaReq { mut auth, req, mfa }: MfaReq<AccountPasswordOp>| { - if !auth.is_admin() && req.old_password.is_none() { - return Err(failure_code( - ErrorCode::BANK_NON_ADMIN_PATCH_MISSING_OLD_PASSWORD, - )); - } - checkpw(&req.new_password, state.cfg.pwd_check_quality)?; - - match reconfig_password( - &state.db, - &state.cfg.pw_crypto, - &auth.username, - &req.new_password, - req.old_password.as_deref(), - auth.is_admin() || mfa.is_2fa(), - ) - .await? - { - PatchAuthResult::UnknownAccount => { - Err(failure_code(ErrorCode::BANK_UNKNOWN_ACCOUNT)) - } - PatchAuthResult::OldPasswordMismatch => { - Err(failure_code(ErrorCode::BANK_PATCH_BAD_OLD_PASSWORD)) - } - PatchAuthResult::TanRequired => Ok(mfa - .response_mfa(&mut auth, &state.db, &state.cfg.ctx) - .await? - .into_response()), - PatchAuthResult::Success => Ok(NoContent.into_response()), - } - }, - ), - ) -} - -pub fn rand_iban_payto() -> IbanPayto { - let iban = IBAN::random(Country::DE); - IbanPayto::new(BankID { iban, bic: None }) -} - -pub async fn create_account( - db: &PgPool, - cfg: &BankCfg, - req: &RegisterAccountRequest, - is_admin: bool, -) -> ApiResult<CreationResult> { - req.validate()?; - - if matches!(req.username.as_str(), "admin" | "bank") { - return Err(failure_code(ErrorCode::BANK_RESERVED_USERNAME_CONFLICT)); - } - - let channels = req.channels(); - - if !is_admin { - if req.debit_threshold.is_some() { - return Err(failure_code(ErrorCode::BANK_NON_ADMIN_PATCH_DEBT_LIMIT)); - } else if req.conversion_rate_class_id.is_some() { - return Err(failure_code( - ErrorCode::BANK_NON_ADMIN_SET_CONVERSION_RATE_CLASS, - )); - } else if !channels.is_empty() { - return Err(failure_code(ErrorCode::BANK_NON_ADMIN_SET_TAN_CHANNEL)); - } - } else { - if let Some(amount) = req.debit_threshold { - cfg.check_regio(&amount)?; - } - } - - for channel in channels { - if !cfg.tan_channels.contains_key(channel) { - return Err(failure( - ErrorCode::BANK_TAN_CHANNEL_NOT_SUPPORTED, - format_args!("unsupported tan channel {channel}"), - )); - } - - let info = match channel { - TanChannel::sms => req.contact_data.phone.opt(), - TanChannel::email => req.contact_data.email.opt(), - }; - - if info.is_none() { - return Err(failure( - ErrorCode::BANK_MISSING_TAN_INFO, - format_args!("missing info for tan channel {channel}"), - )); - } - } - - if req.username == "exchange" && !req.is_taler_exchange { - return Err(failure_status( - ErrorCode::END, - "'exchange' account must be a taler exchange accont", - StatusCode::CONFLICT, - )); - } - - checkpw(&req.password, cfg.pwd_check_quality)?; - - let create = async |payto: LibeufinId| { - crate::db::account::create( - db, - &cfg.ctx, - &cfg.pw_crypto, - &req.username, - &req.password, - &req.name, - req.contact_data.email.opt().map(|it| it.as_str()), - req.contact_data.phone.opt().map(|it| it.as_str()), - req.cashout_payto_uri.as_ref(), - &payto, - req.is_public, - req.is_taler_exchange, - req.debit_threshold.unwrap_or(cfg.default_debt_limit), - if req.is_taler_exchange { - Amount::zero(&cfg.regional_currency) - } else { - cfg.registration_bonus - }, - channels, - req.payto_uri.is_some(), - req.conversion_rate_class_id, - ) - .await - }; - - match cfg.wire_method { - WireMethod::iban => { - if let Some(payto) = &req.payto_uri { - let bank_id = payto.expect_iban()?; - Ok(create(LibeufinId::IBAN(*bank_id)).await?) - } else { - let mut retry = 5; - loop { - let payto = rand_iban_payto(); - let res = create(LibeufinId::IBAN(payto.into_inner())).await?; - if res == CreationResult::PayToReuse && retry > 0 { - retry -= 1; - continue; - } - return Ok(res); - } - } - } - WireMethod::x_taler_bank => { - if let Some(payto) = &req.payto_uri { - let libeufin_id = payto.expect_xtaler_bank()?; - if libeufin_id.username != req.username { - return Err(bad_request(format_args!( - "Expected a payto uri for '{}' got one for '{}'", - req.username, libeufin_id.username - ))); - } - } - Ok(create(LibeufinId::XTalerBank(XTalerBank { - hostname: cfg.ctx.hostname.clone(), - username: req.username.clone(), - })) - .await?) - } - } -} - -/** - * This function creates the admin account ONLY IF it was - * NOT found in the database. It sets it to a random password that - * is only meant to be overridden by a dedicated CLI tool - */ -pub async fn create_admin_account( - db: &PgPool, - cfg: &BankCfg, - pw: Option<&str>, -) -> anyhow::Result<CreationResult> { - let pw = pw - .map(|it| it.to_owned()) - .unwrap_or_else(|| Base32::<32>::secure_rand().to_string()); - - let payto = match cfg.wire_method { - WireMethod::iban => LibeufinId::IBAN(rand_iban_payto().into_inner()), - WireMethod::x_taler_bank => LibeufinId::XTalerBank(XTalerBank { - hostname: cfg.ctx.hostname.clone(), - username: CompactString::const_new("admin"), - }), - }; - - Ok(crate::db::account::create( - db, - &cfg.ctx, - &cfg.pw_crypto, - "admin", - &pw, - "Bank administrator", - None, - None, - None, - &payto, - false, - false, - cfg.default_debt_limit, - Amount::zero(&cfg.regional_currency), - &[], - false, - None, - ) - .await?) -} - -pub async fn patch_account( - db: &PgPool, - cfg: &BankCfg, - req: &AccountReconfiguration, - username: &str, - is_admin: bool, - is2fa: bool, -) -> ApiResult<PatchResult> { - if let Some(amount) = req.debit_threshold { - cfg.check_regio(&amount)?; - } - - if username == "admin" && req.is_public == Some(true) { - return Err(failure_status( - ErrorCode::END, - "'admin' account cannot be public", - StatusCode::CONFLICT, - )); - } - - if username == "exchange" && req.is_taler_exchange == Some(false) { - return Err(failure_status( - ErrorCode::END, - "'exchange' account must be a taler exchange account", - StatusCode::CONFLICT, - )); - } - - if let Some(channels) = req.channels() { - for channel in channels { - if !cfg.tan_channels.contains_key(channel) { - return Err(failure( - ErrorCode::BANK_TAN_CHANNEL_NOT_SUPPORTED, - format_args!("unsupported tan channel {channel}"), - )); - } - } - } - - Ok(reconfig( - db, - &cfg.regional_currency, - username, - req, - is_admin, - is2fa, - cfg.allow_edit_name, - cfg.allow_edit_cashout, - ) - .await?) -} - -#[cfg(test)] -pub mod test { - - use axum::http::{Method, StatusCode}; - use jiff::Timestamp; - use serde::Serialize; - use taler_common::{ - error_code::ErrorCode, - types::{amount::amount, payto::PaytoImpl}, - }; - use taler_test_utils::{json, server::TestServer as _}; - - use crate::{ - TanChannel, - api::{ - account::{ - AccountData, AccountStatus, Balance, ChallengeContactData, CreditDebitInfo, - ListBankAccountsResponse, Maybe, PublicAccountsResponse, RegisterAccountResponse, - rand_iban_payto, - }, - conversion::ConversionRateClass, - test::{Auth, BankTestCtx, MfaRequest, bank_setup, bank_setup_conf}, - }, - db::gc::collect, - payto::FullBankPayto, - }; - - #[tokio::test] - async fn create() { - let mut ctx = bank_setup().await; - - let admin_only = async |body: serde_json::Value, error: ErrorCode| { - // Check restricted - ctx.post("/accounts").json(&body).await.assert_error(error); - ctx.post_admin("/accounts").json(&body).await.assert_ok(); - }; - ctx.auth_routine(Method::GET, "/accounts/merchant", Auth::UserOrAdmin) - .await; - - // Check generated payto - { - let body = json!({ - "username": "john", - "password": "password", - "name": "John" - }); - // Check ok - let payto = ctx - .post("/accounts") - .json(&body) - .await - .assert_ok_json::<RegisterAccountResponse>() - .internal_payto_uri; - // Check idempotency - assert_eq!( - payto, - ctx.post("/accounts") - .json(&body) - .await - .assert_ok_json::<RegisterAccountResponse>() - .internal_payto_uri - ); - // Check idempotency with payto - ctx.post("/accounts") - .json(&json!(body + { - "payto_uri": payto - })) - .await - .assert_ok_json::<RegisterAccountResponse>(); - // Check payto conflict - ctx.post("/accounts") - .json(&json!(body + { - "payto_uri": rand_iban_payto() - })) - .await - .assert_error(ErrorCode::BANK_REGISTER_USERNAME_REUSE); - } - - let name = "Jane"; - let payto = &rand_iban_payto(); - let req = json!({ - "username": "foo", - "password": "password", - "name": name, - "is_public": true, - "payto_uri": &payto, - "is_taler_exchange": true - }); - // Check given payto - { - let full: FullBankPayto = payto.full(name).convert(); - // Check ok - assert_eq!( - full, - ctx.post("/accounts") - .json(&req) - .await - .assert_ok_json::<RegisterAccountResponse>() - .internal_payto_uri - ); - // Check idempotency - assert_eq!( - full, - ctx.post("/accounts") - .json(&req) - .await - .assert_ok_json::<RegisterAccountResponse>() - .internal_payto_uri - ); - } - - // Check admin only debit_threshold - admin_only( - json!({ - "username": "bat", - "password": "password", - "name": "Bat", - "debit_threshold": "KUDOS:42" - }), - ErrorCode::BANK_NON_ADMIN_PATCH_DEBT_LIMIT, - ) - .await; - - // Check admin only conversion_rate_class_id - let conv_class_id = ctx.create_conversion_rate_class().await; - admin_only( - json!({ - "username": "bat2", - "password": "password", - "name": "Bat", - "conversion_rate_class_id": conv_class_id - }), - ErrorCode::BANK_NON_ADMIN_SET_CONVERSION_RATE_CLASS, - ) - .await; - - // Check admin only tan_channel - admin_only( - json!({ - "username": "bat3", - "password": "password", - "name": "Bat", - "contact_data" : { - "phone" : "+456" - }, - "tan_channel": "sms" - }), - ErrorCode::BANK_NON_ADMIN_SET_TAN_CHANNEL, - ) - .await; - - // Check both tan channels - ctx.post("/accounts") - .json(json!({ - "username": "bat3", - "password": "password", - "name": "Bat", - "tan_channel": "sms", - "tan_channels": [] - })) - .await - .assert_bad_request(); - - // Check tan info - for channel in TanChannel::entries { - ctx.post_admin("/accounts") - .json(json!({ - "username": "bat", - "password": "password", - "name": "Bat", - "tan_channel": channel - })) - .await - .assert_error(ErrorCode::BANK_MISSING_TAN_INFO); - ctx.post_admin("/accounts") - .json(json!({ - "username": "bat", - "password": "password", - "name": "Bat", - "tan_channels": [channel] - })) - .await - .assert_error(ErrorCode::BANK_MISSING_TAN_INFO); - } - ctx.post_admin("/accounts") - .json(json!({ - "username": "bat", - "password": "password", - "name": "Bat", - "tan_channels": TanChannel::entries - })) - .await - .assert_error(ErrorCode::BANK_MISSING_TAN_INFO); - - // Check unknown conversion rate class - ctx.post_admin("/accounts") - .json(json!({ - "username": "bat4", - "password": "password", - "name": "Bat", - "conversion_rate_class_id": 42 - })) - .await - .assert_error(ErrorCode::BANK_CONVERSION_RATE_CLASS_UNKNOWN); - - for username in ["admin", "bank"] { - ctx.post("/accounts") - .json(json!({ - "username": username, - "password": "password", - "name": "John Smith" - })) - .await - .assert_error(ErrorCode::BANK_RESERVED_USERNAME_CONFLICT); - } - - for username in [ - "bad@username", - "bad/username", - " spaces ", - &"long".repeat(40), - ] { - ctx.post("/accounts") - .json(json!({ - "username": username, - "password": "password", - "name": "John Smith" - })) - .await - .assert_bad_request(); - } - - // Non exchange account - ctx.post("/accounts") - .json(json!({ - "username": "exchange", - "password": "password", - "name": "Exchange" - })) - .await - .assert_error_status(ErrorCode::END, StatusCode::CONFLICT); - - // Username conflict - ctx.post("/accounts") - .json(json!(req + { "name" : "Foo" })) - .await - .assert_error(ErrorCode::BANK_REGISTER_USERNAME_REUSE); - // Payto conflict - ctx.post("/accounts") - .json(json!(req + { "username" : "bar" })) - .await - .assert_error(ErrorCode::BANK_REGISTER_PAYTO_URI_REUSE); - ctx.get_admin("/accounts/bar") - .await - .assert_error(ErrorCode::BANK_UNKNOWN_ACCOUNT); - // Bad payto kind - ctx.post("/accounts") - .json(json!(req +{ - "payto_uri": "payto://x-taler-bank/bank.hostname.test/bar" - })) - .await - .assert_bad_request(); - // Short password - ctx.post("/accounts") - .json(json!(req + { "password" : "short" })) - .await - .assert_error(ErrorCode::BANK_PASSWORD_TOO_SHORT); - // Long password - ctx.post("/accounts") - .json(json!(req +{ - "password": "loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong-password" - })) - .await - .assert_error(ErrorCode::BANK_PASSWORD_TOO_LONG); - // Check currency - ctx.post_admin("/accounts") - .json(json!(req + { "debit_threshold": "EUR:100" })) - .await - .assert_error(ErrorCode::GENERIC_CURRENCY_MISMATCH); - - // Check cashout payto receiver name logic - ctx.post("/accounts") - .json(json!({ - "username": "cashout_guess", - "password": "cashout_guess-password", - "name": "Mr Guess My Name", - "cashout_payto_uri": &payto - })) - .await - .assert_ok(); - ctx.post("/accounts") - .json(json!({ - "username": "cashout_keep", - "password": "cashout_keep-password", - "name": "Mr Keep My Name", - "cashout_payto_uri": payto.as_full_uri("Santa Claus") - })) - .await - .assert_ok(); - ctx.cache_tokens(&["cashout_guess", "cashout_keep"]).await; - let acc: AccountData = ctx.geta("/accounts/cashout_guess").await.assert_ok_json(); - assert_eq!( - acc.cashout_payto_uri.unwrap(), - payto.as_full_uri("Mr Guess My Name") - ); - let acc: AccountData = ctx.geta("/accounts/cashout_keep").await.assert_ok_json(); - assert_eq!( - acc.cashout_payto_uri.unwrap(), - payto.as_full_uri("Mr Keep My Name") - ); - - // Check input restriction - { - let req = json!({ - "username": "username", - "password": "password", - "name": "Name" - }); - ctx.post("/accounts") - .json(json!(req + { - "contact_data": { "phone" : " +456" } - })) - .await - .assert_bad_request(); - ctx.post("/accounts") - .json(json!(req + { - "contact_data": { "phone" : "test@gmail.com" } - })) - .await - .assert_bad_request(); - } - - // Create -> get - ctx.post("/accounts") - .json(json!({ - "username": "minimal", - "password": "minimal-password", - "name": "John Smith", - })) - .await - .assert_ok(); - let new = rand_iban_payto(); - ctx.post_admin("/accounts") - .json(json!({ - "username": "maximal", - "password": "maximal-password", - "name": "John Smith", - "is_public": true, - "is_taler_exchange": true, - "contact_data": { - "phone": "+456", - "email": "test@gmail.com" - }, - "cashout_payto_uri": payto, - "payto_uri": new, - "debit_threshold": "KUDOS:12", - "tan_channels": ["sms", "email"], - "conversion_rate_class_id": conv_class_id - })) - .await - .assert_ok(); - ctx.cache_tokens(&["minimal", "maximal"]).await; - let acc: AccountData = ctx.geta("/accounts/minimal").await.assert_ok_json(); - pretty_assertions::assert_eq!( - AccountData { - name: "John Smith".into(), - balance: Balance { - amount: amount("KUDOS:0"), - credit_debit_indicator: CreditDebitInfo::credit, - }, - cashout_payto_uri: None, - contact_data: ChallengeContactData { - phone: Maybe::Null, - email: Maybe::Null, - }, - conversion_rate: Some(acc.conversion_rate.clone().unwrap()), - conversion_rate_class_id: None, - debit_threshold: acc.debit_threshold, - is_locked: false, - is_public: false, - is_taler_exchange: false, - payto_uri: acc.payto_uri.clone(), - status: AccountStatus::active, - tan_channel: None, - tan_channels: Vec::new(), - }, - acc - ); - let acc: AccountData = ctx.geta("/accounts/maximal").await.assert_ok_json(); - pretty_assertions::assert_eq!( - AccountData { - name: "John Smith".into(), - balance: Balance { - amount: amount("KUDOS:0"), - credit_debit_indicator: CreditDebitInfo::credit, - }, - cashout_payto_uri: Some(payto.as_full_uri("John Smith")), - contact_data: ChallengeContactData { - phone: Maybe::Some("+456".into()), - email: Maybe::Some("test@gmail.com".into()) - }, - conversion_rate: Some(acc.conversion_rate.clone().unwrap()), - conversion_rate_class_id: Some(conv_class_id), - debit_threshold: acc.debit_threshold, - is_locked: false, - is_public: true, - is_taler_exchange: true, - payto_uri: new.full("John Smith").convert(), - status: AccountStatus::active, - tan_channel: Some(TanChannel::sms), - tan_channels: vec![TanChannel::sms, TanChannel::email] - }, - acc - ); - - let ctx = ctx.swap_cfg("test_bonus.conf").await; - // Create bonus - { - let req = json!({ - "password": "password", - "name": "Mallory" - }); - - ctx.set_max_debt("admin", "1000").await; - - // Check OK - for i in 0..10 { - let username = format!("foo{i}"); - ctx.post_admin("/accounts") - .json(json!(req + { "username": username })) - .await - .assert_ok(); - ctx.assert_balance(&username, "100").await; - } - ctx.assert_balance("admin", "-1000").await; - - // Check insufficient fund - ctx.post_admin("/accounts") - .json(json!(req + { "username": "bar" })) - .await - .assert_error(ErrorCode::BANK_UNALLOWED_DEBIT); - ctx.get_admin("/accounts/bar") - .await - .assert_error(ErrorCode::BANK_UNKNOWN_ACCOUNT); - } - - // Restricted account creation - let ctx = ctx.swap_cfg("test_restrict.conf").await; - ctx.auth_routine(Method::POST, "/accounts", Auth::UserOrAdmin) - .await; - ctx.post_admin("/accounts") - .json(json!({ - "username": "foobar", - "password": "password-xyz", - "name": "Mallory" - })) - .await - .assert_ok(); - - // Unsupported tan - let ctx = ctx.swap_cfg("test_tan_err.conf").await; - ctx.post_admin("/accounts") - .json(json!({ - "username": "foo", - "password": "password-xyz", - "name": "Mallory", - "tan_channel": "email" - })) - .await - .assert_error(ErrorCode::BANK_TAN_CHANNEL_NOT_SUPPORTED); - - // No password check - let ctx = ctx.swap_cfg("test_no_password_check.conf").await; - // Short password - ctx.post("/accounts") - .json(json!({ - "username": "short", - "password": "short", - "name": "John Smith", - })) - .await - .assert_ok(); - // Long password - ctx.post("/accounts") - .json(json!({ - "username": "long", - "password": "loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong-password", - "name": "John Smith" - })) - .await - .assert_ok(); - } - - #[tokio::test] - async fn delete() { - let mut ctx = bank_setup().await; - ctx.auth_routine(Method::DELETE, "/accounts/merchant", Auth::Token) - .await; - - // Reserved accounts - for username in ["admin", "bank"] { - ctx.delete_admin(format!("/accounts/{username}")) - .await - .assert_error(ErrorCode::BANK_RESERVED_USERNAME_CONFLICT); - } - ctx.deletea("/accounts/exchange") - .await - .assert_error(ErrorCode::BANK_RESERVED_USERNAME_CONFLICT); - - let payto = ctx.tmp_payto().await; - ctx.post("/accounts") - .json(json!({ - "username": "john", - "password": "john-password", - "name": "John", - "payto_uri": payto - })) - .await - .assert_ok(); - ctx.cache_tokens(&["john"]).await; - ctx.fill_tan_info("john").await; - // Fail to delete, due to a non-zero balance. - ctx.tx("customer", "1", "john").await; - ctx.deletea("/accounts/john") - .await - .assert_error(ErrorCode::BANK_ACCOUNT_BALANCE_NOT_ZERO); - // Successful deletion - ctx.tx("john", "1", "customer").await; - ctx.deletea("/accounts/john") - .await - .assert_challenge(&ctx) - .await - .assert_no_content(); - // Account no longer exists - ctx.deletea("/accounts/john") - .await - .assert_error(ErrorCode::GENERIC_TOKEN_UNKNOWN); - ctx.delete_admin("/accounts/john") - .await - .assert_error(ErrorCode::BANK_UNKNOWN_ACCOUNT); - - // GC - { - let now = Timestamp::now(); - collect(&ctx.state.db, &now, &now, &now).await.unwrap(); - // TODO need more operations - } - - // Test admin-only account deletion - let ctx = ctx.swap_cfg("test_restrict.conf").await; - ctx.auth_routine(Method::DELETE, "/accounts/merchant", Auth::Admin) - .await; - // Exchange is still restricted - ctx.delete_admin("/accounts/exchange") - .await - .assert_error(ErrorCode::BANK_RESERVED_USERNAME_CONFLICT); - - // Test delete exchange account - let ctx = ctx.swap_cfg("test_no_conversion.conf").await; - // Exchange is no longer restricted - ctx.deletea("/accounts/exchange").await.assert_no_content(); - } - - async fn check_admin_only(ctx: &BankTestCtx, req: impl Serialize, error: ErrorCode) { - // Check restricted - ctx.patcha("/accounts/merchant") - .json(&req) - .await - .assert_error(error); - // Check admin always can - ctx.patch_admin("/accounts/merchant") - .json(&req) - .await - .assert_no_content(); - // Check idempotent - ctx.patch_admin("/accounts/merchant") - .json(&req) - .await - .assert_no_content(); - } - - #[tokio::test] - async fn reconfig() { - let mut ctx = bank_setup().await; - - ctx.auth_routine(Method::PATCH, "/accounts/merchant", Auth::UserOrAdmin) - .await; - - for channel in TanChannel::entries { - ctx.patcha("/accounts/merchant") - .json(json!({ "tan_channel": channel })) - .await - .assert_error(ErrorCode::BANK_MISSING_TAN_INFO); - ctx.patcha("/accounts/merchant") - .json(json!({ "tan_channels": [channel] })) - .await - .assert_error(ErrorCode::BANK_MISSING_TAN_INFO); - } - ctx.patcha("/accounts/merchant") - .json(json!({ "tan_channels": TanChannel::entries })) - .await - .assert_error(ErrorCode::BANK_MISSING_TAN_INFO); - - // Successful attempt now - let cashout = rand_iban_payto(); - let req = json!({ - "cashout_payto_uri": cashout, - "name": "Roger", - "is_public": true, - "is_taler_exchange": true, - "contact_data": { - "phone": "+99", - "email": "foo@example.com" - } - }); - ctx.patcha("/accounts/merchant") - .json(&req) - .await - .assert_no_content(); - // Checking idempotent - ctx.patcha("/accounts/merchant") - .json(&req) - .await - .assert_no_content(); - - // Check admin only - check_admin_only( - &ctx, - json!(req + { "debit_threshold": "KUDOS:100" }), - ErrorCode::BANK_NON_ADMIN_PATCH_DEBT_LIMIT, - ) - .await; - check_admin_only( - &ctx, - json!(req + { "conversion_rate_class_id": 1 }), - ErrorCode::BANK_NON_ADMIN_SET_CONVERSION_RATE_CLASS, - ) - .await; - - // Check unknown conversion rate class - ctx.patch_admin("/accounts/merchant") - .json(json!(req + { "conversion_rate_class_id": 42 })) - .await - .assert_error(ErrorCode::BANK_CONVERSION_RATE_CLASS_UNKNOWN); - - // Check currency - ctx.patch_admin("/accounts/merchant") - .json(json!(req + { "debit_threshold": "EUR:100" })) - .await - .assert_error(ErrorCode::GENERIC_CURRENCY_MISMATCH); - - // Check patch - let acc: AccountData = ctx.geta("/accounts/merchant").await.assert_ok_json(); - let expected = AccountData { - name: "Roger".into(), - balance: Balance { - amount: amount("KUDOS:0"), - credit_debit_indicator: CreditDebitInfo::credit, - }, - cashout_payto_uri: Some(acc.cashout_payto_uri.clone().unwrap()), - contact_data: ChallengeContactData { - phone: Maybe::Some("+99".into()), - email: Maybe::Some("foo@example.com".into()), - }, - conversion_rate: Some(acc.conversion_rate.clone().unwrap()), - conversion_rate_class_id: Some(1), - debit_threshold: acc.debit_threshold, - is_locked: false, - is_public: true, - is_taler_exchange: true, - payto_uri: ctx.merchant_payto.clone().into_inner().full("Roger"), - status: AccountStatus::active, - tan_channel: None, - tan_channels: Vec::new(), - }; - pretty_assertions::assert_eq!(acc, expected); - - // Check keep values when there is no changes - ctx.patcha("/accounts/merchant") - .json(json!({})) - .await - .assert_no_content(); - pretty_assertions::assert_eq!( - ctx.geta("/accounts/merchant") - .await - .assert_ok_json::<AccountData>(), - expected - ); - - // Admin cannot be public - ctx.patcha("/accounts/admin") - .json(json!({ - "is_public": true - })) - .await - .assert_error_status(ErrorCode::END, StatusCode::CONFLICT); - - // Exchange must be exchange - ctx.patcha("/accounts/exchange") - .json(json!({ - "is_taler_exchange": false - })) - .await - .assert_error_status(ErrorCode::END, StatusCode::CONFLICT); - - // Check cashout payto receiver name logic - ctx.post("/accounts") - .json(json!({ - "username": "cashout", - "password": "cashout-password", - "name": "Mr Cashout Cashout" - })) - .await - .assert_ok(); - ctx.cache_tokens(&["cashout"]).await; - - for (cashout, name, expect) in [ - ( - cashout.as_uri(), - None, - cashout.as_full_uri("Mr Cashout Cashout"), - ), - ( - cashout.as_uri(), - Some("New name"), - cashout.as_full_uri("New name"), - ), - ( - cashout.as_full_uri("Full name"), - None, - cashout.as_full_uri("New name"), - ), - ( - cashout.as_full_uri("Full second name"), - Some("Another name"), - cashout.as_full_uri("Another name"), - ), - ] { - let mut v = json!({ - "cashout_payto_uri": cashout, - }); - if let Some(name) = name { - v["name"] = name.into(); - } - ctx.patch_admin("/accounts/cashout") - .json(v) - .await - .assert_no_content(); - let acc: AccountData = ctx.geta("/accounts/cashout").await.assert_ok_json(); - assert_eq!(expect, acc.cashout_payto_uri.unwrap()); - } - - // Check 2FA - ctx.fill_tan_info("customer").await; - ctx.patcha("/accounts/customer") - .json(json!({ "is_public": true })) - .await - .assert_challenge_check(&ctx, async |_| { - let acc: AccountData = ctx.geta("/accounts/customer").await.assert_ok_json(); - assert!(!acc.is_public); - }) - .await - .assert_no_content(); - - let acc: AccountData = ctx.geta("/accounts/customer").await.assert_ok_json(); - assert!(acc.is_public); - - // Restriction - let ctx = ctx.swap_cfg("test_restrict.conf").await; - { - check_admin_only( - &ctx, - json!({ "name": "Another Foo" }), - ErrorCode::BANK_NON_ADMIN_PATCH_LEGAL_NAME, - ) - .await; - check_admin_only( - &ctx, - json!({ "cashout_payto_uri": rand_iban_payto() }), - ErrorCode::BANK_NON_ADMIN_PATCH_CASHOUT, - ) - .await; - - // Check idempotent - let acc: AccountData = ctx.geta("/accounts/merchant").await.assert_ok_json(); - ctx.patcha("/accounts/merchant") - .json(json!({ - "name": acc.name, - "cashout_payto_uri": acc.cashout_payto_uri, - "debit_threshold": acc.debit_threshold - })) - .await - .assert_no_content(); - } - - // Unsupported tan - let ctx = ctx.swap_cfg("test_tan_err.conf").await; - ctx.patcha("/accounts/customer") - .json(json!({ - "tan_channel": "email" - })) - .await - .assert_error(ErrorCode::BANK_TAN_CHANNEL_NOT_SUPPORTED); - } - - #[tokio::test] - async fn password() { - let ctx = bank_setup().await; - ctx.auth_routine(Method::PATCH, "/accounts/merchant/auth", Auth::UserOrAdmin) - .await; - - // Changing the password - ctx.patcha("/accounts/customer/auth") - .json(json!({ - "old_password": "customer-password", - "new_password": "new-password" - })) - .await - .assert_no_content(); - // Previous password should fail. - ctx.post("/accounts/customer/token") - .basic_auth("customer", "customer-password") - .await - .assert_status(StatusCode::UNAUTHORIZED); - // New password should succeed - ctx.post("/accounts/customer/token") - .basic_auth("customer", "new-password") - .json(json!({ "scope": "readonly" })) - .await - .assert_ok(); - ctx.patcha("/accounts/customer/auth") - .json(json!({ - "old_password": "new-password", - "new_password": "customer-password" - })) - .await - .assert_no_content(); - - // Check require test old password - ctx.patcha("/accounts/customer/auth") - .json(json!({ - "old_password": "bad-password", - "new_password": "new-password" - })) - .await - .assert_error(ErrorCode::BANK_PATCH_BAD_OLD_PASSWORD); - // Check require old password for user - ctx.patcha("/accounts/customer/auth") - .json(json!({ - "new_password": "new-password" - })) - .await - .assert_error(ErrorCode::BANK_NON_ADMIN_PATCH_MISSING_OLD_PASSWORD); - // Testing short password - ctx.patcha("/accounts/customer/auth") - .json(json!({ - "old_password": "ignored", - "new_password": "short" - })) - .await - .assert_error(ErrorCode::BANK_PASSWORD_TOO_SHORT); - // Testing long password - ctx.patcha("/accounts/customer/auth") - .json(json!({ - "old_password": "ignored", - "new_password": "loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong-password" - })) - .await - .assert_error(ErrorCode::BANK_PASSWORD_TOO_LONG); - - // Check admin - ctx.patch_admin("/accounts/customer/auth") - .json(json!({ - "new_password": "customer-password" - })) - .await - .assert_no_content(); - - // Check 2FA - ctx.fill_tan_info("customer").await; - ctx.patcha("/accounts/customer/auth") - .json(json!({ - "old_password": "customer-password", - "new_password": "new-password" - })) - .await - .assert_challenge(&ctx) - .await - .assert_no_content(); - ctx.patch_admin("/accounts/customer/auth") - .json(json!({ - "new_password": "customer-password" - })) - .await - .assert_no_content(); - - // Check 2FA after password check - ctx.patcha("/accounts/customer/auth") - .json(json!({ - "old_password": "bad-password", - "new_password": "new-password" - })) - .await - .assert_error(ErrorCode::BANK_PATCH_BAD_OLD_PASSWORD); - - // No password check - let ctx = ctx.swap_cfg("test_no_password_check.conf").await; - ctx.patcha("/accounts/merchant/auth") - .json(json!({ - "old_password": "merchant-password", - "new_password": "short" - })) - .await - .assert_no_content(); - // Testing long password - ctx.patcha("/accounts/merchant/auth") - .json(json!({ - "old_password": "short", - "new_password": "loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong-password" - })) - .await - .assert_no_content(); - } - - #[tokio::test] - async fn list() { - let ctx = bank_setup_conf("test_no_conversion.conf").await; - ctx.auth_routine(Method::GET, "/accounts", Auth::Admin) - .await; - - // Remove default accounts - let accounts = ["merchant", "exchange", "customer"]; - for username in accounts { - ctx.delete_admin(format!("/accounts/{username}")) - .await - .assert_no_content(); - } - let req: ListBankAccountsResponse = ctx.get_admin("/accounts").await.assert_ok_json(); - for account in req.accounts { - assert_eq!(account.conversion_rate, None); - if accounts.contains(&account.username.as_str()) { - assert_eq!(account.status, AccountStatus::deleted); - } else { - assert_eq!(account.status, AccountStatus::active); - } - } - - // Hard delete accounts - let now = Timestamp::now(); - collect(&ctx.state.db, &now, &now, &now).await.unwrap(); - ctx.get("/public-accounts").await.assert_no_content(); - ctx.get_admin("/accounts").await.assert_ok(); - - let ctx = ctx.swap_cfg("test.conf").await; - - for _ in 0..3 { - ctx.create_conversion_rate_class().await; - } - - // Gen some public and private accounts - for i in 0..5 { - let m = i % 3; - ctx.post_admin("/accounts") - .json(json!({ - "username": format!("{i}"), - "password": "password", - "name": format!("Mr 1{i}"), - "is_public": i % 2 == 0, - "conversion_rate_class_id": if m > 0 && m <= 3 { - Some(m) - } else { - None - } - })) - .await - .assert_ok(); - } - // All public - let res: PublicAccountsResponse = ctx.get("/public-accounts").await.assert_ok_json(); - assert_eq!(res.public_accounts.len(), 3); - for acc in res.public_accounts { - assert_eq!(0, acc.username.parse::<u8>().unwrap() % 2) - } - // Conversion rate - let res: ListBankAccountsResponse = ctx.get_admin("/accounts").await.assert_ok_json(); - for acc in res.accounts { - assert_eq!( - acc.conversion_rate, - Some( - ctx.get_admin(format!("/accounts/{}/conversion-info/rate", acc.username)) - .await - .assert_ok_json() - ) - ); - } - // Filtering - let check_ids = async |query: &str, ids: &[&str]| { - let res = ctx.get_admin(format!("/accounts?{query}")).await; - if ids.is_empty() { - res.assert_no_content(); - } else { - let res: ListBankAccountsResponse = res.assert_ok_json(); - assert_eq!( - ids, - res.accounts - .into_iter() - .map(|it| it.username) - .collect::<Vec<_>>() - ) - } - }; - check_ids("", &["4", "3", "2", "1", "0", "admin"]).await; - check_ids("filter_name=1", &["4", "3", "2", "1", "0"]).await; - check_ids("filter_name=3", &["3"]).await; - check_ids("conversion_rate_class_id=1", &["4", "1"]).await; - check_ids("conversion_rate_class_id=2", &["2"]).await; - check_ids("conversion_rate_class_id=3", &[]).await; - check_ids("conversion_rate_class_id=4", &[]).await; - check_ids("conversion_rate_class_id=0", &["3", "0", "admin"]).await; - check_ids("conversion_rate_class_id=0&filter_name=1", &["3", "0"]).await; - for (id, num) in [(1, 2), (2, 1), (3, 0)] { - assert_eq!( - num, - ctx.get_admin(format!("/conversion-rate-classes/{id}")) - .await - .assert_ok_json::<ConversionRateClass>() - .num_users - ); - } - } -} diff --git a/crates/libeufin-bank/src/api/cashout.rs b/crates/libeufin-bank/src/api/cashout.rs @@ -1,457 +0,0 @@ -/* -* This file is part of LibEuFin. -* Copyright (C) 2026 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/> -*/ - -use std::sync::Arc; - -use axum::{ - Json, Router, - extract::State, - middleware::{self, Next}, - response::{IntoResponse, NoContent}, - routing::{get, post}, -}; -use compact_str::CompactString; -use jiff::Timestamp; -use serde::{Deserialize, Serialize}; -use taler_api::{ - error::{ApiResult, failure_code, not_implemented}, - extract::{Path, Query}, -}; -use taler_common::{ - api::{ShortHashCode, params::PageParams}, - error_code::ErrorCode, - types::{amount::Amount, timestamp::TalerTimestamp}, -}; - -use crate::{ - api::BankState, - auth::{AdminRAuth, UserRWAuth}, - db::cashout::{CreationResult, create, get_for_user, page_all, page_for_user}, - mfa::{CashoutOp, MfaReq}, -}; - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[allow(non_camel_case_types)] -pub enum CashoutStatus { - pending, - aborted, - confirmed, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct CashoutRequest { - pub request_uid: ShortHashCode, - pub subject: Option<String>, - pub amount_debit: Amount, - pub amount_credit: Amount, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct CashoutResponse { - pub cashout_id: u64, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct Cashouts { - pub cashouts: Vec<CashoutInfo>, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct CashoutInfo { - pub cashout_id: u64, - pub status: CashoutStatus, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct GlobalCashouts { - pub cashouts: Vec<GlobalCashoutInfo>, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct GlobalCashoutInfo { - pub cashout_id: u64, - pub username: CompactString, - pub status: CashoutStatus, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct CashoutStatusResponse { - pub amount_debit: Amount, - pub amount_credit: Amount, - pub subject: String, - pub creation_time: TalerTimestamp, - pub confirmation_time: Option<TalerTimestamp>, -} - -pub fn cashout_api(state: Arc<BankState>) -> Router<Arc<BankState>> { - Router::new() - .route( - "/cashouts", - get( - async |Query(params): Query<PageParams>, - _: AdminRAuth, - State(state): State<Arc<BankState>>| { - let params = params.check()?; - - let cashouts = page_all(&state.db, &params).await?; - if cashouts.is_empty() { - ApiResult::Ok(NoContent.into_response()) - } else { - Ok(Json(GlobalCashouts { cashouts }).into_response()) - } - }, - ), - ) - .route( - "/accounts/{username}/cashouts", - post( - async |State(state): State<Arc<BankState>>, - MfaReq { mut auth, req, mfa }: MfaReq<CashoutOp>| { - state.cfg.check_regio(&req.amount_debit)?; - state.cfg.check_fiat(&req.amount_credit)?; - match create( - &state.db, - &auth.username, - &req.request_uid, - &req.amount_debit, - &req.amount_credit, - &req.subject.unwrap_or_default(), - &Timestamp::now(), - mfa.is_2fa(), - ) - .await? - { - CreationResult::Success(cashout_id) => { - Ok(Json(CashoutResponse { cashout_id }).into_response()) - } - CreationResult::UnderMin => { - Err(failure_code(ErrorCode::BANK_CONVERSION_AMOUNT_TO_SMALL)) - } - CreationResult::BadConversion => { - Err(failure_code(ErrorCode::BANK_BAD_CONVERSION)) - } - CreationResult::AccountNotFound => { - Err(failure_code(ErrorCode::BANK_UNKNOWN_ACCOUNT)) - } - CreationResult::AccountIsExchange => { - Err(failure_code(ErrorCode::BANK_ACCOUNT_IS_EXCHANGE)) - } - CreationResult::BalanceInsufficient => { - Err(failure_code(ErrorCode::BANK_UNALLOWED_DEBIT)) - } - CreationResult::RequestUidReuse => { - Err(failure_code(ErrorCode::BANK_TRANSFER_REQUEST_UID_REUSED)) - } - CreationResult::NoCashoutPayto => { - Err(failure_code(ErrorCode::BANK_CONFIRM_INCOMPLETE)) - } - CreationResult::TanRequired => Ok(mfa - .response_mfa(&mut auth, &state.db, &state.cfg.ctx) - .await? - .into_response()), - } - }, - ) - .get( - async |Query(params): Query<PageParams>, - auth: UserRWAuth, - State(state): State<Arc<BankState>>| { - let params = params.check()?; - - let cashouts = page_for_user(&state.db, &auth.username, &params).await?; - if cashouts.is_empty() { - ApiResult::Ok(NoContent.into_response()) - } else { - Ok(Json(Cashouts { cashouts }).into_response()) - } - }, - ), - ) - .route( - "/accounts/{username}/cashouts/{id}", - get( - async |Path((_, id)): Path<((), u64)>, - auth: UserRWAuth, - State(state): State<Arc<BankState>>| { - match get_for_user( - &state.db, - &state.cfg.regional_currency, - state.cfg.fiat_currency().unwrap(), - &auth.username, - id, - ) - .await? - { - Some(res) => Ok(Json(res)), - None => Err(failure_code(ErrorCode::BANK_TRANSACTION_NOT_FOUND)), - } - }, - ), - ) - .route_layer(middleware::from_fn_with_state( - state, - async |State(state): State<Arc<BankState>>, req, next: Next| { - if state.cfg.fiat.is_none() { - not_implemented().into_response() - } else { - next.run(req).await - } - }, - )) -} - -#[cfg(test)] -pub mod test { - use axum::http::Method; - use taler_common::{api::ShortHashCode, error_code::ErrorCode, types::amount::amount}; - use taler_test_utils::{ - json, - routine::{Page, routine_pagination}, - server::TestServer, - tasks, - }; - - use crate::api::{ - cashout::{CashoutResponse, CashoutStatusResponse, Cashouts, GlobalCashouts}, - conversion::ConversionRateClassResponse, - test::{Auth, MfaRequest, bank_setup}, - }; - - #[tokio::test] - async fn cashout() { - let ctx = bank_setup().await; - - ctx.auth_routine(Method::POST, "/accounts/merchant/cashouts", Auth::UserOnly) - .await; - ctx.auth_routine( - Method::GET, - "/accounts/merchant/cashouts/42", - Auth::UserOrAdmin, - ) - .await; - ctx.auth_routine( - Method::GET, - "/accounts/merchant/cashouts", - Auth::UserOrAdmin, - ) - .await; - ctx.auth_routine(Method::GET, "/cashouts", Auth::Admin) - .await; - - let str = "KUDOS:1.5"; - let debit = amount(str); - let credit = ctx.convert(str).await; - - let req = json!({ - "request_uid": ShortHashCode::rand(), - "amount_debit": debit, - "amount_credit": credit, - "subject": "test subject" - }); - - // Missing info - ctx.posta("/accounts/customer/cashouts") - .json(&req) - .await - .assert_error(ErrorCode::BANK_CONFIRM_INCOMPLETE); - - ctx.fill_cashout_info("customer").await; - - // Ok - let res = ctx - .posta("/accounts/customer/cashouts") - .json(&req) - .await - .assert_ok_json::<CashoutResponse>(); - // Idempotent - assert_eq!( - res, - ctx.posta("/accounts/customer/cashouts") - .json(&req) - .await - .assert_ok_json::<CashoutResponse>() - ); - - // Trigger conflict due to reused request_uid - ctx.posta("/accounts/customer/cashouts") - .json(json!(req + { - "amount_debit": "KUDOS:2", - "amount_credit": ctx.convert("KUDOS:2").await - })) - .await - .assert_error(ErrorCode::BANK_TRANSFER_REQUEST_UID_REUSED); - - // Check exchange account - ctx.posta("/accounts/exchange/cashouts") - .json(&req) - .await - .assert_error(ErrorCode::BANK_ACCOUNT_IS_EXCHANGE); - - // Check insufficient fund - ctx.posta("/accounts/customer/cashouts") - .json(json!({ - "request_uid": ShortHashCode::rand(), - "amount_debit": "KUDOS:75", - "amount_credit": ctx.convert("KUDOS:75").await, - })) - .await - .assert_error(ErrorCode::BANK_UNALLOWED_DEBIT); - - // Check wrong conversion - ctx.posta("/accounts/customer/cashouts") - .json(json!(req + { - "amount_credit": ctx.convert("KUDOS:2").await - })) - .await - .assert_error(ErrorCode::BANK_BAD_CONVERSION); - - // Check min amount - ctx.posta("/accounts/customer/cashouts") - .json(json!(req + { "amount_debit": "KUDOS:0.09" })) - .await - .assert_error(ErrorCode::BANK_CONVERSION_AMOUNT_TO_SMALL); - - // Check custom min account - let id = ctx - .post_admin("/conversion-rate-classes") - .json(json!({ - "name": "Custom class", - "cashout_min_amount": "KUDOS:10" - })) - .await - .assert_ok_json::<ConversionRateClassResponse>() - .conversion_rate_class_id; - ctx.patch_admin("/accounts/customer") - .json(json!({ - "conversion_rate_class_id": id - })) - .await - .assert_no_content(); - ctx.posta("/accounts/customer/cashouts") - .json(json!(req + { - "amount_debit": "KUDOS:5", - "amount_credit": ctx.convert("KUDOS:5").await - })) - .await - .assert_error(ErrorCode::BANK_CONVERSION_AMOUNT_TO_SMALL); - ctx.patch_admin("/accounts/customer") - .json(json!({ - "conversion_rate_class_id": () - })) - .await - .assert_no_content(); - - // Wrong currency - ctx.posta("/accounts/customer/cashouts") - .json(json!(req + { "amount_debit": "EUR:1" })) - .await - .assert_error(ErrorCode::GENERIC_CURRENCY_MISMATCH); - ctx.posta("/accounts/customer/cashouts") - .json(json!(req + { "amount_credit": "KUDOS:1" })) - .await - .assert_error(ErrorCode::GENERIC_CURRENCY_MISMATCH); - - // 2fa - ctx.fill_tan_info("merchant").await; - ctx.fill_cashout_info("merchant").await; - ctx.assert_balance("merchant", "0").await; - ctx.posta("/accounts/merchant/cashouts") - .json(json!(req + { "request_uid": ShortHashCode::rand() })) - .await - .assert_challenge_check(&ctx, async |_| ctx.assert_balance("merchant", "0").await) - .await - .assert_ok_json::<CashoutResponse>(); - ctx.assert_balance("merchant", "-1.5").await; - - // Get - let c = ctx - .geta(format!("/accounts/customer/cashouts/{}", res.cashout_id)) - .await - .assert_ok_json::<CashoutStatusResponse>(); - assert_eq!( - c, - CashoutStatusResponse { - amount_debit: debit, - amount_credit: credit, - subject: "test subject".into(), - creation_time: c.creation_time, - confirmation_time: Some(c.creation_time) - } - ); - - // Bad ID - ctx.geta("/accounts/customer/cashouts/chocolate") - .await - .assert_error(ErrorCode::GENERIC_PATH_SEGMENT_MALFORMED); - - // Unknown - ctx.geta("/accounts/customer/cashouts/42") - .await - .assert_error(ErrorCode::BANK_TRANSACTION_NOT_FOUND); - - // Another user's operation - ctx.geta(format!("/accounts/merchant/cashouts/{}", res.cashout_id)) - .await - .assert_error(ErrorCode::BANK_TRANSACTION_NOT_FOUND); - - // History - let router = ctx.admin_router().await; - let c = &ctx; - routine_pagination::<Cashouts>( - &router, - "/accounts/customer/cashouts", - tasks!({ - c.cashout("0.1").await; - }), - ) - .await; - routine_pagination::<Cashouts>( - &router, - "/cashouts", - tasks!({ - c.cashout("0.1").await; - }), - ) - .await; - - // Not implemnted - let ctx = ctx.swap_cfg("test_no_conversion.conf").await; - ctx.get("/accounts/customer/cashouts") - .await - .assert_not_implemented(); - } - - impl Page for Cashouts { - fn ids(&self) -> Vec<i64> { - self.cashouts - .iter() - .map(|it| it.cashout_id as i64) - .collect() - } - } - - impl Page for GlobalCashouts { - fn ids(&self) -> Vec<i64> { - self.cashouts - .iter() - .map(|it| it.cashout_id as i64) - .collect() - } - } -} diff --git a/crates/libeufin-bank/src/api/conversion.rs b/crates/libeufin-bank/src/api/conversion.rs @@ -1,1015 +0,0 @@ -/* -* This file is part of LibEuFin. -* Copyright (C) 2026 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/> -*/ - -use std::sync::Arc; - -use axum::{ - Json, Router, - extract::{FromRequestParts, State}, - middleware::{self, Next}, - response::{IntoResponse, NoContent, Response}, - routing::{get, patch, post}, -}; -use compact_str::{CompactString, format_compact}; -use serde::{Deserialize, Serialize}; -use sqlx::prelude::Type; -use taler_api::{ - error::{ApiError, ApiResult, bad_request, failure, failure_code, forbidden, not_implemented}, - extract::{Path, Query, Req}, -}; -use taler_common::{ - api::params::{Page, PageParams, ParamsErr}, - error_code::ErrorCode, - types::amount::{Amount, Currency, Decimal}, -}; -use taler_macros::{EnumMeta, api_config}; - -use crate::{ - api::{BankState, CONVERSION_API_VERSION, IMPLEMENTATION}, - auth::{AdminRAuth, AdminRWAuth, UserOptRAuth, UserRAuth}, - config::{BankCfg, CurrencySpecification}, - db::conversion::{ - ConversionResult, CreateResult, PatchResult, conversion, create, delete_class, get_class, - get_class_rate, get_default_rate, get_user_rate, page_class, patch_class, update_config, - user_conversion, - }, -}; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, EnumMeta, Type)] -#[enum_meta(Str)] -#[sqlx(type_name = "rounding_mode")] -#[allow(non_camel_case_types)] -pub enum RoundingMode { - zero, - up, - nearest, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct ConversionRate { - pub cashin_ratio: Decimal, - pub cashin_fee: Amount, - pub cashin_tiny_amount: Amount, - pub cashin_rounding_mode: RoundingMode, - pub cashin_min_amount: Amount, - pub cashout_ratio: Decimal, - pub cashout_fee: Amount, - pub cashout_tiny_amount: Amount, - pub cashout_rounding_mode: RoundingMode, - pub cashout_min_amount: Amount, -} - -impl ConversionRate { - pub fn check(&self, cfg: &BankCfg) -> ApiResult<()> { - let Self { - cashin_fee, - cashin_tiny_amount, - cashin_min_amount, - cashout_fee, - cashout_tiny_amount, - cashout_min_amount, - .. - } = self; - for amount in &[cashin_fee, cashin_tiny_amount, cashout_min_amount] { - cfg.check_regio(amount)?; - } - for amount in &[cashout_fee, cashout_tiny_amount, cashin_min_amount] { - cfg.check_fiat(amount)?; - } - - if cashout_tiny_amount.is_zero() { - Err(bad_request("cashout_tiny_amount must be > 0")) - } else if cashin_tiny_amount.is_zero() { - Err(bad_request("cashin_tiny_amount must be > 0")) - } else if cashout_tiny_amount.is_sub_cent() { - Err(bad_request( - "Sub-cent amounts no supported by cashout, cashout_tiny_amount must be >= 0.01", - )) - } else { - Ok(()) - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct ConversionResponse { - pub amount_debit: Amount, - pub amount_credit: Amount, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct ConversionRateClassInput { - pub name: CompactString, - pub description: Option<CompactString>, - pub cashin_ratio: Option<Decimal>, - pub cashin_fee: Option<Amount>, - pub cashin_rounding_mode: Option<RoundingMode>, - pub cashin_min_amount: Option<Amount>, - pub cashout_ratio: Option<Decimal>, - pub cashout_fee: Option<Amount>, - pub cashout_rounding_mode: Option<RoundingMode>, - pub cashout_min_amount: Option<Amount>, -} - -impl ConversionRateClassInput { - pub fn check(&self, cfg: &BankCfg) -> ApiResult<()> { - let Self { - cashin_fee, - cashin_min_amount, - cashout_fee, - cashout_min_amount, - .. - } = self; - for amount in [cashin_fee, cashout_min_amount].into_iter().flatten() { - cfg.check_regio(amount)?; - } - for amount in [cashout_fee, cashin_min_amount].into_iter().flatten() { - cfg.check_fiat(amount)?; - } - Ok(()) - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct ConversionRateClassResponse { - pub conversion_rate_class_id: u64, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct ConversionRateClass { - pub conversion_rate_class_id: u64, - pub name: CompactString, - pub description: Option<CompactString>, - pub num_users: u64, - pub cashin_ratio: Option<Decimal>, - pub cashin_fee: Option<Amount>, - pub cashin_rounding_mode: Option<RoundingMode>, - pub cashin_min_amount: Option<Amount>, - pub cashout_ratio: Option<Decimal>, - pub cashout_fee: Option<Amount>, - pub cashout_rounding_mode: Option<RoundingMode>, - pub cashout_min_amount: Option<Amount>, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct ConversionRateClasses { - pub classes: Vec<ConversionRateClass>, -} - -#[api_config("taler-conversion-info")] -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] -pub struct ConversionConfig<'a> { - pub regional_currency: &'a Currency, - pub regional_currency_specification: &'a CurrencySpecification, - pub fiat_currency: &'a Currency, - pub fiat_currency_specification: &'a CurrencySpecification, - pub conversion_rate: &'a ConversionRate, -} - -#[derive(Debug, Clone, Deserialize)] -pub struct ClassParams { - #[serde(flatten)] - pub page: PageParams, - pub filter_name: Option<CompactString>, -} - -impl ClassParams { - pub fn check(self) -> Result<Class, ParamsErr> { - Ok(Class { - page: self.page.check()?, - filter_name: self.filter_name.map(|it| format_compact!("%{it}%")), - }) - } -} - -#[derive(Debug)] -pub struct Class { - pub page: Page, - pub filter_name: Option<CompactString>, -} - -pub struct Conversion; - -impl FromRequestParts<Arc<BankState>> for Conversion { - type Rejection = ApiError; - - async fn from_request_parts( - _: &mut axum::http::request::Parts, - state: &Arc<BankState>, - ) -> Result<Self, Self::Rejection> { - if state.cfg.fiat.is_none() { - Err(not_implemented()) - } else { - Ok(Conversion) - } - } -} - -#[derive(Debug, Clone, Deserialize)] -pub struct RateParams { - amount_debit: Option<Amount>, - amount_credit: Option<Amount>, -} - -impl RateParams { - pub fn check(self, cfg: &BankCfg, cashin: bool) -> ApiResult<Rate> { - let rate: Rate = { - match (self.amount_credit, self.amount_debit) { - (None, None) => { - return Err(failure( - ErrorCode::GENERIC_PARAMETER_MISSING, - "Either param 'amount_debit' or 'amount_credit' is require", - )); - } - (Some(_), Some(_)) => { - return Err(failure( - ErrorCode::GENERIC_PARAMETER_MALFORMED, - "Cannot have both 'amount_debit' and 'amount_credit' params", - )); - } - (None, Some(amount_debit)) => Rate::Debit { amount_debit }, - (Some(amount_credit), None) => Rate::Credit { amount_credit }, - } - }; - if cashin { - match &rate { - Rate::Debit { amount_debit } => cfg.check_fiat(amount_debit)?, - Rate::Credit { amount_credit } => cfg.check_regio(amount_credit)?, - } - } else { - match &rate { - Rate::Debit { amount_debit } => cfg.check_regio(amount_debit)?, - Rate::Credit { amount_credit } => cfg.check_fiat(amount_credit)?, - } - }; - Ok(rate) - } -} - -#[derive(Debug, Clone)] -pub enum Rate { - Debit { amount_debit: Amount }, - Credit { amount_credit: Amount }, -} - -async fn config(State(state): State<Arc<BankState>>) -> ApiResult<Response> { - let (fiat, spec) = state.cfg.fiat.as_ref().unwrap(); - let rate = get_default_rate(&state.db, &state.cfg.regional_currency, fiat).await?; - ApiResult::Ok( - Json(ConversionConfig { - name: (), - version: CONVERSION_API_VERSION, - implementation: Some(IMPLEMENTATION), - conversion_rate: &rate, - fiat_currency: fiat, - fiat_currency_specification: spec, - regional_currency: &state.cfg.regional_currency, - regional_currency_specification: &state.cfg.regional_currency_spec, - }) - .into_response(), - ) -} - -async fn update_cfg( - State(state): State<Arc<BankState>>, - _: AdminRWAuth, - Req(req): Req<ConversionRate>, -) -> ApiResult<NoContent> { - req.check(&state.cfg)?; - update_config(&state.db, &req).await?; - ApiResult::Ok(NoContent) -} - -async fn convert( - cfg: &BankCfg, - params: RateParams, - cashin: bool, - lambda: impl AsyncFnOnce(bool, &Amount) -> sqlx::Result<ConversionResult>, -) -> ApiResult<Response> { - let params = params.check(cfg, cashin)?; - - fn err_handling(res: sqlx::Result<ConversionResult>) -> ApiResult<Amount> { - match res? { - ConversionResult::Success(amount) => Ok(amount), - ConversionResult::ToSmall => Err(failure( - ErrorCode::BANK_BAD_CONVERSION, - "amount is too small to be converted", - )), - ConversionResult::IsExchange => Err(failure( - ErrorCode::BANK_ACCOUNT_IS_EXCHANGE, - "exchange accounts cannot cashout", - )), - ConversionResult::NotExchange => Err(failure( - ErrorCode::BANK_ACCOUNT_IS_NOT_EXCHANGE, - "only exchange accounts can cashin", - )), - } - } - - let (amount_debit, amount_credit) = match params { - Rate::Debit { amount_debit } => ( - amount_debit, - err_handling(lambda(true, &amount_debit).await)?, - ), - Rate::Credit { amount_credit } => ( - err_handling(lambda(false, &amount_credit).await)?, - amount_credit, - ), - }; - Ok(Json(ConversionResponse { - amount_debit, - amount_credit, - }) - .into_response()) -} - -pub fn conversion_api(state: Arc<BankState>) -> Router<Arc<BankState>> { - Router::new() - .route("/conversion-info/config", get(config)) - .route( - "/conversion-rate-classes/{id}/conversion-info/config", - get(config), - ) - .route("/accounts/{username}/conversion-info/config", get(config)) - .route("/conversion-info/conversion-rate", post(update_cfg)) - .route( - "/conversion-rate-classes/{id}/conversion-info/conversion-rate", - post(update_cfg), - ) - .route( - "/accounts/{username}/conversion-info/conversion-rate", - post(update_cfg), - ) - .route( - "/conversion-info/rate", - get(async |State(state): State<Arc<BankState>>| { - ApiResult::Ok(Json( - get_default_rate( - &state.db, - &state.cfg.regional_currency, - state.cfg.fiat_currency().unwrap(), - ) - .await?, - )) - }), - ) - .route( - "/conversion-info/cashout-rate", - get( - async |State(state): State<Arc<BankState>>, Query(params): Query<RateParams>| { - let regio = &state.cfg.regional_currency; - let fiat = state.cfg.fiat_currency().unwrap(); - convert(&state.cfg, params, false, async |to, amount| { - conversion(&state.db, regio, fiat, amount, to, false, None).await - }) - .await - }, - ), - ) - .route( - "/conversion-info/cashin-rate", - get( - async |State(state): State<Arc<BankState>>, Query(params): Query<RateParams>| { - let regio = &state.cfg.regional_currency; - let fiat = state.cfg.fiat_currency().unwrap(); - convert(&state.cfg, params, true, async |to, amount| { - conversion(&state.db, regio, fiat, amount, to, true, None).await - }) - .await - }, - ), - ) - .route( - "/conversion-rate-classes/{id}/conversion-info/rate", - get( - async |_: AdminRAuth, Path(id): Path<u64>, State(state): State<Arc<BankState>>| { - ApiResult::Ok(Json( - get_class_rate( - &state.db, - &state.cfg.regional_currency, - state.cfg.fiat_currency().unwrap(), - id, - ) - .await?, - )) - }, - ), - ) - .route( - "/conversion-rate-classes/{id}/conversion-info/cashout-rate", - get( - async |_: AdminRAuth, - Path(id): Path<u64>, - Query(params): Query<RateParams>, - State(state): State<Arc<BankState>>| { - let regio = &state.cfg.regional_currency; - let fiat = state.cfg.fiat_currency().unwrap(); - convert(&state.cfg, params, false, async |to, amount| { - conversion(&state.db, regio, fiat, amount, to, false, Some(id)).await - }) - .await - }, - ), - ) - .route( - "/conversion-rate-classes/{id}/conversion-info/cashin-rate", - get( - async |_: AdminRAuth, - Path(id): Path<u64>, - Query(params): Query<RateParams>, - State(state): State<Arc<BankState>>| { - let regio = &state.cfg.regional_currency; - let fiat = state.cfg.fiat_currency().unwrap(); - convert(&state.cfg, params, true, async |to, amount| { - conversion(&state.db, regio, fiat, amount, to, true, Some(id)).await - }) - .await - }, - ), - ) - .route( - "/accounts/{username}/conversion-info/rate", - get( - async |auth: UserOptRAuth, State(state): State<Arc<BankState>>| { - let (is_exchange, rate) = get_user_rate( - &state.db, - &state.cfg.regional_currency, - state.cfg.fiat_currency(), - &auth.username, - ) - .await?; - if !is_exchange && !auth.is_authenticated() { - return Err(forbidden("Non exchange account rates are private")); - } - Ok(Json(rate)) - }, - ), - ) - .route( - "/accounts/{username}/conversion-info/cashout-rate", - get( - async |auth: UserRAuth, - Query(params): Query<RateParams>, - State(state): State<Arc<BankState>>| { - let regio = &state.cfg.regional_currency; - let fiat = state.cfg.fiat_currency().unwrap(); - convert(&state.cfg, params, false, async |to, amount| { - user_conversion(&state.db, regio, fiat, amount, to, false, &auth.username) - .await - }) - .await - }, - ), - ) - .route( - "/accounts/{username}/conversion-info/cashin-rate", - get( - async |Path(username): Path<CompactString>, - Query(params): Query<RateParams>, - State(state): State<Arc<BankState>>| { - let regio = &state.cfg.regional_currency; - let fiat = state.cfg.fiat_currency().unwrap(); - convert(&state.cfg, params, true, async |to, amount| { - user_conversion(&state.db, regio, fiat, amount, to, true, &username).await - }) - .await - }, - ), - ) - .route( - "/conversion-rate-classes", - post( - async |_: AdminRWAuth, - State(state): State<Arc<BankState>>, - Req(req): Req<ConversionRateClassInput>| { - req.check(&state.cfg)?; - match create(&state.db, &req).await? { - CreateResult::Success(id) => Ok(Json(ConversionRateClassResponse { - conversion_rate_class_id: id, - })), - CreateResult::NameReuse => Err(failure_code(ErrorCode::BANK_NAME_REUSE)), - } - }, - ) - .get( - async |_: AdminRAuth, - Query(params): Query<ClassParams>, - State(state): State<Arc<BankState>>| { - let params = params.check()?; - let classes = page_class( - &state.db, - &state.cfg.regional_currency, - state.cfg.fiat_currency().unwrap(), - &params, - ) - .await?; - if classes.is_empty() { - ApiResult::Ok(NoContent.into_response()) - } else { - ApiResult::Ok(Json(ConversionRateClasses { classes }).into_response()) - } - }, - ), - ) - .route( - "/conversion-rate-classes/{id}", - patch( - async |_: AdminRWAuth, - Path(id): Path<u64>, - State(state): State<Arc<BankState>>, - Req(req): Req<ConversionRateClassInput>| { - req.check(&state.cfg)?; - match patch_class(&state.db, id, &req).await? { - PatchResult::Success => Ok(NoContent), - PatchResult::Unknown => { - Err(failure_code(ErrorCode::BANK_TRANSACTION_NOT_FOUND)) - } - PatchResult::NameReuse => Err(failure_code(ErrorCode::BANK_NAME_REUSE)), - } - }, - ) - .delete( - async |_: AdminRWAuth, Path(id): Path<u64>, State(state): State<Arc<BankState>>| { - if delete_class(&state.db, id).await? { - Ok(NoContent) - } else { - Err(failure_code(ErrorCode::BANK_TRANSACTION_NOT_FOUND)) - } - }, - ) - .get( - async |_: AdminRAuth, Path(id): Path<u64>, State(state): State<Arc<BankState>>| { - if let Some(class) = get_class( - &state.db, - &state.cfg.regional_currency, - state.cfg.fiat_currency().unwrap(), - id, - ) - .await? - { - Ok(Json(class)) - } else { - Err(failure_code(ErrorCode::BANK_TRANSACTION_NOT_FOUND)) - } - }, - ), - ) - .route_layer(middleware::from_fn_with_state( - state, - async |State(state): State<Arc<BankState>>, req, next: Next| { - if state.cfg.fiat.is_none() { - not_implemented().into_response() - } else { - next.run(req).await - } - }, - )) -} - -#[cfg(test)] -pub mod test { - - use axum::http::Method; - use taler_common::{error_code::ErrorCode, types::amount::amount}; - use taler_test_utils::{json, server::TestServer}; - - use crate::api::{ - conversion::{ - ConversionRate, ConversionRateClass, ConversionRateClassResponse, - ConversionRateClasses, ConversionResponse, - }, - test::{Auth, bank_setup}, - }; - - #[tokio::test] - async fn rates() { - let ctx = bank_setup().await; - - // Auth - for path in [ - "/conversion-rate-classes", - "/conversion-info/conversion-rate", - "/accounts/merchant/conversion-info/conversion-rate", - "/conversion-rate-classes/1/conversion-info/conversion-rate", - ] { - ctx.auth_routine(Method::POST, path, Auth::Admin).await; - } - ctx.auth_routine( - Method::GET, - "/accounts/merchant/conversion-info/rate", - Auth::Optional, - ) - .await; - ctx.auth_routine( - Method::GET, - "/conversion-rate-classes/1/conversion-info/rate", - Auth::Admin, - ) - .await; - for path in [ - "/conversion-rate-classes", - "/conversion-rate-classes/1", - "/conversion-rate-classes/1/conversion-info/cashout-rate?amount_debit=KUDOS:1", - ] { - ctx.auth_routine(Method::GET, path, Auth::Admin).await; - } - ctx.auth_routine( - Method::GET, - "/accounts/merchant/conversion-info/cashout-rate?amount_debit=KUDOS:1", - Auth::UserOrAdmin, - ) - .await; - - // Convertion rate - { - let ok = json!({ - "cashin_ratio": "0.8", - "cashin_fee": "KUDOS:0.02", - "cashin_tiny_amount": "KUDOS:0.01", - "cashin_rounding_mode": "nearest", - "cashin_min_amount": "EUR:0", - "cashout_ratio": "1.25", - "cashout_fee": "EUR:0.003", - "cashout_tiny_amount": "EUR:0.01", - "cashout_rounding_mode": "zero", - "cashout_min_amount": "KUDOS:0.1" - }); - for prefix in ["", "/conversion-rate-classes/1", "/accounts/merchant"] { - // Good rates - ctx.post_admin(format!("{prefix}/conversion-info/conversion-rate")) - .json(&ok) - .await - .assert_no_content(); - // Bad currency - ctx.post_admin(format!("{prefix}/conversion-info/conversion-rate")) - .json(json!(ok + { "cashout_fee": "CHF:0.003" })) - .await - .assert_error(ErrorCode::GENERIC_CURRENCY_MISMATCH); - // Zero tiny amount - ctx.post_admin(format!("{prefix}/conversion-info/conversion-rate")) - .json(json!(ok + { "cashout_tiny_amount": "EUR:0" })) - .await - .assert_bad_request(); - ctx.post_admin(format!("{prefix}/conversion-info/conversion-rate")) - .json(json!(ok + { "cashin_tiny_amount": "KUDOS:0" })) - .await - .assert_bad_request(); - // Subcent cashout tiny amount - ctx.post_admin(format!("{prefix}/conversion-info/conversion-rate")) - .json(json!(ok + { "cashout_tiny_amount": "EUR:0.0001" })) - .await - .assert_bad_request() - } - } - - // User rate - { - ctx.get("/conversion-info/rate") - .await - .assert_ok_json::<ConversionRate>(); - ctx.geta("/accounts/merchant/conversion-info/rate") - .await - .assert_ok_json::<ConversionRate>(); - ctx.get("/accounts/exchange/conversion-info/rate") - .await - .assert_ok_json::<ConversionRate>(); - ctx.get_admin("/conversion-rate-classes/1/conversion-info/rate") - .await - .assert_ok_json::<ConversionRate>(); - } - - // Cashout rate - { - for prefix in ["", "/conversion-rate-classes/1", "/accounts/merchant"] { - // Check conversion to - let res: ConversionResponse = ctx - .get_admin(format!( - "{prefix}/conversion-info/cashout-rate?amount_debit=KUDOS:1" - )) - .await - .assert_ok_json(); - assert_eq!(res.amount_debit, amount("KUDOS:1")); - assert_eq!(res.amount_credit, amount("EUR:1.25")); - - // Check conversion from - let res: ConversionResponse = ctx - .get_admin(format!( - "{prefix}/conversion-info/cashout-rate?amount_credit=EUR:1.257" - )) - .await - .assert_ok_json(); - assert_eq!(res.amount_debit, amount("KUDOS:1.01")); - assert_eq!(res.amount_credit, amount("EUR:1.257")); - - // Too small - ctx.get_admin(format!( - "{prefix}/conversion-info/cashout-rate?amount_debit=KUDOS:0.0008" - )) - .await - .assert_error(ErrorCode::BANK_BAD_CONVERSION); - // No amount - ctx.get_admin(format!("{prefix}/conversion-info/cashout-rate")) - .await - .assert_error(ErrorCode::GENERIC_PARAMETER_MISSING); - // Both amount - ctx.get_admin(format!( - "{prefix}/conversion-info/cashout-rate?amount_debit=EUR:1&amount_credit=KUDOS:1" - )) - .await - .assert_error(ErrorCode::GENERIC_PARAMETER_MALFORMED); - // Wrong format - ctx.get_admin(format!( - "{prefix}/conversion-info/cashout-rate?amount_debit=1" - )) - .await - .assert_error(ErrorCode::GENERIC_PARAMETER_MALFORMED); - ctx.get_admin(format!( - "{prefix}/conversion-info/cashout-rate?amount_credit=1" - )) - .await - .assert_error(ErrorCode::GENERIC_PARAMETER_MALFORMED); - // Wrong currency - ctx.get_admin(format!( - "{prefix}/conversion-info/cashout-rate?amount_debit=EUR:1" - )) - .await - .assert_error(ErrorCode::GENERIC_CURRENCY_MISMATCH); - ctx.get_admin(format!( - "{prefix}/conversion-info/cashout-rate?amount_credit=KUDOS:1" - )) - .await - .assert_error(ErrorCode::GENERIC_CURRENCY_MISMATCH); - } - ctx.geta("/accounts/exchange/conversion-info/cashout-rate?amount_debit=KUDOS:1") - .await - .assert_error(ErrorCode::BANK_ACCOUNT_IS_EXCHANGE); - } - - // Cashin rate - { - for prefix in ["", "/conversion-rate-classes/1", "/accounts/exchange"] { - for (from, to) in [(0.75, 0.58), (0.32, 0.24), (0.66, 0.51)] { - // Check conversion to - let res: ConversionResponse = ctx - .get_admin(format!( - "{prefix}/conversion-info/cashin-rate?amount_debit=EUR:{from}" - )) - .await - .assert_ok_json(); - assert_eq!(res.amount_credit, amount(format!("KUDOS:{to}"))); - assert_eq!(res.amount_debit, amount(format!("EUR:{from}"))); - - // Check conversion from - let res: ConversionResponse = ctx - .get_admin(format!( - "{prefix}/conversion-info/cashin-rate?amount_credit=KUDOS:{to}" - )) - .await - .assert_ok_json(); - assert_eq!(res.amount_credit, amount(format!("KUDOS:{to}"))); - assert_eq!(res.amount_debit, amount(format!("EUR:{from}"))); - } - - // No amount - ctx.get_admin(format!("{prefix}/conversion-info/cashin-rate")) - .await - .assert_error(ErrorCode::GENERIC_PARAMETER_MISSING); - // Both amount - ctx.get_admin(format!( - "{prefix}/conversion-info/cashin-rate?amount_debit=EUR:1&amount_credit=KUDOS:1" - )) - .await - .assert_error(ErrorCode::GENERIC_PARAMETER_MALFORMED); - // Wrong format - ctx.get_admin(format!( - "{prefix}/conversion-info/cashin-rate?amount_debit=1" - )) - .await - .assert_error(ErrorCode::GENERIC_PARAMETER_MALFORMED); - ctx.get_admin(format!( - "{prefix}/conversion-info/cashin-rate?amount_credit=1" - )) - .await - .assert_error(ErrorCode::GENERIC_PARAMETER_MALFORMED); - // Wrong currency - ctx.get_admin(format!( - "{prefix}/conversion-info/cashin-rate?amount_debit=KUDOS:1" - )) - .await - .assert_error(ErrorCode::GENERIC_CURRENCY_MISMATCH); - ctx.get_admin(format!( - "{prefix}/conversion-info/cashin-rate?amount_credit=EUR:1" - )) - .await - .assert_error(ErrorCode::GENERIC_CURRENCY_MISMATCH); - } - ctx.geta("/accounts/merchant/conversion-info/cashin-rate?amount_debit=EUR:1") - .await - .assert_error(ErrorCode::BANK_ACCOUNT_IS_NOT_EXCHANGE); - } - - let prefixes = ["", "/conversion-rate-classes/1", "/accounts/merchant"]; - - // No rate - sqlx::query("DELETE FROM config WHERE key='conversion_rate'") - .execute(&ctx.state.db) - .await - .unwrap(); - for prefix in prefixes { - ctx.get(format!("{prefix}/conversion-info/config")) - .await - .assert_ok(); - ctx.get_admin(format!("{prefix}/conversion-info/cashin-rate")) - .await - .assert_error(ErrorCode::GENERIC_PARAMETER_MISSING); - ctx.get_admin(format!( - "{prefix}/conversion-info/cashout-rate?amount_credit=EUR:1" - )) - .await - .assert_error(ErrorCode::BANK_BAD_CONVERSION); - } - - // Not implemented - let ctx = ctx.swap_cfg("test_no_conversion.conf").await; - ctx.get_admin("/conversion-rate-classes") - .await - .assert_not_implemented(); - ctx.get_admin("/conversion-rate-classes/1") - .await - .assert_not_implemented(); - - for prefix in prefixes { - ctx.get(format!("{prefix}/conversion-info/rate")) - .await - .assert_not_implemented(); - ctx.get(format!("{prefix}/conversion-info/cashin-rate")) - .await - .assert_not_implemented(); - ctx.get(format!("{prefix}/conversion-info/cashout-rate")) - .await - .assert_not_implemented(); - } - } - - #[tokio::test] - async fn classes() { - let ctx = bank_setup().await; - - // Config - for prefix in ["", "/conversion-rate-classes/1", "/accounts/merchant"] { - ctx.get(format!("{prefix}/conversion-info/config")) - .await - .assert_ok(); - } - - let full = json!({ - "description": "A nice little class", - "cashin_ratio": "0.1", - "cashin_fee": "KUDOS:0.2", - "cashin_tiny_amount": "KUDOS:0.3", - "cashin_rounding_mode": "nearest", - "cashin_min_amount": "EUR:0", - "cashout_ratio": "0.4", - "cashout_fee": "EUR:0.5", - "cashout_tiny_amount": "EUR:0.6", - "cashout_rounding_mode": "zero", - "cashout_min_amount": "KUDOS:0.7" - }); - - // Create full - let res: ConversionRateClassResponse = ctx - .post_admin("/conversion-rate-classes") - .json(json!(full + { "name": "Class n°1" })) - .await - .assert_ok_json(); - assert_eq!(res.conversion_rate_class_id, 2); - let rate: ConversionRateClass = ctx - .get_admin("/conversion-rate-classes/2") - .await - .assert_ok_json(); - ctx.patch_admin("/conversion-rate-classes/2") - .json(json!(full + { "name": "Class n°1" })) - .await - .assert_no_content(); - assert_eq!( - rate, - ctx.get_admin("/conversion-rate-classes/2") - .await - .assert_ok_json() - ); - - // Create empty - let res: ConversionRateClassResponse = ctx - .post_admin("/conversion-rate-classes") - .json(json!({ "name": "Class n°2" })) - .await - .assert_ok_json(); - assert_eq!(res.conversion_rate_class_id, 3); - let rate: ConversionRateClass = ctx - .get_admin("/conversion-rate-classes/3") - .await - .assert_ok_json(); - ctx.patch_admin("/conversion-rate-classes/3") - .json(json!({ "name": "Class n°2" })) - .await - .assert_no_content(); - assert_eq!( - rate, - ctx.get_admin("/conversion-rate-classes/3") - .await - .assert_ok_json() - ); - - // Bad currency - ctx.post_admin("/conversion-rate-classes") - .json(json!(full + { - "name": "Bad currency", - "cashout_fee": "CHF:0.003" - })) - .await - .assert_error(ErrorCode::GENERIC_CURRENCY_MISMATCH); - - // Name reuse currency - ctx.post_admin("/conversion-rate-classes") - .json(json!(full + { "name": "Class n°1" })) - .await - .assert_error(ErrorCode::BANK_NAME_REUSE); - ctx.patch_admin("/conversion-rate-classes/3") - .json(json!(full + { "name": "Class n°1" })) - .await - .assert_error(ErrorCode::BANK_NAME_REUSE); - - // Page - assert_eq!( - 3, - ctx.get_admin("/conversion-rate-classes") - .await - .assert_ok_json::<ConversionRateClasses>() - .classes - .len() - ); - for _ in 0..4 { - ctx.create_conversion_rate_class().await; - } - assert_eq!( - 7, - ctx.get_admin("/conversion-rate-classes") - .await - .assert_ok_json::<ConversionRateClasses>() - .classes - .len() - ); - assert_eq!( - 5, - ctx.get_admin("/conversion-rate-classes?filter_name=Gen") - .await - .assert_ok_json::<ConversionRateClasses>() - .classes - .len() - ); - - // Delete all - for id in (1..4).chain(5..9) { - ctx.delete_admin(format!("/conversion-rate-classes/{id}")) - .await - .assert_no_content(); - ctx.delete_admin(format!("/conversion-rate-classes/{id}")) - .await - .assert_error(ErrorCode::BANK_TRANSACTION_NOT_FOUND); - } - ctx.get_admin("/conversion-rate-classes?filter_name=Gen") - .await - .assert_no_content(); - - // Check no classes - ctx.get_admin("/conversion-rate-classes") - .await - .assert_no_content(); - ctx.get_admin("/conversion-rate-classes/1") - .await - .assert_error(ErrorCode::BANK_TRANSACTION_NOT_FOUND); - ctx.patch_admin("/conversion-rate-classes/1") - .json(json!(full + { "name": "Class" })) - .await - .assert_error(ErrorCode::BANK_TRANSACTION_NOT_FOUND); - ctx.delete_admin("/conversion-rate-classes/1") - .await - .assert_error(ErrorCode::BANK_TRANSACTION_NOT_FOUND); - } -} diff --git a/crates/libeufin-bank/src/api/wire.rs b/crates/libeufin-bank/src/api/wire.rs @@ -1,719 +0,0 @@ -/* -* This file is part of LibEuFin. -* Copyright (C) 2026 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/> -*/ - -use std::sync::Arc; - -use axum::{ - Json, Router, - extract::State, - response::{IntoResponse, NoContent, Response}, - routing::{get, post}, -}; -use jiff::Timestamp; -use serde::Deserialize; -use sqlx::PgPool; -use taler_api::{ - api::Validation, - constants::WIRE_GATEWAY_API_VERSION, - error::{ApiResult, failure_code}, - extract::{Path, Query, Req}, - subject::{IncomingSubject, fmt_in_subject}, -}; -use taler_common::{ - api::{ - params::{HistoryParams, Page, PageParams}, - wire::{ - AddIncomingRequest, AddIncomingResponse, AddKycauthRequest, AddMappedRequest, - IncomingHistory, OutgoingHistory, TransferList, TransferRequest, TransferResponse, - TransferState, WireConfig, - }, - }, - error_code::ErrorCode, - types::{amount::Amount, payto::PaytoURI}, -}; - -use crate::{ - api::{BankState, IMPLEMENTATION}, - auth::{UserAdminAuth, WireRAuth, WireRWAuth}, - db::{ - account::check_info, - exchange::{ - AddIncomingResult, TransferResult, add_incoming, incoming_history, outgoing_history, - page_transfer, transfer, transfer_by_id, - }, - }, - payto::BankPayto, -}; - -#[derive(Debug, Clone, Deserialize)] -pub struct TransferParams { - #[serde(flatten)] - pub page: PageParams, - pub status: Option<TransferState>, -} - -impl TransferParams { - pub fn check(self) -> ApiResult<Transfer> { - Ok(Transfer { - page: self.page.check()?, - status: self.status, - }) - } -} - -pub struct Transfer { - pub page: Page, - pub status: Option<TransferState>, -} - -#[derive(Debug, Clone, Deserialize)] -pub struct AccountCheckParams { - pub account: BankPayto, -} - -async fn admin_add_incoming( - db: &PgPool, - username: &str, - amount: &Amount, - debtor: &PaytoURI, - metadata: &IncomingSubject, -) -> ApiResult<Response> { - let payto = BankPayto::try_from(debtor)?; - match add_incoming( - db, - amount, - &payto, - &format!( - "Admin incoming {}", - fmt_in_subject(metadata.ty(), metadata.key()) - ), - username, - &Timestamp::now(), - metadata, - ) - .await? - { - AddIncomingResult::Success { id, timestamp, .. } => Ok(Json(AddIncomingResponse { - row_id: id, - timestamp: timestamp.into(), - }) - .into_response()), - AddIncomingResult::NotAnExchange => { - Err(failure_code(ErrorCode::BANK_ACCOUNT_IS_NOT_EXCHANGE)) - } - AddIncomingResult::UnknownExchange => Err(failure_code(ErrorCode::BANK_UNKNOWN_ACCOUNT)), - AddIncomingResult::UnknownDebtor => Err(failure_code(ErrorCode::BANK_UNKNOWN_DEBTOR)), - AddIncomingResult::BothPartyAreExchange => { - Err(failure_code(ErrorCode::BANK_ACCOUNT_IS_EXCHANGE)) - } - AddIncomingResult::ReservePubReuse => { - Err(failure_code(ErrorCode::BANK_DUPLICATE_RESERVE_PUB_SUBJECT)) - } - AddIncomingResult::UnknownMapping => { - Err(failure_code(ErrorCode::BANK_TRANSFER_MAPPING_UNKNOWN)) - } - AddIncomingResult::MappingReuse => { - Err(failure_code(ErrorCode::BANK_TRANSFER_MAPPING_REUSED)) - } - AddIncomingResult::BalanceInsufficient => { - Err(failure_code(ErrorCode::BANK_UNALLOWED_DEBIT)) - } - } -} - -pub fn wire_api() -> Router<Arc<BankState>> { - Router::new() - .route( - "/accounts/{username}/taler-wire-gateway/config", - get(async |State(state): State<Arc<BankState>>| { - Json(WireConfig { - name: (), - version: WIRE_GATEWAY_API_VERSION, - implementation: Some(IMPLEMENTATION), - currency: state.cfg.regional_currency, - support_account_check: true, - }) - }), - ) - .route( - "/accounts/{username}/taler-wire-gateway/transfer", - post( - async |auth: WireRWAuth, - State(state): State<Arc<BankState>>, - Req(req): Req<TransferRequest>| { - req.check(&state.cfg.regional_currency)?; - let payto = BankPayto::try_from(&req.credit_account)?; - match transfer( - &state.db, - &auth.username, - &req, - &payto, - &Timestamp::now(), - state.cfg.fiat.is_some(), - ) - .await? - { - TransferResult::Success { id, timestamp } => Ok(Json(TransferResponse { - timestamp: timestamp.into(), - row_id: id, - })), - TransferResult::NotAnExchange => { - Err(failure_code(ErrorCode::BANK_ACCOUNT_IS_NOT_EXCHANGE)) - } - TransferResult::UnknownExchange => { - Err(failure_code(ErrorCode::BANK_UNKNOWN_ACCOUNT)) - } - TransferResult::BothPartyAreExchange => { - Err(failure_code(ErrorCode::BANK_ACCOUNT_IS_EXCHANGE)) - } - TransferResult::BalanceInsufficient => { - Err(failure_code(ErrorCode::BANK_UNALLOWED_DEBIT)) - } - TransferResult::ReserveUidReuse => { - Err(failure_code(ErrorCode::BANK_TRANSFER_REQUEST_UID_REUSED)) - } - TransferResult::WtidReuse => { - Err(failure_code(ErrorCode::BANK_TRANSFER_WTID_REUSED)) - } - TransferResult::AdminCreditor => { - Err(failure_code(ErrorCode::BANK_ADMIN_CREDITOR)) - } - } - }, - ), - ) - .route( - "/accounts/{username}/taler-wire-gateway/transfers", - get( - async |Query(params): Query<TransferParams>, - mut auth: WireRAuth, - State(state): State<Arc<BankState>>| { - let params = params.check()?; - let info = auth.bank_info(&state.db, &state.cfg.ctx).await?; - if !info.is_exchange { - return Err(failure_code(ErrorCode::BANK_ACCOUNT_IS_NOT_EXCHANGE)); - } - let transfers = page_transfer( - &state.db, - &state.cfg.ctx, - &state.cfg.regional_currency, - &params.page, - info.bank_account_id, - params.status, - ) - .await?; - if transfers.is_empty() { - ApiResult::Ok(NoContent.into_response()) - } else { - Ok(Json(TransferList { - transfers, - debit_account: info.payto.as_uri(), - }) - .into_response()) - } - }, - ), - ) - .route( - "/accounts/{username}/taler-wire-gateway/transfers/{id}", - get( - async |Path((_, id)): Path<((), u64)>, - mut auth: WireRAuth, - State(state): State<Arc<BankState>>| { - let info = auth.bank_info(&state.db, &state.cfg.ctx).await?; - if !info.is_exchange { - return Err(failure_code(ErrorCode::BANK_ACCOUNT_IS_NOT_EXCHANGE)); - } - match transfer_by_id( - &state.db, - &state.cfg.ctx, - &state.cfg.regional_currency, - info.bank_account_id, - id, - ) - .await? - { - Some(t) => Ok(Json(t)), - None => Err(failure_code(ErrorCode::BANK_TRANSACTION_NOT_FOUND)), - } - }, - ), - ) - .route( - "/accounts/{username}/taler-wire-gateway/history/incoming", - get( - async |Query(params): Query<HistoryParams>, - mut auth: WireRAuth, - State(state): State<Arc<BankState>>| { - let params = params.check()?; - let info = auth.bank_info(&state.db, &state.cfg.ctx).await?; - if !info.is_exchange { - return Err(failure_code(ErrorCode::BANK_ACCOUNT_IS_NOT_EXCHANGE)); - } - let incoming_transactions = incoming_history( - &state.db, - &state.cfg.ctx, - &state.taler_in_channel, - &state.cfg.regional_currency, - &params, - info.bank_account_id, - ) - .await?; - if incoming_transactions.is_empty() { - ApiResult::Ok(NoContent.into_response()) - } else { - Ok(Json(IncomingHistory { - credit_account: info.payto.as_uri(), - incoming_transactions, - }) - .into_response()) - } - }, - ), - ) - .route( - "/accounts/{username}/taler-wire-gateway/history/outgoing", - get( - async |Query(params): Query<HistoryParams>, - mut auth: WireRAuth, - State(state): State<Arc<BankState>>| { - let params = params.check()?; - let info = auth.bank_info(&state.db, &state.cfg.ctx).await?; - if !info.is_exchange { - return Err(failure_code(ErrorCode::BANK_ACCOUNT_IS_NOT_EXCHANGE)); - } - let outgoing_transactions = outgoing_history( - &state.db, - &state.cfg.ctx, - &state.taler_out_channel, - &state.cfg.regional_currency, - &params, - info.bank_account_id, - ) - .await?; - if outgoing_transactions.is_empty() { - ApiResult::Ok(NoContent.into_response()) - } else { - Ok(Json(OutgoingHistory { - debit_account: info.payto.as_uri(), - outgoing_transactions, - }) - .into_response()) - } - }, - ), - ) - .route( - "/accounts/{username}/taler-wire-gateway/admin/add-incoming", - post( - async |auth: UserAdminAuth, - State(state): State<Arc<BankState>>, - Req(req): Req<AddIncomingRequest>| { - req.check(&state.cfg.regional_currency)?; - admin_add_incoming( - &state.db, - &auth.username, - &req.amount, - &req.debit_account, - &IncomingSubject::Reserve(req.reserve_pub), - ) - .await - }, - ), - ) - .route( - "/accounts/{username}/taler-wire-gateway/admin/add-kycauth", - post( - async |auth: UserAdminAuth, - State(state): State<Arc<BankState>>, - Req(req): Req<AddKycauthRequest>| { - req.check(&state.cfg.regional_currency)?; - admin_add_incoming( - &state.db, - &auth.username, - &req.amount, - &req.debit_account, - &IncomingSubject::Kyc(req.account_pub), - ) - .await - }, - ), - ) - .route( - "/accounts/{username}/taler-wire-gateway/admin/add-mapped", - post( - async |auth: UserAdminAuth, - State(state): State<Arc<BankState>>, - Req(req): Req<AddMappedRequest>| { - req.check(&state.cfg.regional_currency)?; - admin_add_incoming( - &state.db, - &auth.username, - &req.amount, - &req.debit_account, - &IncomingSubject::Map(req.authorization_pub), - ) - .await - }, - ), - ) - .route( - "/accounts/{username}/taler-wire-gateway/account/check", - get( - async |Query(params): Query<AccountCheckParams>, - mut auth: WireRAuth, - State(state): State<Arc<BankState>>| { - let info = auth.bank_info(&state.db, &state.cfg.ctx).await?; - if !info.is_exchange { - return Err(failure_code(ErrorCode::BANK_ACCOUNT_IS_NOT_EXCHANGE)); - } - match check_info(&state.db, &params.account).await? { - Some(t) => Ok(Json(t)), - None => Err(failure_code(ErrorCode::BANK_UNKNOWN_ACCOUNT)), - } - }, - ), - ) -} - -#[cfg(test)] -pub mod test { - use axum::http::Method; - use taler_api::subject::fmt_out_subject; - use taler_common::{ - api::{ - EddsaPublicKey, HashCode, ShortHashCode, - wire::{AccountInfo, TransferResponse, TransferState, TransferStatus}, - }, - db::IncomingType, - error_code::ErrorCode, - types::{amount::amount, payto::PaytoImpl}, - }; - use taler_test_utils::{ - json, - routine::{ - admin_add_incoming_routine, in_history_routine, out_history_routine, transfer_routine, - }, - server::TestServer as _, - tasks, - }; - - use crate::api::test::{Auth, bank_setup}; - - #[tokio::test] - async fn transfer() { - let ctx = bank_setup().await; - - ctx.auth_routine( - Method::POST, - "/accounts/merchant/taler-wire-gateway/transfer", - Auth::UserOnly, - ) - .await; - ctx.auth_routine( - Method::GET, - "/accounts/merchant/taler-wire-gateway/transfers/32", - Auth::UserOnly, - ) - .await; - ctx.auth_routine( - Method::GET, - "/accounts/merchant/taler-wire-gateway/transfers", - Auth::UserOnly, - ) - .await; - - let req = json!({ - "request_uid": HashCode::rand(), - "amount": "KUDOS:55", - "exchange_base_url": "http://exchange.example.com/", - "wtid": ShortHashCode::rand(), - "credit_account": ctx.merchant_payto - }); - - ctx.posta("/accounts/exchange/taler-wire-gateway/transfer") - .json(&req) - .await - .assert_error(ErrorCode::BANK_UNALLOWED_DEBIT); - - ctx.set_max_debt("exchange", "1000").await; - transfer_routine( - &ctx.admin_router().await.prefix("/accounts/exchange"), - TransferState::success, - &ctx.customer_payto.as_uri(), - ) - .await; - - let admin_req = json!(req + { - "credit_account" : ctx.admin_payto - }); - - // Check conversion bounce - for _ in 0..2 { - ctx.posta("/accounts/exchange/taler-wire-gateway/transfer") - .json(&admin_req) - .await - .assert_ok(); - } - - let ctx = ctx.swap_cfg("test_no_conversion.conf").await; - // Transfer works for common accounts - - let req = json!(req + { - "request_uid": HashCode::rand(), - "wtid": ShortHashCode::rand(), - }); - for _ in 0..2 { - ctx.posta("/accounts/exchange/taler-wire-gateway/transfer") - .json(&req) - .await - .assert_ok(); - } - // But fails to admin accounts - ctx.posta("/accounts/exchange/taler-wire-gateway/transfer") - .json(json!(admin_req + { - "request_uid": HashCode::rand(), - "wtid": ShortHashCode::rand(), - })) - .await - .assert_error(ErrorCode::BANK_ADMIN_CREDITOR); - - // Dot now fail for unknown account - let res = ctx - .posta("/accounts/exchange/taler-wire-gateway/transfer") - .json(json!(req + { - "request_uid": HashCode::rand(), - "wtid": ShortHashCode::rand(), - "credit_account": ctx.unknown_payto - })) - .await - .assert_ok_json::<TransferResponse>(); - let s: TransferStatus = ctx - .geta(format!( - "/accounts/exchange/taler-wire-gateway/transfers/{}", - res.row_id - )) - .await - .assert_ok_json(); - assert_eq!( - TransferStatus { - status: TransferState::permanent_failure, - status_msg: Some("Unknown account".into()), - amount: amount("KUDOS:55"), - origin_exchange_url: s.origin_exchange_url.clone(), - metadata: None, - wtid: s.wtid.clone(), - credit_account: s.credit_account.clone(), - timestamp: s.timestamp, - }, - s - ); - - // Not an exchange - ctx.geta(format!( - "/accounts/merchant/taler-wire-gateway/transfers/{}", - res.row_id - )) - .await - .assert_error(ErrorCode::BANK_ACCOUNT_IS_NOT_EXCHANGE); - // Another account - ctx.patcha("/accounts/merchant") - .json(json!({ - "is_taler_exchange": true - })) - .await - .assert_no_content(); - ctx.geta(format!( - "/accounts/merchant/taler-wire-gateway/transfers/{}", - res.row_id - )) - .await - .assert_error(ErrorCode::BANK_TRANSACTION_NOT_FOUND); - } - - #[tokio::test] - async fn outgoing_history() { - let ctx = &bank_setup().await; - ctx.auth_routine( - Method::GET, - "/accounts/merchant/taler-wire-gateway/history/outgoing", - Auth::UserOnly, - ) - .await; - ctx.set_max_debt("exchange", "1000").await; - out_history_routine( - &ctx.admin_router().await.prefix("/accounts/exchange"), - tasks!( - // Transactions using clean add incoming logic - { ctx.transfer("10", &ctx.customer_payto, None).await }, - // And with metadata - { - ctx.transfer("12", &ctx.customer_payto, Some("CON:ID".into())) - .await - } - ), - tasks!( - // // Failed transfer - { ctx.transfer("10", &ctx.unknown_payto, None).await }, - // Ignore manual outgoing transaction - { - ctx.tx_s( - "exchange", - "10", - "merchant", - fmt_out_subject( - &ShortHashCode::rand(), - "https://exchange.exmaple.com/", - None, - ), - ) - .await - }, - // Ignore malformed incoming transaction - { ctx.tx("merchant", "10", "exchange",).await }, - // Ignore malformed outgoing transaction - { ctx.tx("exchange", "10", "merchant",).await }, - ), - ) - .await; - } - - #[tokio::test] - async fn incoming_history() { - let ctx = &bank_setup().await; - ctx.auth_routine( - Method::GET, - "/accounts/merchant/taler-wire-gateway/history/incoming", - Auth::UserOnly, - ) - .await; - ctx.set_max_debt("customer", "1000").await; - ctx.set_max_debt("merchant", "1000").await; - in_history_routine( - &ctx.admin_router().await.prefix("/accounts/exchange"), - &ctx.customer_payto.as_uri(), - false, - tasks!( - // Reserve transactions using raw bank transaction logic - { - ctx.tx_s( - "merchant", - "10", - "exchange", - format!("history test with {} reserve pub", EddsaPublicKey::rand()), - ) - .await - }, - // Reserve transactions using withdraw logic - { ctx.withdrawal("9").await }, - // KYC transactions using raw bank transaction logic - { - ctx.tx_s( - "merchant", - "10", - "exchange", - format!( - "history test with KYC:{} account pub", - EddsaPublicKey::rand() - ), - ) - .await - } - ), - tasks!( - // Ignore malformed incoming transaction - { ctx.tx("merchant", "10", "exchange",).await }, - // Ignore malformed outgoing transaction - { ctx.tx("exchange", "10", "merchant",).await }, - ), - ) - .await; - } - - #[tokio::test] - async fn admin_add_incoming() { - let ctx = bank_setup().await; - ctx.auth_routine( - Method::GET, - "/accounts/merchant/taler-wire-gateway/history/incoming", - Auth::UserOnly, - ) - .await; - for ty in IncomingType::entries { - let (path, key) = match ty { - IncomingType::reserve => ("incoming", "reserve_pub"), - IncomingType::kyc => ("kycauth", "account_pub"), - IncomingType::map => ("mapped", "authorization_pub"), - }; - let path = format!("/accounts/exchange/taler-wire-gateway/admin/add-{path}"); - ctx.auth_routine(Method::POST, &path, Auth::Admin).await; - let req = json!({ - "amount": format!("KUDOS:44"), - "debit_account": ctx.customer_payto, - key: EddsaPublicKey::rand(), - }); - - ctx.post_admin(path.replace("exchange", "merchant")) - .json(&req) - .await - .assert_error(ErrorCode::BANK_ACCOUNT_IS_NOT_EXCHANGE); - ctx.post_admin(&path) - .json(json!(req + { - "debit_account": ctx.exchange_payto - })) - .await - .assert_error(ErrorCode::BANK_ACCOUNT_IS_EXCHANGE); - if *ty != IncomingType::map { - ctx.post_admin(&path) - .json(json!(req + { - "amount": format!("KUDOS:44"), - })) - .await - .assert_error(ErrorCode::BANK_UNALLOWED_DEBIT); - } - } - ctx.set_max_debt("customer", "1000").await; - admin_add_incoming_routine( - &ctx.admin_router().await.prefix("/accounts/exchange"), - &ctx.customer_payto.as_full_uri("Customer"), - true, - ) - .await; - } - - #[tokio::test] - async fn account_check() { - let ctx = bank_setup().await; - ctx.geta("/accounts/exchange/taler-wire-gateway/account/check") - .await - .assert_error(ErrorCode::GENERIC_PARAMETER_MISSING); - ctx.geta(format!( - "/accounts/exchange/taler-wire-gateway/account/check?account={}", - ctx.unknown_payto - )) - .await - .assert_error(ErrorCode::BANK_UNKNOWN_ACCOUNT); - ctx.geta(format!( - "/accounts/exchange/taler-wire-gateway/account/check?account={}", - ctx.merchant_payto - )) - .await - .assert_ok_json::<AccountInfo>(); - } -} diff --git a/crates/libeufin-bank/src/lib.rs b/crates/libeufin-bank/src/lib.rs @@ -1,560 +0,0 @@ -/* -* This file is part of LibEuFin. -* Copyright (C) 2026 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/> -*/ - -use std::{ - sync::Arc, - time::{Duration, Instant}, -}; - -use anyhow::{anyhow, bail}; -use compact_str::CompactString; -use dialoguer::Password; -use jiff::{Timestamp, Zoned}; -use rand::{RngExt as _, distr::Alphanumeric}; -use serde::{Deserialize, Serialize}; -use taler_api::api::TalerRouter; -use taler_build::long_version; -use taler_common::{ - CommonArgs, - cli::ConfigCmd, - config::{Config, parser::ConfigSource}, - types::{ - amount::Amount, - base32::Base32, - payto::IbanPayto, - timestamp::{RelativeTime, TalerTimestamp}, - }, -}; -use taler_macros::EnumMeta; -use tracing::info; - -use crate::{ - api::{ - BankState, - account::{ - AccountReconfiguration, ChallengeContactData, Maybe, RegisterAccountRequest, - create_account, create_admin_account, - }, - bank_api, - }, - auth::{TOKEN_PREFIX, TokenScope}, - config::BankCfg, - db::{ - account::{ - CreationResult, PatchAuthResult, PatchResult, bank_info, reconfig, reconfig_password, - }, - dbinit, - gc::collect, - pool, - token::{TokenCreationResult, access}, - }, - payto::{BankPayto, PaytoCtx}, - pw::{PwCrypto, checkpw}, -}; - -pub mod api; -pub mod auth; -pub mod config; -pub mod db; -pub mod mfa; -pub mod payto; -pub mod pw; - -pub const CONFIG_SOURCE: ConfigSource = - ConfigSource::new("libeufin", "libeufin-bank", "libeufin-bank"); - -#[derive(clap::Subcommand, Debug)] -pub enum Cmd { - /// Initialize libeufin-bank database - Dbinit { - /// Reset database (DANGEROUS: All existing data is lost) - #[arg(short, long)] - reset: bool, - }, - /// Change account password - Passwd { - /// Account username - username: CompactString, - /// Account password used for authentication - password: Option<String>, - }, - /// Create authentication token for a user - CreateToken { - /// Account username - #[arg(short, long, visible_alias("user"))] - username: CompactString, - - /// Scope for the token - #[arg(short, long)] - scope: TokenScope, - - /// Custom token validity duration - #[arg(short, long, value_name("forever|MICROS"))] - duration: Option<RelativeTime>, - - /// Optional token description - #[arg(long)] - description: Option<CompactString>, - - /// Make the token refreshable into a new token - #[arg(long)] - refreshable: bool, - - /// Current token to reuse if still valid - #[arg(long)] - current_token: Option<Base32<32>>, - }, - /// Run libeufin-bank HTTP server - Serve, - /// Create an account, returning the payto://-URI associated with it - CreateAccount { - /// Optional JSON payload. If provided, CLI options are ignored. - #[arg()] - json: Option<String>, - - /// Account unique username - #[arg(short, long, visible_alias("user"), required_unless_present("json"))] - username: Option<CompactString>, - - /// Account password used for authentication - #[arg(short, long)] - password: Option<CompactString>, - - /// Legal name of the account owner - #[arg(long, required_unless_present("json"))] - name: Option<CompactString>, - - /// Make this account visible to anyone - #[arg(long)] - public: bool, - - /// Make this account a taler exchange - #[arg(long)] - exchange: bool, - - /// E-Mail address used for TAN transmission - #[arg(long)] - email: Option<CompactString>, - - /// Phone number used for TAN transmission - #[arg(long)] - phone: Option<CompactString>, - - /// Payto URI of a fiat account who receive cashout amount - #[arg(long, alias("cashout_payto_uri"))] - cashout_payto_uri: Option<IbanPayto>, - - /// Payto URI of this account - #[arg(long)] - payto_uri: Option<BankPayto>, - - /// Max debit allowed for this account - #[arg(long, alias("debit_threshold"))] - debit_threshold: Option<Amount>, - - /// Enables 2FA and set the TAN channel used for challenges - #[arg(long)] - #[arg(long, alias("tan_channel"))] - tan_channel: Vec<TanChannel>, - }, - /// Edit an existing account - EditAccount { - /// Account unique username - username: CompactString, - - /// Legal name of the account owner - #[arg(long)] - name: Option<CompactString>, - - /// Make this account visible to anyone - #[arg(long)] - public: Option<bool>, - - /// Make this account a taler exchange - #[arg(long)] - exchange: Option<bool>, - - /// E-Mail address used for TAN transmission - #[arg(long)] - email: Option<Maybe<CompactString>>, - - /// Phone number used for TAN transmission - #[arg(long)] - phone: Option<Maybe<CompactString>>, - - /// Payto URI of a fiat account who receive cashout amount - #[arg(long, alias("cashout_payto_uri"))] - cashout_payto_uri: Option<Maybe<IbanPayto>>, - - /// Max debit allowed for this account - #[arg(long, alias("debit_threshold"))] - debit_threshold: Option<Amount>, - - /// Enables 2FA and set the TAN channel used for challenges - #[arg(long, alias("tan_channel"))] - tan_channel: Option<Vec<TanChannel>>, - }, - /// Run garbage collection: abort expired operations and clean expired data - Gc, - /// Benchmark password hashing algorithm and configuration - BenchPwh, - #[command(subcommand)] - Config(ConfigCmd), -} - -#[derive(clap::Parser, Debug)] -#[command(long_version = long_version(), about, long_about = None)] -pub struct Args { - #[clap(flatten)] - pub common: CommonArgs, - - #[command(subcommand)] - pub cmd: Cmd, -} - -// Allowed values for cashout TAN channels. -#[derive( - sqlx::Type, - Debug, - Clone, - Copy, - PartialEq, - Eq, - PartialOrd, - Ord, - EnumMeta, - Serialize, - Deserialize, - clap::ValueEnum, -)] -#[sqlx(type_name = "tan_enum")] -#[enum_meta(Str)] -#[allow(non_camel_case_types)] -pub enum TanChannel { - sms, - email, -} - -pub async fn run(cfg: Config, cmd: Cmd) -> anyhow::Result<()> { - match cmd { - Cmd::Dbinit { reset } => { - let cfg = BankCfg::parse(cfg)?; - let db = dbinit(&cfg, reset).await?; - match create_admin_account(&db, &cfg, None).await? { - CreationResult::Success(_) => { - info!("Admin's account created") - } - CreationResult::UsernameReuse => {} - CreationResult::PayToReuse - | CreationResult::UnknownConversionClass - | CreationResult::BonusBalanceInsufficient => unreachable!(), - } - } - Cmd::Passwd { username, password } => { - let cfg = BankCfg::parse(cfg)?; - let db = pool(&cfg).await?; - - let pw = match password { - Some(p) => p, - None => Password::new() - .with_prompt("Password") - .with_confirmation("Repeat for confirmation", "Values do not match, try again") - .interact()?, - }; - checkpw(&pw, cfg.pwd_check_quality)?; - match reconfig_password(&db, &cfg.pw_crypto, &username, &pw, None, true).await? { - PatchAuthResult::Success => { - info!("Password change for '{username}' account succeeded") - } - PatchAuthResult::UnknownAccount => { - bail!("Password change for '{username}' account failed: unknown account") - } - PatchAuthResult::OldPasswordMismatch | PatchAuthResult::TanRequired => { - unreachable!() - } - } - } - Cmd::CreateToken { - username, - scope, - duration, - description, - refreshable, - current_token, - } => { - let cfg = BankCfg::parse(cfg)?; - let db = pool(&cfg).await?; - let now = Timestamp::now(); - let new = if let Some(current) = current_token - && let Some(token) = access(&db, current.as_ref(), &now).await? - && token.expiration < TalerTimestamp::Timestamp(now) - && scope.logical().is_valid_scope(token.scope, refreshable) - { - current - } else { - let expiration = - match duration.unwrap_or(RelativeTime::Duration(Duration::from_hours(24))) { - RelativeTime::Forever => TalerTimestamp::Never, - RelativeTime::Duration(duration) => TalerTimestamp::Timestamp( - now.checked_add(duration) - .map_err(|e| anyhow!("Bad token duration: {e}"))?, - ), - }; - let token = Base32::<32>::secure_rand(); - match db::token::create( - &db, - &username, - token.as_ref(), - &now, - &expiration, - &scope, - refreshable, - description.as_deref(), - true, - ) - .await? - { - TokenCreationResult::Success(_) => token, - TokenCreationResult::TanRequired => unreachable!(), - } - }; - println!("{TOKEN_PREFIX}{new}"); - } - Cmd::Serve => { - let cfg = BankCfg::parse(cfg)?; - let db = pool(&cfg).await?; - if cfg.fiat.is_some() { - info!("Ensure exchange account exists"); - let Some(info) = bank_info(&db, &cfg.ctx, "exchange").await? else { - bail!( - "Exchange account missing: an exchange account named 'exchange' is required for conversion to be enabled" - ) - }; - if !info.is_exchange { - bail!( - "Account is not an exchange: an exchange account named 'exchange' is required for conversion to be enabled" - ) - } - info!("Ensure conversion is enabled"); - match std::fs::read_to_string(format!( - "{}/libeufin-conversion-setup.sql", - cfg.db_cfg.sql_dir - )) { - Ok(sql) => { - sqlx::raw_sql(&sql).execute(&db).await?; - } - Err(e) => { - bail!( - "Coult not read libeufin-conversion-setup.sql at '{}': {}", - cfg.db_cfg.sql_dir, - e.kind() - ) - } - }; - } else { - info!("Ensure conversion is disabled"); - match std::fs::read_to_string(format!( - "{}/libeufin-conversion-drop.sql", - cfg.db_cfg.sql_dir - )) { - Ok(sql) => { - sqlx::raw_sql(&sql).execute(&db).await?; - } - Err(e) => { - bail!( - "Coult not read libeufin-conversion-setup.sql at '{}': {}", - cfg.db_cfg.sql_dir, - e.kind() - ) - } - }; - } - let state = Arc::new(BankState::start(db, cfg).await); - bank_api(state.clone()) - .serve(&state.cfg.serve, None) - .await?; - } - Cmd::CreateAccount { - json, - username, - password, - name, - public: is_public, - exchange, - email, - phone, - cashout_payto_uri, - payto_uri, - debit_threshold, - tan_channel, - } => { - let cfg = BankCfg::parse(cfg)?; - let db = pool(&cfg).await?; - let req = if let Some(json_str) = json { - serde_json::from_str::<RegisterAccountRequest>(&json_str) - .map_err(|e| anyhow!("Failed to parse JSON: {e}"))? - } else { - RegisterAccountRequest { - username: username.unwrap(), - password: match password { - Some(p) => p, - None => Password::new() - .with_prompt("Password") - .with_confirmation( - "Repeat for confirmation", - "Values do not match, try again", - ) - .interact()? - .into(), - }, - name: name.unwrap(), - is_public, - is_taler_exchange: exchange, - contact_data: ChallengeContactData { - email: email.into(), - phone: phone.into(), - }, - cashout_payto_uri, - payto_uri, - debit_threshold, - tan_channel: None, - tan_channels: Some(tan_channel), - conversion_rate_class_id: None, - } - }; - - match create_account(&db, &cfg, &req, true).await? { - CreationResult::Success(full_payto) => { - info!("Acount '{}' created", req.username); - println!("{full_payto}") - } - CreationResult::UsernameReuse => { - bail!("Account username reuse '{}'", req.username) - } - CreationResult::PayToReuse => bail!("Bank internalPayToUri reuse"), - CreationResult::UnknownConversionClass => unreachable!(), - CreationResult::BonusBalanceInsufficient => { - bail!("Insufficient admin funds to grant bonus") - } - } - } - Cmd::EditAccount { - username, - name, - public, - exchange, - email, - phone, - cashout_payto_uri, - debit_threshold, - tan_channel, - } => { - let cfg = BankCfg::parse(cfg)?; - let db = pool(&cfg).await?; - let req = AccountReconfiguration { - name, - is_taler_exchange: exchange, - is_public: public, - contact_data: ChallengeContactData { - email: email.into(), - phone: phone.into(), - }, - cashout_payto_uri: cashout_payto_uri.into(), - debit_threshold, - tan_channel: Maybe::Missing, - tan_channels: tan_channel, - conversion_rate_class_id: Maybe::Missing, - }; - match reconfig( - &db, - &cfg.regional_currency, - &username, - &req, - true, - true, - true, - true, - ) - .await? - { - PatchResult::UnknownAccount => { - bail!("Password change for '{username}' account failed: unknown account") - } - PatchResult::NonAdminName - | PatchResult::NonAdminCashout - | PatchResult::NonAdminDebtLimit - | PatchResult::NonAdminConversionRateClass - | PatchResult::UnknownConversionClass - | PatchResult::Challenges(_) => unreachable!(), - PatchResult::MissingTanInfo(e) => bail!("{e}"), - PatchResult::Success => info!("Account '{username}' edited"), - } - } - Cmd::Gc => { - let cfg = BankCfg::parse(cfg)?; - let db = pool(&cfg).await?; - let now = Zoned::now(); - collect( - &db, - &(now.clone() + cfg.gc_abort_after).timestamp(), - &(now.clone() + cfg.gc_clean_after).timestamp(), - &(now + cfg.gc_delete_after).timestamp(), - ) - .await?; - } - Cmd::BenchPwh => { - let cfg = BankCfg::parse(cfg)?; - let pwc = cfg.pw_crypto; - - match pwc { - PwCrypto::Bcrypt { cost } => { - println!("Benching bcrypt with cost={cost} for 10s"); - } - PwCrypto::Sha256 => unreachable!(), - } - - let start = Instant::now(); - let stop = start + Duration::from_secs(10); - let mut count = 0; - - loop { - let now = Instant::now(); - if now < stop { - let password: String = rand::rng() - .sample_iter(&Alphanumeric) - .take(20) - .map(char::from) - .collect(); - - pwc.hashpw(&password); - count += 1; - } else { - let elapsed = start.elapsed().as_secs_f64(); - let per_sec = count as f64 / elapsed; - let iter_time_ms = (elapsed * 1000.0) / count as f64; - - println!("hash password in {iter_time_ms:.0}ms {per_sec:.2} H/s"); - break; - } - } - } - Cmd::Config(cmd) => cmd.run(&cfg)?, - } - Ok(()) -} diff --git a/crates/libeufin-ebics/src/ebics/order.rs b/crates/libeufin-ebics/src/ebics/order.rs @@ -1,270 +0,0 @@ -/* -* This file is part of LibEuFin. -* Copyright (C) 2026 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/> -*/ - -use compact_str::CompactString; -use taler_macros::EnumMeta; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Direction { - Download, - Upload, -} - -#[derive(Debug, Clone)] -pub struct BTF { - pub service: CompactString, - pub scope: Option<CompactString>, - pub option: Option<CompactString>, - pub container: Option<CompactString>, - pub msg: CompactString, - pub version: Option<CompactString>, -} - -impl PartialEq for BTF { - fn eq(&self, other: &Self) -> bool { - self.service == other.service - && self.scope == other.scope - && self.option == other.option - && self.container == other.container - && self.msg == other.msg - // Ignore msg version - } -} - -impl std::fmt::Display for BTF { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let BTF { - service: name, - scope, - option, - container, - msg, - version, - } = self; - write!(f, "{name}")?; - for part in [scope, container, option].into_iter().flatten() { - write!(f, "-{part}")?; - } - write!(f, "-{msg}")?; - if let Some(version) = version { - write!(f, ".{version}")?; - } - Ok(()) - } -} - -#[derive(Debug, Clone, PartialEq)] -pub enum Order { - /// Download of a file identified by a BTF structure (Mandatory) - BTD(BTF), - /// Upload of a file identified by a BTF structure (Mandatory) - BTU(BTF), - /// Download retrievable order types (Optional) - HAA, - /// Download customer acknowledgment (Mandatory) - HAC, - /// Send amendment of the subscriber key for identification and authentication and encryption (Mandatory) - HCA, - /// Transmission of the subscriber key for ES identification and authentication and encryption (Mandatory) - HCS, - /// Download supported EBICS versions (Mandatory) - HEV, - /// Transmission of the subscriber key for identification and authentication and encryption within the framework of subscriber initialization (Mandatory) - HIA, - /// Download customer’s customer and subscriber data (Optional) - HKD, - /// Transfer the public bank key (Mandatory) - HPB, - /// Download bank parameters (Mandatory) - HPD, - /// Download subscriber’s customer and subscriber data (Mandatory) - HTD, - /// Download subscriber’s customer and subscriber data (Optional) - HVD, - /// Add EDSsignature (Mandatory) - HVE, - /// Cancellation of orders in the EDS (Mandatory) - HVS, - /// Retrieve EDS transaction details (Mandatory) - HVT, - /// Download EDS overview (Mandatory) - HVU, - /// Download EDS overview with additional informations (Mandatory) - HVZ, - /// Transmission of all public keys (subscriber key, key for identification and authentication and key for encryption) for initialisation in case of CA-issued certificates (Optional) - H3K, - /// Send password initialization - INI, - /// Send public key for signature verification - PUB, - /// Suspension of access authorisation - SPR, - /// deprecated - PTK, -} - -impl Order { - pub const WSS_PARAMS: Self = Self::BTD(BTF { - service: CompactString::const_new("OTH"), - scope: Some(CompactString::const_new("DE")), - msg: CompactString::const_new("wssparam"), - version: None, - container: None, - option: None, - }); - - pub fn doc(&self) -> Option<OrderDoc> { - match self { - Self::HAC => Some(OrderDoc::acknowledgement), - Self::BTD(BTF { msg, .. }) => match msg.as_str() { - "pain.002" => Some(OrderDoc::status), - "camt.052" => Some(OrderDoc::report), - "camt.053" => Some(OrderDoc::statement), - "camt.054" => Some(OrderDoc::notification), - _ => None, - }, - _ => None, - } - } - - /** Check if EBICS order is a downloadable one */ - pub fn is_downloadable(&self) -> bool { - matches!( - self.doc(), - Some(OrderDoc::acknowledgement) - | Some(OrderDoc::status) - | Some(OrderDoc::report) - | Some(OrderDoc::statement) - | Some(OrderDoc::notification) - ) - } - - /** Check if EBICS order is an uploadable one */ - pub fn is_upload(&self) -> bool { - matches!(self, Self::BTU { .. }) - } - - pub fn schema(&self) -> &'static str { - "H005" - } - - pub fn file_type(&self) -> &str { - match self { - Order::BTD(BTF { container, .. }) | Order::BTU(BTF { container, .. }) => { - container.as_deref().unwrap_or("xml") - } - _ => "xml", - } - } - - pub fn ty(&self) -> &'static str { - match self { - Order::BTD { .. } => "BTD", - Order::BTU { .. } => "BTU", - Order::HAA => "HAA", - Order::HAC => "HAC", - Order::HCA => "HCA", - Order::HCS => "HCS", - Order::HEV => "HEV", - Order::HIA => "HIA", - Order::HKD => "HKD", - Order::HPB => "HPB", - Order::HPD => "HPD", - Order::HTD => "HTD", - Order::HVD => "HVD", - Order::HVE => "HVE", - Order::HVS => "HVS", - Order::HVT => "HVT", - Order::HVU => "HVU", - Order::HVZ => "HVZ", - Order::H3K => "H3K", - Order::INI => "INI", - Order::PUB => "PUB", - Order::SPR => "SPR", - Order::PTK => "PTK", - } - } - - pub fn from_parts(ty: &str, btf: Option<BTF>) -> Option<Self> { - match (ty, btf) { - ("BTU", Some(btf)) => Some(Self::BTU(btf)), - ("BTD", Some(btf)) => Some(Self::BTD(btf)), - ("HAA", None) => Some(Self::HAA), - ("HAC", None) => Some(Self::HAC), - ("HCA", None) => Some(Self::HCA), - ("HCS", None) => Some(Self::HCS), - ("HEV", None) => Some(Self::HEV), - ("HIA", None) => Some(Self::HIA), - ("HKD", None) => Some(Self::HKD), - ("HPB", None) => Some(Self::HPB), - ("HPD", None) => Some(Self::HPD), - ("HTD", None) => Some(Self::HTD), - ("HVD", None) => Some(Self::HVD), - ("HVE", None) => Some(Self::HVE), - ("HVS", None) => Some(Self::HVS), - ("HVT", None) => Some(Self::HVT), - ("HVU", None) => Some(Self::HVU), - ("HVZ", None) => Some(Self::HVZ), - ("H3K", None) => Some(Self::H3K), - ("INI", None) => Some(Self::INI), - ("PUB", None) => Some(Self::PUB), - ("SPR", None) => Some(Self::SPR), - ("PTK", None) => Some(Self::PTK), - _ => None, - } - } -} - -impl std::fmt::Display for Order { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(self.ty())?; - match self { - Order::BTD(btf) | Order::BTU(btf) => write!(f, "-{btf}"), - _ => Ok(()), - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumMeta, PartialOrd, Ord)] -#[enum_meta(Str, Description)] -#[allow(non_camel_case_types)] -pub enum OrderDoc { - /// EBICS acknowledgement - CustomerAcknowledgement HAC pain.002 - acknowledgement, - /// Payment status - CustomerPaymentStatusReport pain.002 - status, - /// Debit & credit notifications - BankToCustomerDebitCreditNotification camt.054 - notification, - /// Account statements - BankToCustomerStatement camt.053 - statement, - /// Account intraday reports - BankToCustomerAccountReport camt.052 - report, -} - -impl OrderDoc { - pub fn short_description(&self) -> &'static str { - match self { - Self::acknowledgement => "EBICS acknowledgement", - Self::status => "Payment status", - Self::report => "Account intraday reports", - Self::statement => "Account statements", - Self::notification => "Debit & credit notifications", - } - } -} diff --git a/crates/libeufin-ebics/src/iso20022.rs b/crates/libeufin-ebics/src/iso20022.rs @@ -1,183 +0,0 @@ -/* -* This file is part of LibEuFin. -* Copyright (C) 2026 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/> -*/ - -use taler_macros::EnumMeta; - -pub mod bank_tx_code; -pub mod camt; -pub mod hac; -pub mod model; -pub mod pain001; -pub mod pain002; -pub mod status_code; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumMeta)] -#[enum_meta(Description, Str)] -#[allow(non_camel_case_types)] -pub enum HacAction { - /// File submitted to the bank - FILE_UPLOAD, - /// File downloaded from the bank - FILE_DOWNLOAD, - /// Electronic signature submitted to the bank - ES_UPLOAD, - /// Electronic signature downloaded from the bank - ES_DOWNLOAD, - /// Signature verification - ES_VERIFICATION, - /// Forwarding to EDS - VEU_FORWARDING, - /// EDS signature verification - VEU_VERIFICATION, - /// Forwarded for postprocessing - VEU_VERIFICATION_END, - /// Cancellation of EDS order - VEU_CANCEL_ORDER, - /// Additional information - ADDITIONAL, - /// HAC end of order (positive) - ORDER_HAC_FINAL_POS, - /// HAC end of order (negative) - ORDER_HAC_FINAL_NEG, - // Not in the spec but Credit Suisse test suite use it - /// HAC end of order - ORDER_HAC_FINAL, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumMeta)] -#[enum_meta(Description, Str)] -pub enum ChargeBearer { - /// BorneByDebtor - DEBT, - /// BorneByCreditor - CRED, - /// Shared - SHAR, - /// SLEV - SLEV, -} - -#[cfg(test)] -pub mod test { - use tracing::info; - - use crate::{ - ebics::administrative::{parse_haa, parse_hkd}, - iso20022::{camt::parse_camt, hac::parse_hac, pain002::parse_pain002}, - }; - - #[test] - pub fn sample() { - taler_test_utils::setup_tracing(); - let mut samples = Vec::new(); - for entry in std::fs::read_dir("../../testbench/sample").unwrap() { - let entry = entry.unwrap(); - let path = entry.path(); - if path.is_dir() { - for entry in std::fs::read_dir(path).unwrap() { - let entry = entry.unwrap(); - samples.push((entry.path(), entry.file_name())); - } - } else { - samples.push((path, entry.file_name())); - } - } - for (path, name) in samples { - let xml = std::fs::read(&path).unwrap(); - let name = name.to_string_lossy(); - - info!("Parse sample {path:?}"); - - if name.contains("hac") { - parse_hac(&xml).unwrap(); - } else if name.contains("camt") { - parse_camt(&xml).unwrap(); - } else if name.contains("pain002") { - parse_pain002(&xml).unwrap(); - } else if name.contains("pain001") { - // Ignore - } else { - panic!("Unsupported file type {name}") - } - } - } - - #[test] - pub fn logs() { - taler_test_utils::setup_tracing(); - - if !std::fs::exists("testbench/test").unwrap() { - return; - } - for platform in std::fs::read_dir("testbench/test") - .unwrap() - .map(Result::unwrap) - { - let path = platform.path(); - if !path.is_dir() || platform.file_name() == "platform" { - continue; - } - - // List logs - let mut logs = Vec::new(); - for date in std::fs::read_dir(path).unwrap().map(Result::unwrap) { - let path = date.path(); - if !path.is_dir() { - continue; - } - for tx in std::fs::read_dir(path).unwrap().map(Result::unwrap) { - let payload = tx.path().join("payload"); - if payload.exists() { - logs.extend( - std::fs::read_dir(payload) - .unwrap() - .map(|it| it.unwrap().path()), - ); - } - let payload = tx.path().join("payload.xml"); - if payload.exists() { - logs.push(payload); - } - } - } - for path in logs { - let xml = std::fs::read(&path).unwrap(); - let path = path.to_string_lossy(); - - info!("Parse sample {path:?}"); - - if path.contains("HAC") { - parse_hac(&xml).unwrap(); - } else if path.contains("HKD") { - parse_hkd(&xml).unwrap(); - } else if path.contains("HAA") { - parse_haa(&xml).unwrap(); - } else if path.contains("camt") { - parse_camt(&xml).unwrap(); - } else if path.contains("pain.002") { - parse_pain002(&xml).unwrap(); - } else if path.contains("pain.001") { - // Ignore - } else { - panic!("Unsupported file type {path}") - } - } - } - } -} diff --git a/crates/libeufin-ebics/src/iso20022/camt.rs b/crates/libeufin-ebics/src/iso20022/camt.rs @@ -1,1249 +0,0 @@ -/* -* This file is part of LibEuFin. -* Copyright (C) 2026 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/> -*/ - -use std::{fmt::Write as _, str::FromStr}; - -use compact_str::CompactString; -use jiff::{Timestamp, civil, tz::TimeZone}; -use taler_common::types::{ - amount::{Amount, Currency}, - iban::IBAN, - payto::{BankID, IbanPayto, PaytoImpl, PaytoURI}, -}; -use taler_macros::EnumMeta; -use tracing::{trace, warn}; -use uuid::Uuid; - -use crate::{ - iso20022::{ - ChargeBearer, - bank_tx_code::{BankTxDomainCode, BankTxFamilyCode, BankTxSubFamilyCode}, - model::{BatchId, InId, InTx, OutBatch, OutId, OutReversal, OutTx, Tx}, - status_code::ReturnReason, - }, - xml::{self, Xml, XmlAccess as _}, -}; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumMeta)] -#[enum_meta(Str)] -#[allow(clippy::upper_case_acronyms)] -enum Kind { - CRDT, - DBIT, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum AccountId { - Iban(IBAN), - Other(CompactString), -} - -impl std::fmt::Display for AccountId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - AccountId::Iban(iban) => iban.fmt(f), - AccountId::Other(id) => id.fmt(f), - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct AccountTransactions { - pub id: AccountId, - pub currency: Option<Currency>, - pub txs: Vec<Tx>, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -enum OutIds { - Tx(OutId), - Batch(BatchId), -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -struct BankTxCode { - domain: BankTxDomainCode, - family: BankTxFamilyCode, - subfamily: BankTxSubFamilyCode, -} - -impl BankTxCode { - fn is_reversal(&self) -> bool { - matches!( - self.subfamily, - BankTxSubFamilyCode::RPCR | BankTxSubFamilyCode::RRTN | BankTxSubFamilyCode::PSTE - ) - } -} - -impl std::fmt::Display for BankTxCode { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let Self { - domain, - family, - subfamily, - } = self; - write!( - f, - "{domain} {family} {subfamily} - '{}' '{}' '{}'", - domain.description(), - family.description(), - subfamily.description() - ) - } -} - -/** Parse the instruction execution date */ -fn execution_date(n: Xml) -> xml::Result<Timestamp> { - // Value date if present else booking date - let date = n - .opt("ValDt") - .transpose() - .unwrap_or_else(|| n.one("BookgDt"))?; - let date = if let Some(date) = date.opt("Dt")? { - date.parse::<civil::Date>()?.into() - } else { - date.one("DtTm").parse::<civil::DateTime>()? - }; - Ok(date.to_zoned(TimeZone::UTC).unwrap().timestamp()) -} - -/** Parse a payto */ -fn payto(n: Xml, prefix: &str) -> xml::Result<Option<PaytoURI>> { - let Some(parties) = n.opt("RltdPties")? else { - return Ok(None); - }; - - let Some(iban) = parties - .opt(&format!("{prefix}Acct")) - .one("Id") - .opt("IBAN") - .parse()? - else { - return Ok(None); - }; - // TODO parse BIC - let bank_id = BankID { iban, bic: None }; - Ok(Some(if let Some(p) = parties.opt(prefix)? { - let name = p - .opt("Nm") - .transpose() - .unwrap_or_else(|| p.one("Pty").one("Nm"))? - .text(); - IbanPayto::new(bank_id).as_full_uri(name) - } else { - IbanPayto::new(bank_id).as_uri() - })) -} - -/** Parse batch message ID and transaction end-to-end ID as generated by libeufin-nexus */ -fn outgoing_id(n: Xml, sref: Option<&str>) -> xml::Result<OutIds> { - Ok(if let Some(refs) = n.opt("Refs")? { - let e2e_id: Option<CompactString> = refs.opt("EndToEndId").parse()?; - let msg_id: Option<CompactString> = refs.opt("MsgId").parse()?; - let sref: Option<CompactString> = - sref.filter(|it| *it != "NOTPROVIDED").map(|it| it.into()); - match (e2e_id, msg_id) { - // This is a batch representation - (None, Some(msg_id)) => OutIds::Batch(BatchId { msg_id, sref }), - // If not set use MsgId as end-to-end ID for retrocompatibility - (Some(e2e_id), msg_id) if &e2e_id == "NOTPROVIDED" => OutIds::Tx(OutId { - e2e_id: msg_id.clone(), - msg_id, - sref, - }), - (e2e_id, msg_id) => OutIds::Tx(OutId { - msg_id, - e2e_id, - sref, - }), - } - } else { - OutIds::Tx(OutId { - msg_id: None, - e2e_id: None, - sref: sref.map(|it| it.into()), - }) - }) -} - -/** Parse transaction ids as provided by bank */ -fn incoming_id(n: Xml, sref: Option<&str>) -> xml::Result<InId> { - if let Some(refs) = n.opt("Refs")? { - let uetr: Option<Uuid> = refs.opt("UETR").parse()?; - let tx_id: Option<CompactString> = refs.opt("TxId").parse()?; - Ok(InId { - uetr, - tx_id, - sref: sref.map(|it| it.into()), - }) - } else { - Ok(InId { - uetr: None, - tx_id: None, - sref: sref.map(|it| it.into()), - }) - } -} - -/** Parse transaction wire transfer subject */ -fn wire_transfer_subject(n: Xml) -> xml::Result<Option<String>> { - Ok(n.opt("RmtInf")? - .map(|n| n.many("Ustrd").map(|n| n.text()).collect::<String>())) -} - -/** Parse and format transaction return reasons */ -fn return_reason(n: Xml) -> xml::Result<String> { - let mut buf = String::new(); - if let Some(n) = n.opt("RtrInf")? { - let code: ReturnReason = n.one("Rsn").one("Cd").parse()?; - - write!(&mut buf, "{code} '{}'", code.description()).unwrap(); - let mut infos = n.many("AddtlInf"); - if let Some(first) = infos.next() { - buf.push_str(" - '"); - buf.push_str(first.text()); - for info in infos { - buf.push_str(info.text()); - } - buf.push('\''); - } - } else if let Some(n) = wire_transfer_subject(n)? { - return Ok(n); - } - Ok(buf) -} -/** Parse amount */ -fn amount(n: Xml) -> xml::Result<Amount> { - let amt = n.one("Amt")?; - let currency = amt.attr("Ccy")?; - let amount = amt.text(); - let concat = format!("{currency}:0{amount}"); - Amount::from_str(&concat).map_err(|e| amt.parse_err(e)) -} - -#[derive(Debug, Clone, Copy)] -struct ComplexAmount { - /// Transaction amount - amount: Amount, - /// The applied fee - fee: Amount, -} - -impl ComplexAmount { - /// Check that entry and tx amount are compatible and return the result - fn resolve(&self, tx: &ComplexAmount) -> xml::Result<ComplexAmount> { - // Most time transaction will match - if self.amount == tx.amount && self.fee == tx.fee { - return Ok(*self); - } - - // Or one of the level is missing the fee - if (tx.amount.decimal() > tx.fee.decimal() - && tx.amount.try_sub(&tx.fee).unwrap() == self.amount) - || self.amount.try_sub(&self.fee).unwrap() == tx.amount - { - return if tx.fee.is_zero() { Ok(*self) } else { Ok(*tx) }; - } - - // Or the conversion information are only present at the entry layer - if tx.amount.currency != self.amount.currency { - return Ok(*self); - } - - panic!("Amount mismatch, got {self:?} in the entry and {tx:?} in the tx") - } -} - -struct ChargeRecord { - amount: Amount, - kind: Kind, - included: bool, - bearer: ChargeBearer, -} - -fn charges(n: Xml) -> xml::Result<Vec<ChargeRecord>> { - if let Some(n) = n.opt("Chrgs")? { - n.many("Rcrd") - .map(|n| { - Ok(ChargeRecord { - amount: amount(n)?, - kind: n.opt("CdtDbtInd").parse()?.unwrap_or(Kind::CRDT), - included: n.opt("ChrgInclInd").parse()? == Some(true), // TODO not clear in spec - bearer: n.opt("Br").parse()?.unwrap_or(ChargeBearer::SHAR), - }) - }) - .collect() - } else { - Ok(Vec::new()) - } -} - -fn complex_amount(amt: Xml, charges: &[ChargeRecord]) -> xml::Result<ComplexAmount> { - // Amount before charges - let currency = amt.attr("Ccy")?; - // In case of fee overflow it's possible to have a negative amount here - // We ignore this as it will be handled elsewhere correctly - let amount = amt.text().trim_start_matches('-'); - let concat = format!("{currency}:0{amount}"); - - let mut amount = Amount::from_str(&concat).map_err(|e| amt.parse_err(e))?; - let mut fee = Amount::zero(&amount.currency); - - for chr in charges { - if chr.included && !chr.amount.is_zero() { - fee = fee.try_add(&chr.amount).expect("Should never overflow"); - if chr.kind == Kind::DBIT { - if chr.bearer == ChargeBearer::DEBT { - if chr.amount.decimal() > amount.decimal() { - // This can happen when an incoming transaction fail because of debit fee - amount = chr.amount.try_sub(&amount).expect("Should never overflow"); - } else { - amount = amount.try_sub(&chr.amount).expect("Should never overflow"); - } - } else if chr.bearer == ChargeBearer::CRED { - amount = amount.try_add(&chr.amount).expect("Should never overflow"); - } else { - return Err(amt.parse_err(format_args!( - "Included charge {} with bearer {}", - chr.kind, chr.bearer - ))); - } - } - } - } - - Ok(ComplexAmount { amount, fee }) -} - -/** Parse bank transaction code */ -fn bank_tx_code(n: Xml) -> xml::Result<BankTxCode> { - let domnd = n.one("Domn")?; - let fmly = domnd.one("Fmly")?; - Ok(BankTxCode { - domain: domnd.one("Cd").parse()?, - family: fmly.one("Cd").parse()?, - subfamily: fmly.one("SubFmlyCd").parse()?, - }) -} - -/** Parse camt files */ -pub fn parse_camt(xml: &[u8]) -> xml::Result<Vec<AccountTransactions>> { - /* - In ISO 20022 specifications, most fields are optional and the same information - can be written several times in different places. For libeufin, we're only - interested in a subset of the available values that can be found in both camt.052, - camt.053 and camt.054. This function should not fail on legitimate files and should - simply warn when available information are insufficient. - - EBICS and ISO20022 do not provide a perfect transaction identifier. The best is the - UETR (unique end-to-end transaction reference), which is a universally unique - identifier (UUID). However, it is not supplied by all banks. TxId (TransactionIdentification) - is a unique identification as assigned by the first instructing agent. As its format - is ambiguous, its uniqueness is not guaranteed by the standard, and it is only - supposed to be unique for a “pre-agreed period”, whatever that means. These two - identifiers are optional in the standard, but have the advantage of being unique - and can be used to track a transaction between banks so we use them when available. - - It is also possible to use AccountServicerReference, which is a unique reference - assigned by the account servicing institution. They can be present at several levels - (batch level, transaction level, etc.) and are often optional. They also have the - disadvantage of being known only by the account servicing institution. They should - therefore only be used as a last resort. - */ - trace!("Parse transactions camt file"); - - fn parse_inner(root: Xml) -> xml::Result<AccountTransactions> { - let (id, currency) = { - let account = root.one("Acct")?; - let id = account.one("Id")?; - let account_id = if let Some(iban) = id.opt("IBAN")? { - AccountId::Iban(iban.parse()?) - } else { - AccountId::Other(id.one("Othr").one("Id").parse()?) - }; - let currency: Option<Currency> = account.opt("Ccy").parse()?; - (account_id, currency) - }; - let txs = root.many("Ntry").try_fold(Vec::new(), |mut txs, entry| { - // Skip if not booked - if !{ - let status = entry.one("Sts")?; - let status = status - .opt("Cd")? - .map(|n| n.text()) - .unwrap_or_else(|| status.text()); - status == "BOOK" - } { - return Ok(txs); - } - - let reversal = entry.opt("RvslInd").parse()? == Some(true); - let entry_code = bank_tx_code(entry.one("BkTxCd")?)?; - let entry_kind = entry.opt("CdtDbtInd").parse::<Kind>()?; - let entry_ref = entry.opt("AcctSvcrRef").parse::<CompactString>()?; - let date = execution_date(entry)?; - let entry_charges = charges(entry)?; - let entry_amount = complex_amount(entry.one("Amt")?, &entry_charges)?; - - let Some(details) = entry.opt("NtryDtls")? else { - return Ok(txs); - }; - // When an entry only contain a single transactions information will sometimes only be stored at the entry level - let unique = details.many("TxDtls").count() == 1; - for tx in details.many("TxDtls") { - // Check information are present and coherent - let kind = tx.opt("CdtDbtInd").parse()?.or(entry_kind).unwrap(); - - // Sometimes the transaction level have a more precise bank transaction code - let code = tx - .opt("BkTxCd")? - .map(bank_tx_code) - .transpose()? - .unwrap_or(entry_code); - - let tx_charges = charges(tx)?; - // Amount - let amount = if unique { - // When unique the charges can be only at the entry level - if let Some(amt) = tx.opt("Amt")? { - let tx_amount = complex_amount( - amt, - if tx_charges.is_empty() { - &entry_charges - } else { - &tx_charges - }, - )?; - // Check coherence - entry_amount.resolve(&tx_amount)? - } else { - entry_amount - } - } else { - // When many inner transaction the entry level is an aggregate of them - // We only use the transaction level information - complex_amount(tx.one("Amt")?, &tx_charges)? - }; - - // We can only use the entry ref as the transaction ref if there is a single transaction in the batch - let sref: Option<CompactString> = tx - .opt("Refs") - .opt("AcctSvcrRef") - .parse::<CompactString>()? - .or_else(|| unique.then(|| entry_ref.clone()).flatten()); - - match (kind, code.is_reversal() || reversal) { - (Kind::CRDT, true) => { - let out_id = outgoing_id(tx, sref.as_deref())?; - if let OutIds::Tx(OutId { - msg_id, - e2e_id: Some(e2e_id), - .. - }) = out_id - { - txs.push(Tx::Reversal(OutReversal { - e2e_id, - msg_id, - reason: return_reason(tx)?, - execution_time: date, - })) - } else { - warn!("missing unique ID for Credit reversal {out_id:?}"); - } - } - (Kind::DBIT, true) | (Kind::CRDT, false) => { - let id = incoming_id(tx, sref.as_deref())?; - if id.uetr.is_none() && id.tx_id.is_none() && id.sref.is_none() { - warn!("missing unique ID for Credit") - } else { - txs.push(Tx::In(InTx { - id, - amount: amount.amount, - credit_fee: amount.fee, - subject: wire_transfer_subject(tx)?, - execution_time: date, - debtor: payto(tx, "Dbtr")?, - })); - } - } - (Kind::DBIT, false) => { - let id = outgoing_id(tx, sref.as_deref())?; - match id { - OutIds::Tx(id) => { - if id.e2e_id.is_none() && id.msg_id.is_none() && id.sref.is_none() { - warn!("missing unique ID for Debit") - } else { - txs.push(Tx::Out(OutTx { - id, - amount: amount.amount, - debit_fee: amount.fee, - subject: wire_transfer_subject(tx)?, - execution_time: date, - creditor: payto(tx, "Cdtr")?, - })); - } - } - OutIds::Batch(BatchId { msg_id, .. }) => { - txs.push(Tx::Batch(OutBatch { - msg_id, - execution_time: date, - })); - } - } - } - } - } - Ok(txs) - })?; - Ok(AccountTransactions { id, currency, txs }) - } - - Xml::parse(xml, "Document", |root| { - if let Some(camt053) = root.opt("BkToCstmrStmt")? { - camt053.many("Stmt").map(parse_inner).collect() - } else if let Some(camt052) = root.opt("BkToCstmrAcctRpt")? { - camt052.many("Rpt").map(parse_inner).collect() - } else if let Some(camt054) = root.opt("BkToCstmrDbtCdtNtfctn")? { - camt054.many("Ntfctn").map(parse_inner).collect() - } else { - Err(root.parse_err("Malformed camt file")) - } - }) -} - -#[cfg(test)] -pub mod test { - use std::str::FromStr; - - use jiff::{Timestamp, civil::Date}; - use taler_common::types::{ - amount::{Amount, Currency}, - iban::IBAN, - payto::{BankID, IbanPayto, PaytoImpl as _, PaytoURI}, - utils::date_to_utc_ts, - }; - - use crate::iso20022::{ - camt::{AccountId, parse_camt}, - model::{InId, InTx, OutBatch, OutId, OutReversal, OutTx, Tx}, - }; - - pub fn date_to_timestamp(date: &str) -> Timestamp { - date_to_utc_ts(&Date::from_str(date).unwrap()) - } - - fn iban_payto(iban: impl AsRef<str>, name: impl AsRef<str>) -> PaytoURI { - IbanPayto::new(BankID { - iban: iban.as_ref().parse().expect("invalid IBAN"), - bic: None, - }) - .as_full_uri(name.as_ref()) - } - - #[track_caller] - pub fn check_tx(path: &str, iban: &str, currency: Option<&str>, txs: &[Tx]) { - let content = std::fs::read(path).unwrap(); - let res = parse_camt(&content).unwrap(); - assert_eq!(res.len(), 1); - - let first = &res[0]; - assert_eq!(first.id, AccountId::Iban(IBAN::from_str(iban).unwrap())); - assert_eq!( - first.currency, - currency.map(|it| Currency::from_str(it).unwrap()) - ); - pretty_assertions::assert_eq!(first.txs, txs); - } - - pub fn tx_out( - id: (Option<&str>, Option<&str>, Option<&str>), - amount: &str, - debit_fee: &str, - subject: Option<&str>, - execution_time: &str, - creditor: Option<(&str, &str)>, - ) -> Tx { - Tx::Out(OutTx { - id: OutId::new( - id.0.map(Into::into), - id.1.map(Into::into), - id.2.map(Into::into), - ), - amount: Amount::from_str(amount).unwrap(), - debit_fee: Amount::from_str(debit_fee).unwrap(), - subject: subject.map(Into::into), - execution_time: date_to_timestamp(execution_time), - creditor: creditor.map(|(iban, name)| iban_payto(iban, name)), - }) - } - - pub fn tx_in( - id: (Option<&str>, Option<&str>, Option<&str>), - amount: &str, - credit_fee: &str, - subject: Option<&str>, - execution_time: &str, - debtor: Option<(&str, &str)>, - ) -> Tx { - Tx::In(InTx { - id: InId::new( - id.0.map(|it| it.parse().unwrap()), - id.1.map(Into::into), - id.2.map(Into::into), - ), - amount: Amount::from_str(amount).unwrap(), - credit_fee: Amount::from_str(credit_fee).unwrap(), - subject: subject.map(Into::into), - execution_time: date_to_timestamp(execution_time), - debtor: debtor.map(|(iban, name)| iban_payto(iban, name)), - }) - } - - pub fn tx_reversal( - e2e_id: &str, - msg_id: Option<&str>, - reason: &str, - execution_time: &str, - ) -> Tx { - Tx::Reversal(OutReversal { - e2e_id: e2e_id.parse().unwrap(), - msg_id: msg_id.map(Into::into), - reason: reason.into(), - execution_time: date_to_timestamp(execution_time), - }) - } - - pub fn tx_batch(msg_id: &str, execution_time: &str) -> Tx { - Tx::Batch(OutBatch { - msg_id: msg_id.into(), - execution_time: date_to_timestamp(execution_time), - }) - } - - #[test] - fn postfinance_camt054() { - check_tx( - "../../libeufin-nexus/sample/platform/postfinance_camt054.xml", - "CH9289144596463965762", - Some("CHF"), - &[ - tx_out( - ( - Some("ZS1PGNTSV0ZNDFAJBBWWB8015G"), - Some("ZS1PGNTSV0ZNDFAJBBWWB8015G"), - None, - ), - "CHF:3.00", - "CHF:0", - None, - "2024-01-15", - None, - ), - tx_in( - ( - Some("62e2b511-7313-4ccd-8d40-c9d8e612cd71"), - None, - Some("231121CH0AZWCR9T"), - ), - "CHF:10", - "CHF:0", - Some("G1XTY6HGWGMVRM7E6XQ4JHJK561ETFDFTJZ7JVGV543XZCB27YBG"), - "2023-12-19", - Some(("CH7389144832588726658", "Mr Test")), - ), - tx_in( - ( - Some("62e2b511-7313-4ccd-8d40-c9d8e612cd71"), - None, - Some("231121CH0AZWCVR1"), - ), - "CHF:2.53", - "CHF:0", - Some("G1XTY6HGWGMVRM7E6XQ4JHJK561ETFDFTJZ7JVGV543XZCB27YB"), - "2023-12-19", - Some(("CH7389144832588726658", "Mr Test")), - ), - tx_reversal( - "50820f78-9024-44ff-978d-63a18c", - Some("50820f78-9024-44ff-978d-63a18c"), - "", - "2024-01-15", - ), - tx_batch("ZS1PGNTSV0ZNDFAJBBWWB8015G", "2024-01-15"), - ], - ); - } - - #[test] - fn postfinance_camt053() { - check_tx( - "../../libeufin-nexus/sample/platform/postfinance_camt053.xml", - "CH9289144596463965762", - Some("CHF"), - &[ - tx_reversal( - "889d1a80-1267-49bd-8fcc-85701a", - Some("889d1a80-1267-49bd-8fcc-85701a"), - "InconsistenWithEndCustomer 'Identification of end customer is not consistent with associated account number, organisation ID or private ID' - 'more info here ...'", - "2023-11-22", - ), - tx_reversal( - "4cc61cc7-6230-49c2-b5e2-b40bbb", - Some("4cc61cc7-6230-49c2-b5e2-b40bbb"), - "MissingCreditorNameOrAddress 'Specification of the creditor’s name and/or address needed for regulatory requirements is insufficient or missing' - 'more info here ...'", - "2023-11-22", - ), - tx_batch("EB4D22D428214261B2B3012D2A8CEC36", "2024-08-26"), - ], - ); - } - - #[test] - fn raiffeisen_camt053() { - check_tx( - "../../libeufin-nexus/sample/platform/raiffeisen_camt053.xml", - "CH7389144832588726658", - None, - &[ - tx_in( - (None, None, Some("A200020494367552")), - "CHF:20000", - "CHF:0", - Some("1. TZ 2025"), - "2025-12-23", - Some(("CH7389144832588726658", "KANTON BERN")), - ), - tx_out( - (None, None, Some("19868398389")), - "CHF:15", - "CHF:0", - None, - "2025-12-31", - None, - ), - tx_out( - (None, None, Some("19890406743")), - "CHF:2", - "CHF:0", - None, - "2025-12-31", - None, - ), - tx_out( - (None, None, Some("19885172770")), - "CHF:3", - "CHF:0", - None, - "2025-12-31", - None, - ), - ], - ); - } - - #[test] - fn valiant_camt052() { - check_tx( - "../../libeufin-nexus/sample/platform/valiant_camt052.xml", - "CH7389144832588726658", - Some("CHF"), - &[ - tx_out( - ( - Some("MJDJO2BDDBL7YSL2P96SXHG3TQZEZQD26L"), - Some("4UWWIDGTEIGDU6Z721QE95PYJSIEA48PYE"), - Some("ZV20251030/511372/1"), - ), - "CHF:0.1", - "CHF:0", - Some("single 2025-10-30T09:46:04.55293090 9Z"), - "2025-10-30", - Some(("CH7389144832588726658", "Grothoff Hans")), - ), - tx_out( - ( - Some("5HIS3433VVIBAANHW3GX9DR1AXRS43KZ4U"), - Some("SKMU2891PAAYBDW22DBWX2W7KTFZ1CDFO8"), - Some("ZV20251030/511373/1"), - ), - "CHF:0.1", - "CHF:0", - Some("multi 0 2025-10-30T09:46:10.3877961 30Z"), - "2025-10-30", - Some(("CH7389144832588726658", "Grothoff Hans")), - ), - tx_out( - ( - Some("5HIS3433VVIBAANHW3GX9DR1AXRS43KZ4U"), - Some("RC9YD301NZ17YKD6WDWLNOROFHIIN29VJN"), - Some("ZV20251030/511373/2"), - ), - "CHF:0.11", - "CHF:0", - Some("multi 1 2025-10-30T09:46:10.3877961 30Z"), - "2025-10-30", - Some(("CH7389144832588726658", "Grothoff Hans")), - ), - tx_out( - ( - Some("5HIS3433VVIBAANHW3GX9DR1AXRS43KZ4U"), - Some("GKDGTHLB82X6XVHBJIJ1CK8MEGU9XJ2EL7"), - Some("ZV20251030/511373/3"), - ), - "CHF:0.12", - "CHF:0", - Some("multi 2 2025-10-30T09:46:10.3877961 30Z"), - "2025-10-30", - Some(("CH7389144832588726658", "Grothoff Hans")), - ), - tx_out( - ( - Some("5HIS3433VVIBAANHW3GX9DR1AXRS43KZ4U"), - Some("PXCH2VVVTXEXBVDWICP23HZ4NV0H2CWW28"), - Some("ZV20251030/511373/4"), - ), - "CHF:0.13", - "CHF:0", - Some("multi 3 2025-10-30T09:46:10.3877961 30Z"), - "2025-10-30", - Some(("CH7389144832588726658", "Grothoff Hans")), - ), - tx_in( - (None, Some("51030655601.0001"), Some("ZV20251030/514778/1")), - "CHF:0.85", - "CHF:0", - Some("fun stuff"), - "2025-10-30", - Some(("CH7389144832588726658", "Grothoff Hans")), - ), - tx_in( - (None, Some("51030655601.0002"), Some("ZV20251030/514779/1")), - "CHF:0.95", - "CHF:0", - Some("Taler PC2MKG0B7CK32K1T7DP08P6E1B7FHB6HY6R Q0PT3VTPBPRPYM1B0"), - "2025-10-30", - Some(("CH7389144832588726658", "Grothoff Hans")), - ), - tx_out( - ( - Some("X166701F6RV59LP71RVWVIW9SV2AFZYLG4"), - Some("R48UBIIB7B4LX0DMVOSI0ZTJWMMG8FMNKX"), - Some("ZV20251030/524078/1"), - ), - "CHF:0.21", - "CHF:0", - Some("bad name 2025-10-30T12:03:24.997478 811Z"), - "2025-10-30", - Some(("CH6208704048981247126", "John Smith")), - ), - tx_out( - ( - Some("6OZN5T9W7MK6BIZYE01E62NHGP5JLMUD4X"), - Some("02WDIX4J90Z1M1WNFHLNSXY59SHXQTQCMQ"), - Some("ZV20251030/524079/1"), - ), - "CHF:0.1", - "CHF:0", - Some("single 2025-10-30T12:04:00.37042083 6Z"), - "2025-10-30", - Some(("CH7389144832588726658", "Grothoff Hans")), - ), - tx_out( - ( - Some("6OZN5T9W7MK6BIZYE01E62NHGP5JLMUD4X"), - Some("XAP5L7HVWPLCEMECU4GZK6GKUPBL0TD13Y"), - Some("ZV20251030/524079/2"), - ), - "CHF:0.21", - "CHF:0", - Some("bad name 2025-10-30T12:03:53.042190 686Z"), - "2025-10-30", - Some(("CH6208704048981247126", "John Smith")), - ), - tx_reversal( - "XAP5L7HVWPLCEMECU4GZK6GKUPBL0TD13Y", - None, - "Error msg in german", - "2025-10-30", - ), - tx_reversal( - "R48UBIIB7B4LX0DMVOSI0ZTJWMMG8FMNKX", - None, - "Error msg in german", - "2025-10-30", - ), - tx_out( - ( - Some("OLAMDPI6YPMNRZHQ5PQ6JCVUQV2AN5NW6P"), - Some("TU2WJ54DR9Z6HT5VE494BNH4EXUSM0DRF7"), - Some("ZV20251030/524077/1"), - ), - "CHF:0.23", - "CHF:5", - Some("foreign iban 2025-10-30T12:03:44.0972 63765Z"), - "2025-10-30", - Some(("DE48330605920000686018", "Christian Grothoff")), - ), - tx_out( - ( - Some("6OZN5T9W7MK6BIZYE01E62NHGP5JLMUD4X"), - Some("GM8I8GIETR72LP6CFBGRBUDKNO2CEQBGOE"), - Some("ZV20251030/524080/1"), - ), - "CHF:0.23", - "CHF:5", - Some("foreign iban 2025-10-30T12:03:58.0046 73747Z"), - "2025-10-30", - Some(("DE48330605920000686018", "Christian Grothoff")), - ), - tx_in( - ( - Some("7b76d488-05d5-44ab-9d77-31d4165ec158"), - Some("00204EQY370"), - Some("ZV20251118/685062/1"), - ), - "CHF:4.55", - "CHF:0", - Some("TEST"), - "2025-11-18", - None, - ), - ], - ) - } - - #[test] - fn gls_camt052() { - check_tx( - "../../libeufin-nexus/sample/platform/gls_camt052.xml", - "DE84500105177118117964", - Some("EUR"), - &[ - tx_out( - ( - Some("COMPAT_SUCCESS"), - Some("COMPAT_SUCCESS"), - Some("2024041801514102000"), - ), - "EUR:2", - "EUR:0", - Some("TestABC123"), - "2024-04-18", - Some(("DE20500105172419259181", "John Smith")), - ), - tx_reversal( - "8XK8Z7RAX224FGWK832FD40GYC", - None, - "IncorrectAccountNumber 'Format of the account number specified is not correct' - 'IBAN fehlerhaft und ungültig'", - "2024-09-05", - ), - tx_in( - ( - None, - Some("BYLADEM1WOR-G2910276709458A2"), - Some("2024041210041357000"), - ), - "EUR:3", - "EUR:0", - Some("Taler FJDQ7W6G7NWX4H9M1MKA12090FRC9K7DA6N0FANDZZFXTR6QHX5G Test.,-"), - "2024-04-12", - Some(("DE84500105177118117964", "John Smith")), - ), - tx_reversal( - "COMPAT_FAILURE", - None, - "IncorrectAccountNumber 'Format of the account number specified is not correct' - 'IBAN ...'", - "2024-04-12", - ), - tx_out( - ( - Some("BATCH_SINGLE_SUCCESS"), - Some("FD622SMXKT5QWSAHDY0H8NYG3G"), - Some("2024090216552232000"), - ), - "EUR:1.1", - "EUR:0", - Some("single 2024-09-02T14:29:52.875253314Z"), - "2024-09-02", - Some(("DE89500105173198527518", "Grothoff Hans")), - ), - tx_out( - ( - Some("YF5QBARGQ0MNY0VK59S477VDG4"), - Some("YF5QBARGQ0MNY0VK59S477VDG4"), - Some("2024041810552821000"), - ), - "EUR:1.1", - "EUR:0", - Some("Simple tx"), - "2024-04-18", - Some(("DE20500105172419259181", "John Smith")), - ), - tx_batch("BATCH_MANY_SUCCESS", "2024-09-20"), - tx_out( - ( - Some("BATCH_SINGLE_RETURN"), - Some("KLJJ28S1LVNDK1R2HCHLN884M7EKM5XGM5"), - Some("2024092100252498000"), - ), - "EUR:0.42", - "EUR:0", - Some("This should fail because bad iban"), - "2024-09-23", - Some(("DE18500105173385245163", "John Smith")), - ), - tx_reversal( - "KLJJ28S1LVNDK1R2HCHLN884M7EKM5XGM5", - None, - "IncorrectAccountNumber 'Format of the account number specified is not correct' - 'IBAN fehlerhaft und ungültig'", - "2024-09-24", - ), - ], - ) - } - - #[test] - fn gls_camt053() { - check_tx( - "../../libeufin-nexus/sample/platform/gls_camt053.xml", - "DE84500105177118117964", - Some("EUR"), - &[ - tx_out( - ( - Some("COMPAT_SUCCESS"), - Some("COMPAT_SUCCESS"), - Some("2024041801514102000"), - ), - "EUR:2", - "EUR:0", - Some("TestABC123"), - "2024-04-18", - Some(("DE20500105172419259181", "John Smith")), - ), - tx_reversal( - "KGTDBASWTJ6JM89WXD3Q5KFQC4", - None, - "Retoure aus SEPA Überweisung multi line", - "2024-09-04", - ), - tx_batch("BATCH_MANY_PART", "2024-09-04"), - tx_in( - ( - None, - Some("BYLADEM1WOR-G2910276709458A2"), - Some("2024041210041357000"), - ), - "EUR:3", - "EUR:0", - Some("Taler FJDQ7W6G7NWX4H9M1MKA12090FRC9K7DA6N0FANDZZFXTR6QHX5G Test.,-"), - "2024-04-12", - Some(("DE84500105177118117964", "John Smith")), - ), - tx_reversal( - "COMPAT_FAILURE", - None, - "IncorrectAccountNumber 'Format of the account number specified is not correct' - 'IBAN ...'", - "2024-04-12", - ), - tx_out( - ( - Some("BATCH_SINGLE_SUCCESS"), - Some("FD622SMXKT5QWSAHDY0H8NYG3G"), - Some("2024090216552232000"), - ), - "EUR:1.1", - "EUR:0", - Some("single 2024-09-02T14:29:52.875253314Z"), - "2024-09-02", - Some(("DE89500105173198527518", "Grothoff Hans")), - ), - tx_out( - ( - Some("YF5QBARGQ0MNY0VK59S477VDG4"), - Some("YF5QBARGQ0MNY0VK59S477VDG4"), - Some("2024041810552821000"), - ), - "EUR:1.1", - "EUR:0", - Some("Simple tx"), - "2024-04-18", - Some(("DE20500105172419259181", "John Smith")), - ), - ], - ) - } - - #[test] - fn gls_camt054() { - check_tx( - "../../libeufin-nexus/sample/platform/gls_camt054.xml", - "DE84500105177118117964", - Some("EUR"), - &[tx_in( - (None, Some("IS11PGENODEFF2DA8899900378806"), None), - "EUR:2.5", - "EUR:0", - Some("Test ICT"), - "2024-05-05", - Some(("DE84500105177118117964", "Mr Test")), - )], - ); - } - - #[test] - fn maerki_baumann_camt053() { - check_tx( - "../../libeufin-nexus/sample/platform/maerki_baumann_camt053.xml", - "CH7389144832588726658", - Some("CHF"), - &[ - tx_in( - ( - Some("adbe4a5a-6cea-4263-b259-8ab964561a32"), - Some("41103099704.0002"), - Some("ZV20241104/765446/1"), - ), - "CHF:1", - "CHF:0.2", - Some("SFHP6H24C16A5J05Q3FJW2XN1PB3EK70ZPY 5SJ30ADGY68FWN68G"), - "2024-11-04", - Some(("CH7389144832588726658", "Mr Test")), - ), - tx_in( - ( - Some("7371795e-62fa-42dd-93b7-da89cc120faa"), - Some("41103099704.0003"), - Some("ZV20241104/765447/1"), - ), - "CHF:1", - "CHF:0.2", - Some("Random subject"), - "2024-11-04", - Some(("CH7389144832588726658", "Mr Test")), - ), - tx_in( - (None, Some("50523424675.0001"), Some("ZV20250523/851716/1")), - "CHF:0.5", - "CHF:0.2", - None, - "2025-05-23", - Some(("CH7389144832588726658", "Grothoff Hans")), - ), - tx_out( - ( - Some("BATCH_SINGLE_REPORTING"), - Some("5IBJZOWESQGPCSOXSNNBBY49ZURI5W7Q4H"), - Some("ZV20241121/773541/1"), - ), - "CHF:0.1", - "CHF:0", - Some("multi 0 2024-11-21T15:21:59.8859234 63Z"), - "2024-11-27", - Some(("CH7389144832588726658", "Grothoff Hans")), - ), - tx_out( - ( - Some("BATCH_SINGLE_REPORTING"), - Some("XZ15UR0XU52QWI7Q4XB88EDS44PLH7DYXH"), - Some("ZV20241121/773541/4"), - ), - "CHF:0.13", - "CHF:0", - Some("multi 3 2024-11-21T15:21:59.8859234 63Z"), - "2024-11-27", - Some(("CH7389144832588726658", "Grothoff Hans")), - ), - tx_out( - ( - Some("BATCH_SINGLE_REPORTING"), - Some("A09R35EW0359SZ51464E7TC37A0P2CBK04"), - Some("ZV20241121/773541/3"), - ), - "CHF:0.12", - "CHF:0", - Some("multi 2 2024-11-21T15:21:59.8859234 63Z"), - "2024-11-27", - Some(("CH7389144832588726658", "Grothoff Hans")), - ), - tx_out( - ( - Some("BATCH_SINGLE_REPORTING"), - Some("UYXZ78LE9KAIMBY6UNXFYT1K8KNY8VLZLT"), - Some("ZV20241121/773541/2"), - ), - "CHF:0.11", - "CHF:0", - Some("multi 1 2024-11-21T15:21:59.8859234 63Z"), - "2024-11-27", - Some(("CH7389144832588726658", "Grothoff Hans")), - ), - tx_in( - ( - Some("f203fbb4-6e13-4c78-9b2a-d852fea6374a"), - Some("41202060702.0001"), - Some("ZV20241202/778108/1"), - ), - "CHF:0.05", - "CHF:0.2", - Some("mini"), - "2024-12-02", - Some(("CH7389144832588726658", "Grothoff Hans")), - ), - tx_in( - ( - Some("81b0d8c6-a677-4577-b75e-a639dcc03681"), - Some("41120636093.0001"), - Some("ZV20241121/773118/1"), - ), - "CHF:0.1", - "CHF:0.2", - Some("small transfer test"), - "2024-11-21", - Some(("CH7389144832588726658", "Grothoff Hans")), - ), - tx_out( - (None, None, Some("GB20241220/205792/1")), - "CHF:3000", - "CHF:0", - None, - "2024-12-20", - None, - ), - tx_in( - (None, None, Some("ZV20250114/796191/1")), - "CHF:3003", - "CHF:0", - Some("Fix bad payment by MB."), - "2025-01-27", - None, - ), - tx_in( - (None, Some("F000787951230001"), Some("ZV20250526/852733/1")), - "CHF:1.38", - "CHF:0.2", - Some("Taler XT3D9MADR4V85JBWX47SMJFDQD2FDZDHHPH8R25YDG1KNVTSEH6G"), - "2025-05-26", - Some(("DE20500105172419259181", "Mr German")), - ), - ], - ) - } -} diff --git a/crates/libeufin-ebics/src/iso20022/hac.rs b/crates/libeufin-ebics/src/iso20022/hac.rs @@ -1,198 +0,0 @@ -/* -* This file is part of LibEuFin. -* Copyright (C) 2026 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/> -*/ - -use std::fmt::Display; - -use compact_str::CompactString; -use jiff::Timestamp; -use taler_common::types::utils::date_time_to_utc_ts; - -use crate::{ - iso20022::{HacAction, status_code::StatusReason}, - xml::{self, Xml, XmlAccess}, -}; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct CustomerAck { - pub action: HacAction, - pub order_id: Option<CompactString>, - pub code: Option<StatusReason>, - pub info: Box<str>, - pub timestamp: Timestamp, -} - -impl CustomerAck { - fn msg_fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let Self { - action, code, info, .. - } = self; - write!(f, "{action}")?; - if let Some(code) = code { - write!(f, "{}", code.code())?; - } - write!(f, " - '{}'", action.description())?; - if let Some(code) = code { - write!(f, " '{}'", code.description())?; - } - if !info.is_empty() { - write!(f, " - '{info}'")?; - } - Ok(()) - } -} - -impl Display for CustomerAck { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let Self { - order_id, - timestamp, - .. - } = self; - write!(f, "{timestamp}")?; - if let Some(id) = order_id { - write!(f, "{id}")?; - } - write!(f, " {}", std::fmt::from_fn(|f| self.msg_fmt(f))) - } -} -/** Parse HAC pain.002 XML file */ -pub fn parse_hac(xml: &[u8]) -> xml::Result<Vec<CustomerAck>> { - Xml::parse(xml, "Document", |root| { - root.one("CstmrPmtStsRpt")? - .many("OrgnlPmtInfAndSts") - .map(|n| { - let mut timestamp = None; - let mut order_id = None; - let info = n.one("StsRsnInf")?; - for entry in info.one("Orgtr").one("Id").one("OrgId")?.many("Othr") { - let value = entry.one("Id"); - let key = entry.one("SchmeNm").one("Prtry")?.text(); - match key { - "TimeStamp" => { - timestamp = Some(date_time_to_utc_ts( - &value.decode(|dt| dt.trim_end_matches('Z').parse())?, - )) - } - "OrderID" => order_id = Some(value.parse()?), - _ => {} - } - } - Ok(CustomerAck { - action: n.one("OrgnlPmtInfId").parse()?, - order_id, - code: info.opt("Rsn").one("Cd").parse()?, - info: info.many("AddtlInf").map(|n| n.text()).collect(), - timestamp: timestamp.unwrap(), - }) - }) - .collect() - }) -} - -#[cfg(test)] -mod test { - use taler_common::types::utils::date_time_to_utc_ts; - - use crate::iso20022::{ - HacAction, - hac::{CustomerAck, parse_hac}, - status_code::StatusReason, - }; - - #[test] - fn hac() { - pub fn ack( - action: HacAction, - order_id: Option<&str>, - code: Option<StatusReason>, - info: &str, - timestamp: &str, - ) -> CustomerAck { - CustomerAck { - action, - order_id: order_id.map(Into::into), - code, - info: info.into(), - timestamp: date_time_to_utc_ts(&timestamp.trim_end_matches('Z').parse().unwrap()), - } - } - pretty_assertions::assert_eq!( - parse_hac(&std::fs::read("../../libeufin-nexus/sample/platform/hac.xml").unwrap()) - .unwrap(), - [ - ack( - HacAction::FILE_DOWNLOAD, - None, - Some(StatusReason::TransmissionSuccessful), - "", - "2024-09-02T15:47:30.350Z" - ), - ack( - HacAction::FILE_UPLOAD, - Some("ORDER_SUCCESS"), - Some(StatusReason::TransmissionSuccessful), - "", - "2024-09-02T20:48:43.153Z" - ), - ack( - HacAction::ES_VERIFICATION, - Some("ORDER_SUCCESS"), - Some(StatusReason::ElectronicSignaturesCorrect), - "", - "2024-09-02T20:48:43.153Z" - ), - ack( - HacAction::ORDER_HAC_FINAL_POS, - Some("ORDER_SUCCESS"), - None, - "Some multiline info", - "2024-09-02T20:48:43.153Z" - ), - ack( - HacAction::FILE_DOWNLOAD, - None, - Some(StatusReason::NoDataAvailable), - "", - "2024-09-02T15:47:31.754Z" - ), - ack( - HacAction::FILE_UPLOAD, - Some("ORDER_FAILURE"), - Some(StatusReason::TransmissionSuccessful), - "", - "2024-08-23T15:34:11.987Z" - ), - ack( - HacAction::ES_VERIFICATION, - Some("ORDER_FAILURE"), - Some(StatusReason::IncorrectFileStructure), - "", - "2024-08-23T15:34:13.307Z" - ), - ack( - HacAction::ORDER_HAC_FINAL_NEG, - Some("ORDER_FAILURE"), - None, - "", - "2024-08-23T15:34:13.307Z" - ), - ] - ) - } -} diff --git a/crates/libeufin-ebics/src/iso20022/pain001.rs b/crates/libeufin-ebics/src/iso20022/pain001.rs @@ -1,246 +0,0 @@ -/* -* This file is part of LibEuFin. -* Copyright (C) 2026 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/> -*/ - -use jiff::{Timestamp, Zoned, tz::TimeZone}; -use taler_common::types::{ - amount::{Amount, Decimal}, - payto::FullIbanPayto, -}; - -use crate::{ - dialect::{Dialect, Standard}, - ebics::EbicsErrKind, - xml, - xml::XmlWriter, -}; - -/** pain.001 transaction metadata */ -pub struct Pain001Tx<'a> { - pub creditor: &'a FullIbanPayto, - pub amount: Amount, - pub subject: &'a str, - pub e2e_id: &'a str, -} - -/** pain.001 message metadata */ -pub struct Pain001Msg<'a> { - pub msg_id: &'a str, - pub timestamp: &'a Timestamp, - pub debtor: &'a FullIbanPayto, - pub sum: Amount, - pub txs: Vec<Pain001Tx<'a>>, -} - -/** Check EBICS compability of an amount */ -fn ebics_amount(amount: &Amount) -> Result<Decimal, EbicsErrKind> { - if amount.is_sub_cent() { - return Err(EbicsErrKind::Custom( - "Sub-cent amounts not supported".into(), - )); - } - Ok(amount.decimal()) -} - -/** Create a pain.001 XML document [msg] valid for [dialect] */ -pub fn create_pain001( - msg: &Pain001Msg, - dialect: &Dialect, - instant: bool, -) -> Result<String, EbicsErrKind> { - let version = "09"; - let suffix = match dialect.standard() { - Standard::SIX => ".ch.03", - Standard::GBIC => "", - }; - let total = ebics_amount(&msg.sum)?; - Ok(xml!( - "Document" - "xmlns"=(format_args!("urn:iso:std:iso:20022:tech:xsd:pain.001.001.{version}")) - "xmlns:xsi"=(format_args!("http://www.w3.org/2001/XMLSchema-instance")) - "xsi:schemaLocation"=(format_args!("urn:iso:std:iso:20022:tech:xsd:pain.001.001.{version} pain.001.001.{version}{suffix}.xsd")) - { - "CstmrCdtTrfInitn" { - "GrpHdr" { - // Used for idempotency as banks will refuse to process EBICS request with the same MsgId for a pre-agreed period - // Used to uniquely identify batches of transactions in other files - "MsgId": msg.msg_id, - "CreDtTm": msg.timestamp, - "NbOfTxs": msg.txs.len(), - "CtrlSum": total, - "InitgPty" { - "Nm": msg.debtor.name - }/* - // TODO fail with GLS: ES_VERIFICATION IncorrectFileStructure - 'Signature verification' 'The file format is incomplete or invalid' - "InitnSrc" { - "Nm": "LibEuFin", - "Prvdr": "Taler Systems SA", - "Vrsn": taler_build::long_version() - }*/ - }, - "PmtInf" { - "PmtInfId": "NOTPROVIDED", - "PmtMtd": "TRF", - "BtchBookg": "false", - "NbOfTxs": msg.txs.len(), - "CtrlSum": total, - @ |w: &mut XmlWriter| if dialect.standard() == Standard::GBIC { - xml!(w => "PmtTpInf" { - "SvcLvl" { - "Cd": "SEPA" - }, - @ |w: &mut XmlWriter| if instant { - xml!(w => "LclInstrm" { - "Cd": "INST" - }) - } - }) - }, - "ReqdExctnDt" { - "Dt": Zoned::new(*msg.timestamp, TimeZone::UTC).date().to_string() + "Z" - }, - "Dbtr" { - "Nm": msg.debtor.name - }, - "DbtrAcct" { - "Id" { - "IBAN": msg.debtor.iban - } - }, - "DbtrAgt" { - "FinInstnId" { - @ |w: &mut XmlWriter| if let Some(bic) = &msg.debtor.bic { - xml!(w => "BICFI": bic) - } else { - xml!(w => "Othr" { - "Id": "NOTPROVIDED" - }) - } - } - - }, - "ChrgBr": "SLEV", - @ |w: &mut XmlWriter| for tx in &msg.txs { - xml!(w => "CdtTrfTxInf" { - "PmtId" { - "InstrId": tx.e2e_id, - // Used to uniquely identify transactions in other files - "EndToEndId": tx.e2e_id - }, - "Amt" { - "InstdAmt" "Ccy"=(tx.amount.currency) : ebics_amount(&tx.amount).unwrap() - }, - @ |w: &mut XmlWriter| if let Some(bic) = &tx.creditor.bic { - xml!(w => "CdtrAgt" { - "FinInstnId" { - "BICFI": bic - } - }) - }, - "Cdtr" { - "Nm": tx.creditor.name - // Addr might become a requirement in the future - /*"PstlAdr" { - "TwnNm": "Bochum", - "Ctry": "DE" - }*/ - }, - "CdtrAcct" { - "Id" { - "IBAN": tx.creditor.iban - } - }, - "RmtInf" { - "Ustrd": tx.subject - } - }) - } - } - } - } - )) -} - -#[cfg(test)] -mod test { - use taler_common::types::{ - amount::amount, - payto::{BankID, FullIbanPayto}, - }; - - use crate::{ - dialect::Dialect, - iso20022::{ - camt::test::date_to_timestamp, - pain001::{Pain001Msg, Pain001Tx, create_pain001}, - }, - }; - - #[test] - fn pain001() { - let creditor = &FullIbanPayto::new( - BankID { - iban: "CH4189144589712575493".parse().expect("invalid IBAN"), - bic: None, - }, - "Test", - ); - - let msg = Pain001Msg { - msg_id: "MESSAGE_ID", - timestamp: &date_to_timestamp("2024-09-09"), - debtor: &FullIbanPayto::new( - BankID { - iban: "CH7789144474425692816".parse().expect("invalid IBAN"), - bic: Some("AAAABBCC123".parse().expect("invalid BIC")), - }, - "myname", - ), - sum: amount("CHF:47.32"), - txs: vec![ - Pain001Tx { - creditor, - amount: amount("CHF:42"), - subject: "Test 42", - e2e_id: "TX_FIRST", - }, - Pain001Tx { - creditor, - amount: amount("CHF:5.11"), - subject: "Test 5.11", - e2e_id: "TX_SECOND", - }, - Pain001Tx { - creditor, - amount: amount("CHF:0.21"), - subject: "Test 0.21", - e2e_id: "TX_THIRD", - }, - ], - }; - for dialect in Dialect::entries { - pretty_assertions::assert_eq!( - std::fs::read_to_string(format!( - "../../libeufin-nexus/sample/platform/{dialect}_pain001.xml" - )) - .unwrap(), - create_pain001(&msg, dialect, false).unwrap() - ); - } - } -} diff --git a/crates/libeufin-ebics/src/iso20022/pain002.rs b/crates/libeufin-ebics/src/iso20022/pain002.rs @@ -1,270 +0,0 @@ -/* -* This file is part of LibEuFin. -* Copyright (C) 2026 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/> -*/ - -use std::fmt::{Display, Formatter, Write, from_fn}; - -use compact_str::CompactString; - -use crate::{ - iso20022::status_code::{PaymentGroupStatus, PaymentTransactionStatus, StatusReason}, - xml::{self, Xml, XmlAccess}, -}; - -fn fmt_msg( - f: &mut Formatter<'_>, - code: Option<&str>, - description: Option<&str>, - reasons: &[Reason], -) -> std::fmt::Result { - if let Some(code) = code { - write!(f, "{code}")?; - if let Some(description) = description { - write!(f, " '{description}'")?; - } - if !reasons.is_empty() { - f.write_char(':')?; - } - } - for Reason { - code, - info: information, - } in reasons - { - if let Some(code) = code { - write!(f, " {} '{}'", code.code(), code.description())?; - } - if !information.is_empty() { - write!(f, " '{information}'")?; - } - } - Ok(()) -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Reason { - pub code: Option<StatusReason>, - pub info: Box<str>, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct TxStatus { - pub id: CompactString, - pub e2e_id: CompactString, - pub status: PaymentTransactionStatus, - pub reasons: Box<[Reason]>, -} - -impl TxStatus { - fn fmt_msg(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - fmt_msg( - f, - Some(self.status.code()), - Some(self.status.description()), - &self.reasons, - ) - } - - pub fn msg(&self) -> String { - format!("{}", from_fn(|f| self.fmt_msg(f))) - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct PmtStatus { - pub id: CompactString, - pub status: Option<PaymentGroupStatus>, - pub reasons: Box<[Reason]>, - pub txs: Box<[TxStatus]>, -} - -impl PmtStatus { - fn fmt_msg(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - fmt_msg( - f, - self.status.map(|it| it.code()), - self.status.map(|it| it.description()), - &self.reasons, - ) - } - pub fn msg(&self) -> String { - format!("{}", from_fn(|f| self.fmt_msg(f))) - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct MsgStatus { - pub id: CompactString, - pub status: Option<PaymentGroupStatus>, - pub reasons: Box<[Reason]>, - pub payments: Box<[PmtStatus]>, -} - -impl MsgStatus { - fn fmt_msg(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - fmt_msg( - f, - self.status.map(|it| it.code()), - self.status.map(|it| it.description()), - &self.reasons, - ) - } - pub fn msg(&self) -> String { - format!("{}", from_fn(|f| self.fmt_msg(f))) - } -} - -impl Display for MsgStatus { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{} {}", self.id, from_fn(|f| self.fmt_msg(f)))?; - for p in &self.payments { - write!(f, "\n>{} {}", p.id, from_fn(|f| p.fmt_msg(f)))?; - for tx in &p.txs { - if tx.id != tx.e2e_id { - write!(f, "{} ", tx.id)?; - } - write!(f, "\n>>{} {}", tx.e2e_id, from_fn(|f| tx.fmt_msg(f)))?; - } - } - Ok(()) - } -} - -/** Parse pain.002 XML file */ -pub fn parse_pain002(xml: &[u8]) -> xml::Result<MsgStatus> { - fn reasons(x: Xml) -> xml::Result<Box<[Reason]>> { - x.many("StsRsnInf") - .map(|n| { - let code = n.opt("Rsn").one("Cd").parse()?; - let info = n.many("AddtlInf").map(Xml::text).collect(); - Ok(Reason { code, info }) - }) - .collect() - } - - Xml::parse(xml, "Document", |root| { - let n = root.one("CstmrPmtStsRpt")?; - let status = n.one("OrgnlGrpInfAndSts")?; - Ok(MsgStatus { - id: status.one("OrgnlMsgId").parse()?, - status: status.opt("GrpSts").parse()?, - reasons: reasons(status)?, - payments: n - .many("OrgnlPmtInfAndSts") - .map(|n| { - Ok(PmtStatus { - id: n.one("OrgnlPmtInfId").parse()?, - status: n.opt("PmtInfSts").parse()?, - reasons: reasons(n)?, - txs: n - .many("TxInfAndSts") - .map(|n| { - Ok(TxStatus { - id: n.one("OrgnlInstrId").parse()?, - e2e_id: n.one("OrgnlEndToEndId").parse()?, - status: n.one("TxSts").parse()?, - reasons: reasons(n)?, - }) - }) - .collect::<xml::Result<_>>()?, - }) - }) - .collect::<xml::Result<_>>()?, - }) - }) -} - -#[cfg(test)] -mod test { - use crate::iso20022::{ - pain002::{MsgStatus, PmtStatus, Reason, TxStatus, parse_pain002}, - status_code::{PaymentGroupStatus, PaymentTransactionStatus, StatusReason}, - }; - - #[test] - fn pain002() { - pretty_assertions::assert_eq!( - parse_pain002(&std::fs::read("../../libeufin-nexus/sample/platform/pain002_part.xml").unwrap()).unwrap(), - MsgStatus { - id: "05BD4C5B4A2649B5B08F6EF6A31F197A".into(), - status: Some(PaymentGroupStatus::PartiallyAccepted), - reasons: Box::default(), - payments: Box::new([PmtStatus { - id: "NOTPROVIDED".into(), - status: Some(PaymentGroupStatus::PartiallyAccepted), - reasons: Box::new([ - Reason { - code: Some(StatusReason::ExecutionDateChanged), - info: "Due date is not a working day. Order will be executed on the next working day".into() - } - ]), - txs: Box::new([ - TxStatus { - id: "AQCXNCPWD8PHW5JTN65Y5XTF7R".into(), - e2e_id: "AQCXNCPWD8PHW5JTN65Y5XTF7R".into(), - status: PaymentTransactionStatus::Rejected, - reasons: Box::new([ - Reason { - code: Some(StatusReason::ClosedAccountNumber), - info: "Error message".into() - } - ]) - }, - TxStatus { - id: "EE9SX76FC5YSC657EK3GMVZ9TC".into(), - e2e_id: "EE9SX76FC5YSC657EK3GMVZ9TC".into(), - status: PaymentTransactionStatus::Rejected, - reasons: Box::new([ - Reason { - code: Some(StatusReason::NotSpecifiedReasonAgentGenerated), - info: "Error message".into() - } - ]) - }, - TxStatus { - id: "V5B3MXPEWES9VQW1JDRD6VAET4".into(), - e2e_id: "V5B3MXPEWES9VQW1JDRD6VAET4".into(), - status: PaymentTransactionStatus::Rejected, - reasons: Box::new([ - Reason { - code: Some(StatusReason::MissingDebtorNameOrAddress), - info: "Error message".into() - } - ]) - } - ]) - }]) - } - ); - pretty_assertions::assert_eq!( - parse_pain002( - &std::fs::read("../../libeufin-nexus/sample/platform/pain002_accp.xml").unwrap() - ) - .unwrap(), - MsgStatus { - id: "5HIS3433VVIBAANHW3GX9DR1AXRS43KZ4U".into(), - status: Some(PaymentGroupStatus::AcceptedCustomerProfile), - reasons: Box::new([Reason { - code: None, - info: "PN10630020F0297329.20251030104613.EBTUAAAC.PN1.0002372".into() - }]), - payments: Box::default() - } - ); - } -} diff --git a/crates/libeufin-nexus/src/db/initiated.rs b/crates/libeufin-nexus/src/db/initiated.rs @@ -1,915 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2026 Taler Systems SA - - TALER 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. - - TALER 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 - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> -*/ - -use std::collections::BTreeMap; - -use const_format::formatcp; -use jiff::Timestamp; -use libeufin_ebics::iso20022::model::{OutId, OutTx}; -use sqlx::{PgPool, Row as _, postgres::PgRow}; -use taler_api::{ - db::{BindHelper as _, TypeHelper as _}, - serialized, -}; -use taler_common::types::{ - amount::{Amount, Currency}, - payto::FullIbanPayto, -}; - -use crate::{ - db::{PENDING, UNSETTLED}, - model::{Initiated, PaymentBatch, SubmissionState}, -}; - -/// Outgoing payments initiation result -#[derive(Debug, PartialEq, Eq)] -pub enum PaymentInitiationResult { - Success(u64), - RequestUidReuse, -} - -/// Initiate a new payment -pub async fn initiate( - pool: &PgPool, - amount: &Amount, - subject: &str, - creditor: &FullIbanPayto, - initiation_time: &Timestamp, - e2e_id: &str, -) -> sqlx::Result<PaymentInitiationResult> { - let res = serialized!( - sqlx::query( - " - INSERT INTO initiated_outgoing_transactions ( - amount, - subject, - credit_payto, - initiation_time, - end_to_end_id - ) VALUES ($1,$2,$3,$4,$5) - RETURNING initiated_outgoing_transaction_id - ", - ) - .bind(amount) - .bind(subject) - .bind(creditor.as_uri().as_ref().as_str()) - .bind_timestamp(initiation_time) - .bind(e2e_id) - .try_map(|r: PgRow| Ok(PaymentInitiationResult::Success(r.try_get_u64(0)?))) - .fetch_one(pool) - ); - if let Err(e) = &res - && let Some(db_err) = e.as_database_error() - && db_err.code() == Some(std::borrow::Cow::Borrowed("23505")) - { - Ok(PaymentInitiationResult::RequestUidReuse) - } else { - res - } -} - -/// Group unbatched transaction into a single batch -pub async fn batch_initiated( - pool: &PgPool, - timestamp: &Timestamp, - ebics_id: &str, - require_ack: bool, -) -> sqlx::Result<()> { - serialized!( - sqlx::query("SELECT batch_outgoing_transactions($1, $2, $3)") - .bind_timestamp(timestamp) - .bind(ebics_id) - .bind(require_ack) - .execute(pool) - )?; - Ok(()) -} - -pub async fn initiated_ack(db: &PgPool, id: u64) -> sqlx::Result<bool> { - let res = serialized!( - sqlx::query("UPDATE initiated_outgoing_transactions SET awaiting_ack=false WHERE initiated_outgoing_transaction_id=$1") - .bind(id as i64) - .execute(db) - )?; - Ok(res.rows_affected() > 0) -} - -pub async fn initiated_submittable( - db: &PgPool, - currency: &Currency, -) -> sqlx::Result<Vec<PaymentBatch>> { - const SELECT_PART: &str = " - SELECT initiated_outgoing_batch_id, message_id, creation_date, sum - FROM initiated_outgoing_batches - "; - serialized!(async { - let mut tx = db.begin().await?; - // We want to maximize the number of successfully submitted batches in the event - // of a malformed transaction or a persistent error classified as transient. We send - // the unsubmitted batches first, starting with the oldest by creation time. - // This is the happy path, giving every batch a chance while being fair on the - // basis of creation date. - // Then we retry the failed batches, starting with the oldest by submission time. - // This the bad path retrying each failed batch applying a rotation based on - // resubmission time. - let mut batches = sqlx::query(formatcp!( - " - ({SELECT_PART} WHERE status='unsubmitted' ORDER BY creation_date ASC) - UNION ALL - ({SELECT_PART} WHERE status='transient_failure' ORDER BY submission_date) - " - )) - .try_map(|r: PgRow| { - Ok(PaymentBatch { - id: r.try_get_u64("initiated_outgoing_batch_id")?, - msg_id: r.try_get("message_id")?, - creation_date: r.try_get_timestamp("creation_date")?, - sum: r.try_get_amount("sum", currency)?, - payments: Vec::new(), - }) - }) - .fetch_all(&mut *tx) - .await?; - let mut batch_map: BTreeMap<_, _> = batches.iter_mut().map(|it| (it.id, it)).collect(); - // Then load transactions - sqlx::query( - " - SELECT - initiated_outgoing_transaction_id - ,amount - ,subject - ,credit_payto - ,initiated_outgoing_transactions.initiation_time - ,end_to_end_id - ,initiated_outgoing_batch_id - FROM initiated_outgoing_transactions - JOIN initiated_outgoing_batches USING (initiated_outgoing_batch_id) - WHERE initiated_outgoing_batches.status IN ('unsubmitted', 'transient_failure') - ", - ) - .try_map(|r: PgRow| { - let payment = Initiated { - id: r.try_get_u64("initiated_outgoing_transaction_id")?, - amount: r.try_get_amount("amount", currency)?, - creditor: r.try_get_parse("credit_payto")?, - subject: r.try_get("subject")?, - initiation_time: r.try_get_timestamp("initiation_time")?, - e2e_id: r.try_get("end_to_end_id")?, - }; - let batch_id = r.try_get_u64("initiated_outgoing_batch_id")?; - batch_map.get_mut(&batch_id).unwrap().payments.push(payment); - Ok(()) - }) - .fetch_all(&mut *tx) - .await?; - tx.commit().await?; - Ok(batches) - }) -} - -pub async fn unsettled_tx_in_batch( - db: &PgPool, - currency: &Currency, - msg_id: &str, - execution_time: &Timestamp, -) -> sqlx::Result<Vec<OutTx>> { - serialized!( - sqlx::query(formatcp!( - " - SELECT - end_to_end_id, - amount, - subject, - credit_payto - FROM initiated_outgoing_transactions - JOIN initiated_outgoing_batches USING (initiated_outgoing_batch_id) - WHERE message_id = $1 - AND initiated_outgoing_transactions.{UNSETTLED} - " - )) - .bind(msg_id) - .try_map(|r: PgRow| { - Ok(OutTx { - id: OutId { - msg_id: Some(msg_id.into()), - e2e_id: r.try_get("end_to_end_id")?, - sref: None, - }, - amount: r.try_get_amount("amount", currency)?, - debit_fee: Amount::zero(currency), - subject: r.try_get("subject")?, - execution_time: *execution_time, - creditor: r.try_get_opt_payto("credit_payto")?, - }) - }) - .fetch_all(db) - ) -} - -/** Register submission success of order [orderId] for batch [id] at [timestamp] */ -pub async fn batch_sub_success( - db: &PgPool, - batch_id: u64, - timestamp: &Timestamp, - order_id: &str, -) -> sqlx::Result<()> { - serialized!(async { - let mut tx = db.begin().await?; - // Update batch status - let updated = sqlx::query( - " - UPDATE initiated_outgoing_batches - SET status = 'pending' - ,submission_date = $1 - ,status_msg = NULL - ,order_id = $2 - ,submission_counter = submission_counter + 1 - WHERE initiated_outgoing_batch_id = $3 AND order_id IS NULL - ", - ) - .bind_timestamp(timestamp) - .bind(order_id) - .bind(batch_id as i64) - .execute(&mut *tx) - .await?; - if updated.rows_affected() > 0 { - // Update unsettled batch's transaction status - sqlx::query(formatcp!( - " - UPDATE initiated_outgoing_transactions - SET status = 'pending', status_msg = NULL - WHERE initiated_outgoing_batch_id = $1 AND {UNSETTLED} - " - )) - .bind(batch_id as i64) - .execute(&mut *tx) - .await?; - } - tx.commit().await - }) -} - -/** Register submission failure with [msg] for batch [id] at [timestamp]*/ -pub async fn batch_sub_failure( - db: &PgPool, - batch_id: u64, - timestamp: &Timestamp, - msg: &str, -) -> sqlx::Result<()> { - let permanent = false; - serialized!(async { - let mut tx = db.begin().await?; - // Update batch status - sqlx::query( - " - UPDATE initiated_outgoing_batches - SET status = $1 - ,submission_date = $2 - ,status_msg = $3 - ,submission_counter = submission_counter + 1 - WHERE initiated_outgoing_batch_id = $4 - ", - ) - .bind(if permanent { - SubmissionState::permanent_failure - } else { - SubmissionState::transient_failure - }) - .bind_timestamp(timestamp) - .bind(msg) - .bind(batch_id as i64) - .execute(&mut *tx) - .await?; - // Update unsettled batch's transaction status - sqlx::query(formatcp!( - " - UPDATE initiated_outgoing_transactions - SET status = $1, status_msg = $2 - WHERE initiated_outgoing_batch_id = $3 AND {UNSETTLED} - " - )) - .bind(if permanent { - SubmissionState::permanent_failure - } else { - SubmissionState::transient_failure - }) - .bind(msg) - .bind(batch_id as i64) - .execute(&mut *tx) - .await?; - tx.commit().await - }) -} - -/** Register order step [msg] for [orderId] */ -pub async fn order_step(db: &PgPool, order_id: &str, msg: &str) -> sqlx::Result<()> { - serialized!(async { - let mut tx = db.begin().await?; - // Update batch status - let batch_id = sqlx::query(formatcp!( - " - UPDATE initiated_outgoing_batches - SET status = 'pending', status_msg = $1 - WHERE order_id = $2 AND {PENDING} - RETURNING initiated_outgoing_batch_id - " - )) - .bind(msg) - .bind(order_id) - .try_map(|r: PgRow| r.try_get_u64(0)) - .fetch_optional(&mut *tx) - .await?; - if let Some(batch_id) = batch_id { - // Update unsettled batch's transaction status - sqlx::query(formatcp!( - " - UPDATE initiated_outgoing_transactions - SET status = 'pending', status_msg = $1 - WHERE initiated_outgoing_batch_id = $2 AND {PENDING} - " - )) - .bind(msg) - .bind(batch_id as i64) - .execute(&mut *tx) - .await?; - } - tx.commit().await - }) -} - -/** Register order success for [orderId] and return message_id if found */ -pub async fn order_success(db: &PgPool, order_id: &str) -> sqlx::Result<Option<String>> { - serialized!(async { - let mut tx = db.begin().await?; - // Update batch status - let res = sqlx::query(formatcp!( - " - UPDATE initiated_outgoing_batches - SET status = 'success' - WHERE order_id = $1 - RETURNING initiated_outgoing_batch_id, message_id - " - )) - .bind(order_id) - .try_map(|r: PgRow| Ok((r.try_get_u64(0)?, r.try_get(1)?))) - .fetch_optional(&mut *tx) - .await?; - if let Some((batch_id, _)) = &res { - // Update unsettled batch's transaction status - sqlx::query(formatcp!( - " - UPDATE initiated_outgoing_transactions - SET status = 'pending' - WHERE initiated_outgoing_batch_id = $1 AND {UNSETTLED} - " - )) - .bind(*batch_id as i64) - .execute(&mut *tx) - .await?; - } - tx.commit().await?; - Ok(res.map(|(_, msg_id)| msg_id)) - }) -} - -/** Register order failure for [orderId] and return message_id and previous status_msg if found */ -pub async fn order_failure( - db: &PgPool, - order_id: &str, -) -> sqlx::Result<Option<(String, Option<String>)>> { - serialized!(async { - let mut tx = db.begin().await?; - // Update batch status - let res = sqlx::query(formatcp!( - " - UPDATE initiated_outgoing_batches - SET status = 'permanent_failure' - WHERE order_id = $1 - RETURNING initiated_outgoing_batch_id, message_id, status_msg - " - )) - .bind(order_id) - .try_map(|r: PgRow| Ok((r.try_get_u64(0)?, r.try_get(1)?, r.try_get(2)?))) - .fetch_optional(&mut *tx) - .await?; - if let Some((batch_id, _, _)) = &res { - // Update unsettled batch's transaction status - sqlx::query(formatcp!( - " - UPDATE initiated_outgoing_transactions - SET status = 'permanent_failure' - WHERE initiated_outgoing_batch_id = $1 - " - )) - .bind(*batch_id as i64) - .execute(&mut *tx) - .await?; - } - tx.commit().await?; - Ok(res.map(|(_, msg_id, status_msg)| (msg_id, status_msg))) - }) -} - -/** Register payment status [state] with [msg] for batch [msgId] */ -pub async fn batch_status_update( - db: &PgPool, - msg_id: &str, - state: SubmissionState, - msg: &str, -) -> sqlx::Result<bool> { - serialized!( - sqlx::query(formatcp!( - "SELECT out_ok FROM batch_status_update($1,$2,$3)" - )) - .bind(msg_id) - .bind(state) - .bind(msg) - .try_map(|r: PgRow| r.try_get(0)) - .fetch_one(db) - ) -} - -/** Register payment status [state] with [msg] for transaction [endToEndId] in batch [msgId] */ -pub async fn tx_status_update( - db: &PgPool, - end_to_end_id: &str, - msg_id: &str, - state: SubmissionState, - msg: &str, -) -> sqlx::Result<bool> { - serialized!( - sqlx::query(formatcp!( - "SELECT out_ok FROM tx_status_update($1,$2,$3,$4)" - )) - .bind(end_to_end_id) - .bind(msg_id) - .bind(state) - .bind(msg) - .try_map(|r: PgRow| r.try_get(0)) - .fetch_one(db) - ) -} - -#[cfg(test)] -mod test { - use std::str::FromStr as _; - - use jiff::{Span, Timestamp, civil::Date}; - use libeufin_ebics::{ebics::rand_ebics_id, iso20022::model::Tx}; - use sqlx::{PgPool, Row as _, postgres::PgRow}; - use taler_api::db::TypeHelper as _; - use taler_common::{config::Config, types::utils::date_to_utc_ts}; - - use crate::{ - CONFIG_SOURCE, - config::{NexusCfg, NexusIngestCfg}, - db::{ - initiated::{ - PaymentInitiationResult, batch_initiated, batch_status_update, batch_sub_failure, - batch_sub_success, initiated_submittable, order_failure, order_step, order_success, - tx_status_update, - }, - test::{check_count, db_setup}, - }, - fetch::{register_outgoing, register_tx}, - model::SubmissionState, - test::{CURR, gen_in_pay, gen_initiate, gen_out_pay}, - }; - - #[tokio::test] - pub async fn initiated_skip() { - let (_, db) = db_setup().await; - let cfg = - Config::from_file(CONFIG_SOURCE, Some("../../libeufin-nexus/conf/skip.conf")).unwrap(); - let cfg = NexusCfg::parse(cfg).unwrap(); - let cfg = cfg.ingest().unwrap(); - let millis = Span::new().milliseconds(10); - - async fn ingest(db: &PgPool, cfg: &NexusIngestCfg, execution_time: Timestamp) { - for tx in [ - Tx::In( - gen_in_pay(format_args!("test at {execution_time}")) - .with_execution_time(execution_time), - ), - Tx::Out( - gen_out_pay(format!("test at {execution_time}")) - .with_execution_time(execution_time), - ), - ] { - register_tx(db, cfg, &tx).await.unwrap() - } - } - - assert_eq!( - cfg.ignore_txs_before, - date_to_utc_ts(&Date::from_str("2024-04-04").unwrap()) - ); - assert_eq!( - cfg.ignore_bounces_before, - date_to_utc_ts(&Date::from_str("2024-06-12").unwrap()) - ); - - // No transaction at the beginning - check_count(&db, 0, 0).await; - - // Skipped transactions - ingest(&db, &cfg, cfg.ignore_txs_before - millis).await; - check_count(&db, 0, 0).await; - - // Skipped bounces - ingest(&db, &cfg, cfg.ignore_txs_before).await; - ingest(&db, &cfg, cfg.ignore_txs_before + millis).await; - ingest(&db, &cfg, cfg.ignore_bounces_before - millis).await; - check_count(&db, 6, 0).await; - - // Bounces - ingest(&db, &cfg, cfg.ignore_bounces_before).await; - ingest(&db, &cfg, cfg.ignore_bounces_before + millis).await; - check_count(&db, 10, 2).await; - } - - #[tokio::test] - pub async fn initiated_status() { - use SubmissionState::*; - - let (_, db) = db_setup().await; - - let check_parts = async |batch_id: u64, - batch_status: SubmissionState, - batch_msg: &str, - tx_status: SubmissionState, - tx_msg: &str, - settled_status: SubmissionState, - settled_msg: &str| { - // Check batch status - let msg_id: String = sqlx::query( - " - SELECT message_id, status, status_msg FROM initiated_outgoing_batches WHERE initiated_outgoing_batch_id=$1 - " - ).bind(batch_id as i64) - .try_map(|r: PgRow| { - let msg_id: String = r.try_get("message_id")?; - assert_eq!((batch_status, Some(batch_msg).filter(|it| !it.is_empty())), (r.try_get("status")?, r.try_get("status_msg")?), "{msg_id}"); - Ok(msg_id) - }).fetch_one(&db).await.unwrap(); - // Check tx status - sqlx::query( - " - SELECT end_to_end_id, status, status_msg FROM initiated_outgoing_transactions WHERE initiated_outgoing_batch_id=$1 - " - ).bind(batch_id as i64).try_map(|r: PgRow| { - let end_to_end_id: &str = r.try_get("end_to_end_id")?; - let expected = match end_to_end_id { - "TX" => (tx_status, Some(tx_msg).filter(|it| !it.is_empty())), - "TX_SETTLED" => (settled_status, Some(settled_msg).filter(|it| !it.is_empty())), - _ =>panic!("Unexpected tx $endToEndId") - }; - assert_eq!(expected, - (r.try_get("status")?, r.try_get("status_msg")?), - "{msg_id},{end_to_end_id}" - ); - Ok(()) - }).fetch_all(&db).await.unwrap(); - }; - - let check_batch_tx = async |batch_id: u64, - status: SubmissionState, - msg: &str, - tx_status: SubmissionState| { - check_parts(batch_id, status, msg, tx_status, msg, tx_status, msg).await; - }; - let check_batch = async |batch_id: u64, status: SubmissionState, msg: &str| { - check_batch_tx(batch_id, status, msg, status).await; - }; - let check_order_tx = async |order_id: &str, - status: SubmissionState, - msg: &str, - tx_status: SubmissionState| { - let batch_id = sqlx::query( - "SELECT initiated_outgoing_batch_id FROM initiated_outgoing_batches WHERE order_id=$1" - ).bind(order_id) - .try_map(|r: PgRow| { - r.try_get_u64(0) - }).fetch_one(&db).await.unwrap(); - check_batch_tx(batch_id, status, msg, tx_status).await; - }; - let check_order = async |order_id: &str, status: SubmissionState, msg: &str| { - check_order_tx(order_id, status, msg, status).await; - }; - - async fn test(db: &PgPool, lambda: impl AsyncFnOnce(u64)) { - // Reset DB - sqlx::query("DELETE FROM initiated_outgoing_transactions") - .execute(db) - .await - .unwrap(); - sqlx::query("DELETE FROM initiated_outgoing_batches") - .execute(db) - .await - .unwrap(); - // Create a test batch with three transactions - for id in ["TX", "TX_SETTLED"] { - assert!(matches!( - gen_initiate(db, id, "lol").await, - PaymentInitiationResult::Success(_) - )); - } - batch_initiated(db, &Timestamp::now(), "BATCH", false) - .await - .unwrap(); - - // Create witness transactions and batch - for id in ["WITNESS_1", "WITNESS_2"] { - assert!(matches!( - gen_initiate(db, id, "lol").await, - PaymentInitiationResult::Success(_) - )); - } - batch_initiated(db, &Timestamp::now(), "BATCH_WITNESS", false) - .await - .unwrap(); - for id in ["WITNESS_3", "WITNESS_4"] { - assert!(matches!( - gen_initiate(db, id, "lol").await, - PaymentInitiationResult::Success(_) - )); - } - // Check everything is unsubmitted - sqlx::query( - " - SELECT (SELECT bool_and(status = 'unsubmitted') FROM initiated_outgoing_batches) - AND (SELECT bool_and(status = 'unsubmitted') FROM initiated_outgoing_transactions) - " - ).try_map(|r: PgRow| { - assert!(r.try_get_flag(0).unwrap()); - Ok(()) - }).fetch_one(db).await.unwrap(); - let submitibale = initiated_submittable(db, &CURR).await.unwrap(); - lambda( - submitibale - .iter() - .find(|it| it.msg_id == "BATCH") - .unwrap() - .id, - ) - .await; - // Check witness status is unaltered - sqlx::query( - " - SELECT (SELECT bool_and(status = 'unsubmitted') FROM initiated_outgoing_batches WHERE message_id != 'BATCH') - AND (SELECT bool_and(initiated_outgoing_transactions.status = 'unsubmitted') - FROM initiated_outgoing_transactions JOIN initiated_outgoing_batches USING (initiated_outgoing_batch_id) - WHERE message_id != 'BATCH') - " - ).try_map(|r: PgRow| { - assert!(r.try_get(0)?); - Ok(()) - }).fetch_one(db).await.unwrap(); - } - - let now = Timestamp::now(); - - // Submission retry status - test(&db, async |batch_id| { - batch_sub_failure(&db, batch_id, &now, "First failure") - .await - .unwrap(); - check_batch(batch_id, transient_failure, "First failure").await; - batch_sub_failure(&db, batch_id, &now, "Second failure") - .await - .unwrap(); - check_batch(batch_id, transient_failure, "Second failure").await; - batch_sub_success(&db, batch_id, &now, "ORDER") - .await - .unwrap(); - check_order("ORDER", pending, "").await; - batch_sub_success(&db, batch_id, &now, "ORDER") - .await - .unwrap(); - check_order("ORDER", pending, "").await; - order_step(&db, "ORDER", "step msg").await.unwrap(); - check_order("ORDER", pending, "step msg").await; - order_step(&db, "ORDER", "success msg").await.unwrap(); - check_order("ORDER", pending, "success msg").await; - order_success(&db, "ORDER").await.unwrap(); - check_order_tx("ORDER", success, "success msg", pending).await; - order_step(&db, "ORDER", "late msg").await.unwrap(); - check_order_tx("ORDER", success, "success msg", pending).await; - }) - .await; - - // Order step message on failure - test(&db, async |batch_id| { - batch_sub_success(&db, batch_id, &now, "ORDER") - .await - .unwrap(); - check_order("ORDER", pending, "").await; - order_step(&db, "ORDER", "step msg").await.unwrap(); - check_order("ORDER", pending, "step msg").await; - order_step(&db, "ORDER", "failure msg").await.unwrap(); - check_order("ORDER", pending, "failure msg").await; - assert_eq!( - Some("failure msg"), - order_failure(&db, "ORDER") - .await - .unwrap() - .unwrap() - .1 - .as_deref() - ); - check_order("ORDER", permanent_failure, "failure msg").await; - order_step(&db, "ORDER", "late msg").await.unwrap(); - check_order("ORDER", permanent_failure, "failure msg").await; - }) - .await; - - // Payment & batch status - test(&db, async |batch_id| { - check_batch(batch_id, unsubmitted, "").await; - batch_status_update(&db, "BATCH", pending, "progress") - .await - .unwrap(); - check_batch(batch_id, pending, "progress").await; - tx_status_update(&db, "TX_SETTLED", "", success, "success") - .await - .unwrap(); - check_parts( - batch_id, pending, "progress", pending, "progress", success, "success", - ) - .await; - batch_status_update(&db, "BATCH", transient_failure, "waiting") - .await - .unwrap(); - check_parts( - batch_id, - transient_failure, - "waiting", - transient_failure, - "waiting", - success, - "success", - ) - .await; - tx_status_update(&db, "TX", "BATCH", permanent_failure, "failure") - .await - .unwrap(); - check_parts( - batch_id, - success, - "", - permanent_failure, - "failure", - success, - "success", - ) - .await; - tx_status_update(&db, "TX_SETTLED", "BATCH", permanent_failure, "late") - .await - .unwrap(); - check_parts( - batch_id, - success, - "", - permanent_failure, - "failure", - late_failure, - "late", - ) - .await; - }) - .await; - - // Registration - test(&db, async |batch_id| { - check_batch(batch_id, unsubmitted, "").await; - register_outgoing(&db, &gen_out_pay("").with_e2e_id("TX_SETTLED")) - .await - .unwrap(); - check_parts(batch_id, unsubmitted, "", unsubmitted, "", success, "").await; - register_outgoing(&db, &gen_out_pay("").with_e2e_id("TX").with_msg_id("BATCH")) - .await - .unwrap(); - check_parts(batch_id, success, "", success, "", success, "").await; - }) - .await; - - // Transaction failure take over batch failures - test(&db, async |batch_id| { - check_batch(batch_id, unsubmitted, "").await; - batch_status_update(&db, "BATCH", permanent_failure, "batch") - .await - .unwrap(); - check_parts( - batch_id, - permanent_failure, - "batch", - permanent_failure, - "batch", - permanent_failure, - "batch", - ) - .await; - tx_status_update(&db, "TX", "BATCH", permanent_failure, "tx") - .await - .unwrap(); - batch_status_update(&db, "BATCH", permanent_failure, "batch2") - .await - .unwrap(); - check_parts( - batch_id, - permanent_failure, - "batch", - permanent_failure, - "tx", - permanent_failure, - "batch", - ) - .await; - }) - .await; - - // Unknown order and batch - batch_sub_success(&db, 42, &now, "ORDER_X").await.unwrap(); - batch_sub_failure(&db, 42, &now, "").await.unwrap(); - order_step(&db, "ORDER_X", "msg").await.unwrap(); - batch_status_update(&db, "BATCH_X", success, "") - .await - .unwrap(); - tx_status_update(&db, "TX_X", "BATCH_X", success, "msg") - .await - .unwrap(); - assert!(order_success(&db, "ORDER_X").await.unwrap().is_none()); - assert!(order_failure(&db, "ORDER_X").await.unwrap().is_none()); - } - - #[tokio::test] - pub async fn initiated_submittables() { - let (_, db) = db_setup().await; - let now = Timestamp::now(); - for i in 0..6 { - assert!(matches!( - gen_initiate(&db, format!("PAY{i}"), "").await, - PaymentInitiationResult::Success(_) - )); - batch_initiated(&db, &now, &rand_ebics_id(), false) - .await - .unwrap(); - } - - let check_ids = async |ids: &[&str]| { - assert_eq!( - ids, - initiated_submittable(&db, &CURR) - .await - .unwrap() - .iter() - .flat_map(|it| it.payments.iter().map(|it| it.e2e_id.as_str())) - .collect::<Vec<_>>() - ); - }; - check_ids(&["PAY0", "PAY1", "PAY2", "PAY3", "PAY4", "PAY5"]).await; - - // Check submitted not submitable - batch_sub_success(&db, 1, &now, "ORDER1").await.unwrap(); - check_ids(&["PAY1", "PAY2", "PAY3", "PAY4", "PAY5"]).await; - - // Check transient failure submitable last - batch_sub_failure(&db, 2, &now, "Failure").await.unwrap(); - check_ids(&["PAY2", "PAY3", "PAY4", "PAY5", "PAY1"]).await; - - // Check persistent failure not submitable - batch_sub_success(&db, 4, &now, "ORDER3").await.unwrap(); - order_failure(&db, "ORDER3").await.unwrap(); - check_ids(&["PAY2", "PAY4", "PAY5", "PAY1"]).await; - batch_sub_success(&db, 5, &now, "ORDER4").await.unwrap(); - order_failure(&db, "ORDER4").await.unwrap(); - check_ids(&["PAY2", "PAY5", "PAY1"]).await; - - // Check rotation - batch_sub_failure(&db, 3, &Timestamp::now(), "FAILURE") - .await - .unwrap(); - check_ids(&["PAY5", "PAY1", "PAY2"]).await; - batch_sub_failure(&db, 6, &Timestamp::now(), "FAILURE") - .await - .unwrap(); - check_ids(&["PAY1", "PAY2", "PAY5"]).await; - batch_sub_failure(&db, 2, &Timestamp::now(), "FAILURE") - .await - .unwrap(); - check_ids(&["PAY2", "PAY5", "PAY1"]).await; - } -} diff --git a/crates/libeufin-nexus/src/manual.rs b/crates/libeufin-nexus/src/manual.rs @@ -1,164 +0,0 @@ -/* -* This file is part of LibEuFin. -* Copyright (C) 2026 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/> -*/ - -use std::{ - fmt::Write as _, - io::{BufWriter, Read, Write, stdin, stdout}, -}; - -use anyhow::bail; -use compact_str::CompactString; -use jiff::{Timestamp, tz::TimeZone}; -use libeufin_ebics::ebics::rand_ebics_id; -use sqlx::PgPool; -use taler_macros::EnumMeta; -use tracing::{info, warn}; -use zip::write::FileOptions; - -use crate::{ - batch_pain001, - config::NexusCfg, - db::initiated::{ - batch_initiated, batch_status_update, initiated_ack, initiated_submittable, - tx_status_update, - }, - fetch::register_camt, - model::SubmissionState, -}; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, EnumMeta)] -#[enum_meta(Str)] -pub enum Kind { - batch, - tx, -} - -/// Manual management commands -#[derive(clap::Subcommand, Debug)] -pub enum ManualCmd { - /// Export pending batches as pain001 messages - Export { out: CompactString }, - /// Import EBICS camt files - Import { sources: Vec<CompactString> }, - /// Change batches or transactions status - Status { - kind: Kind, - id: CompactString, - status: SubmissionState, - msg: Option<CompactString>, - }, - /// Manually acknowledge the outgoing transaction for submission - Ack { ids: Vec<u64> }, -} - -impl ManualCmd { - pub async fn run(self, db: &PgPool, cfg: &NexusCfg) -> anyhow::Result<()> { - match self { - ManualCmd::Export { out } => { - let out: &mut dyn Write = if out == "-" { - &mut stdout().lock() - } else { - &mut BufWriter::new(std::fs::File::open(out)?) - }; - - // Create and get pending batches - batch_initiated( - db, - &Timestamp::now(), - &rand_ebics_id(), - cfg.submit()?.require_ack, - ) - .await?; - let mut zip = zip::ZipWriter::new_stream(&mut *out); - let batches = initiated_submittable(&db, &cfg.currency).await?; - let mut metadata = String::new(); - write!(&mut metadata, "Exported {} pain.001 files:", batches.len()).unwrap(); - for batch in batches { - let datetime = batch.creation_date.to_zoned(TimeZone::UTC).datetime(); - zip.start_file( - format!("{datetime}-{}.xml", batch.msg_id), - FileOptions::DEFAULT, - )?; - let xml = batch_pain001(&batch, cfg.ebics()?, false)?; - zip.write_all(xml.as_bytes())?; - write!(&mut metadata, "\nbatch {}:", batch.msg_id)?; - for tx in batch.payments { - write!( - &mut metadata, - "\n- tx {} {} {} '{}'", - tx.e2e_id, tx.amount, tx.creditor.iban, tx.creditor.name - )?; - } - write!(&mut metadata, "\n")?; - } - zip.start_file("README.txt", FileOptions::DEFAULT)?; - zip.write_all(metadata.as_bytes())?; - info!("{metadata}"); - } - ManualCmd::Import { sources } => { - for source in sources { - let xml = if source == "-" { - let mut buf = Vec::with_capacity(8 * 1024); - stdin().lock().read_to_end(&mut buf)?; - buf - } else { - std::fs::read(&source)? - }; - let nb = register_camt(db, cfg, &xml).await?; - info!("Imported {nb} transactions from {source}") - } - } - ManualCmd::Status { - kind, - id, - status, - msg, - } => match kind { - Kind::batch => { - if batch_status_update(&db, &id, status, msg.as_deref().unwrap_or_default()) - .await? - { - info!("Updated batch '{id}' to {status}"); - } else { - bail!("Unknown batch {id}") - } - } - Kind::tx => { - if tx_status_update(&db, &id, "", status, msg.as_deref().unwrap_or_default()) - .await? - { - info!("Updated tx '{id}' to {status}"); - } else { - bail!("Unknown tx {id}") - } - } - }, - ManualCmd::Ack { ids } => { - for id in ids { - if initiated_ack(&db, id).await? { - info!("Mark {id} as acknowledge for submission"); - } else { - warn!("Unknown transaction {id}"); - } - } - } - } - Ok(()) - } -} diff --git a/crates/libeufin-nexus/src/test.rs b/crates/libeufin-nexus/src/test.rs @@ -1,490 +0,0 @@ -/* -* This file is part of LibEuFin. -* Copyright (C) 2026 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/> -*/ - -use std::{fmt::Display, str::FromStr as _, sync::LazyLock}; - -use compact_str::CompactString; -use jiff::Timestamp; -use libeufin_ebics::iso20022::model::{InId, InTx, OutId, OutTx}; -use sqlx::PgPool; -use taler_api::subject::{fmt_in_subject, fmt_out_subject, subject_fmt_qr_bill}; -use taler_common::{ - api::{EddsaPublicKey, EddsaSignature}, - db::IncomingType, - types::{ - amount::{Amount, Currency}, - base32::Base32, - payto::{FullIbanPayto, IbanPayto, PaytoURI, payto}, - }, -}; - -use crate::{ - config::{AccountType, NexusIngestCfg}, - db::{ - initiated::{PaymentInitiationResult, initiate}, - transfer::{RegistrationResult, transfer_register}, - }, - fetch::{register_incoming, register_outgoing}, - model::Initiated, - rand_ebics_id, -}; - -pub const CURR: Currency = Currency::KUDOS; -pub static ACCOUNT: LazyLock<PaytoURI> = - LazyLock::new(|| payto("payto://iban/CH4189144589712575493?receiver-name=Test")); - -/** Generates an outgoing payment, given its subject */ -pub fn gen_out_pay(subject: impl Into<String>) -> OutTx { - OutTx { - id: OutId { - msg_id: None, - e2e_id: Some(rand_ebics_id()), - sref: None, - }, - amount: Amount::new(&CURR, 44, 0), - debit_fee: Amount::zero(&CURR), - creditor: Some( - IbanPayto::from_str("payto://iban/CH4189144589712575493?receiver-name=Test") - .unwrap() - .as_uri(), - ), - subject: Some(subject.into()), - execution_time: Timestamp::now(), - } -} - -/** Generates a payment initiation, given its subject and end-to-end ID */ -pub fn gen_init_pay( - end_to_end_id: impl Into<CompactString>, - subject: impl Into<String>, -) -> Initiated { - Initiated { - id: 0, - amount: Amount::new(&CURR, 44, 0), - creditor: FullIbanPayto::from_str("payto://iban/CH4189144589712575493?receiver-name=Test") - .unwrap(), - subject: subject.into(), - initiation_time: Timestamp::now(), - e2e_id: end_to_end_id.into(), - } -} - -/** Generates an incoming payment, given its subject */ -pub fn gen_in_pay(subject: impl Display) -> InTx { - InTx { - id: InId::new(None, Some(rand_ebics_id()), None), - amount: Amount::new(&CURR, 44, 0), - credit_fee: Amount::zero(&CURR), - debtor: Some( - IbanPayto::from_str("payto://iban/DE84500105177118117964?receiver-name=John+Smith") - .unwrap() - .as_uri(), - ), - subject: Some(subject.to_string()), - execution_time: Timestamp::now(), - } -} - -pub async fn gen_initiate( - db: &PgPool, - end_to_end_id: impl Into<CompactString>, - subject: impl Into<String>, -) -> PaymentInitiationResult { - let init = gen_init_pay(end_to_end_id, subject); - initiate( - db, - &init.amount, - &init.subject, - &init.creditor, - &init.initiation_time, - &init.e2e_id, - ) - .await - .unwrap() -} - -const CFG: NexusIngestCfg = NexusIngestCfg::simple(AccountType::exchange, &CURR); - -async fn prepare(db: &PgPool) -> String { - let key = EddsaPublicKey::rand(); - let sig = EddsaSignature::rand(); - let reference_number = subject_fmt_qr_bill(key.as_ref()); - assert_eq!( - RegistrationResult::Success, - transfer_register( - db, - IncomingType::reserve, - &key, - &key, - &sig, - false, - &reference_number, - &Timestamp::now() - ) - .await - .unwrap() - ); - reference_number -} - -/// Register a talerable reserve prepared incoming transaction -pub async fn prepared_in(db: &PgPool) { - let ref_nb = prepare(db).await; - register_incoming(db, &CFG, &gen_in_pay(ref_nb)) - .await - .unwrap(); -} - -/// Register an incomplete talerable reserve prepared incoming transaction -pub async fn prepared_incomplete_in(db: &PgPool) { - let ref_nb = prepare(db).await; - let incomplete = InTx { - subject: None, - debtor: None, - ..gen_in_pay(ref_nb) - }; - register_incoming(db, &CFG, &incomplete).await.unwrap(); -} - -/// Register a completed talerable reserve prepared incoming transaction -pub async fn prepared_completeted_in(db: &PgPool) { - let ref_nb = prepare(db).await; - let original = gen_in_pay(ref_nb); - let incomplete = InTx { - subject: None, - debtor: None, - ..original.clone() - }; - register_incoming(db, &CFG, &incomplete).await.unwrap(); - register_incoming(db, &CFG, &original).await.unwrap(); -} - -/// Register a talerable reserve incoming transaction -pub async fn talerable_in(db: &PgPool) { - register_incoming( - db, - &CFG, - &gen_in_pay(fmt_in_subject( - IncomingType::reserve, - &EddsaPublicKey::rand(), - )), - ) - .await - .unwrap(); -} - -/// Register a talerable kyc incoming transaction -pub async fn talerable_kyc_in(db: &PgPool) { - register_incoming( - db, - &CFG, - &gen_in_pay(fmt_in_subject(IncomingType::kyc, &EddsaPublicKey::rand())), - ) - .await - .unwrap(); -} - -/// Register an incomplete talerable reserve incoming transaction -pub async fn talerable_incomplete_in(db: &PgPool) { - let incomplete = InTx { - subject: None, - debtor: None, - ..gen_in_pay(fmt_in_subject( - IncomingType::reserve, - &EddsaPublicKey::rand(), - )) - }; - register_incoming(db, &CFG, &incomplete).await.unwrap(); -} - -/// Register a completed talerable reserve incoming transaction -pub async fn talerable_completeted_in(db: &PgPool) { - let original = gen_in_pay(fmt_in_subject( - IncomingType::reserve, - &EddsaPublicKey::rand(), - )); - let incomplete = InTx { - subject: None, - debtor: None, - ..original.clone() - }; - register_incoming(db, &CFG, &incomplete).await.unwrap(); - register_incoming(db, &CFG, &original).await.unwrap(); -} - -/// Register incoming malformed transaction -pub async fn malformed_in(db: &PgPool) { - register_incoming(db, &CFG, &gen_in_pay("ignored")) - .await - .unwrap(); -} - -/// Register incoming incomplete malformed incoming transaction -pub async fn malformed_incomplete_in(db: &PgPool) { - let incomplete = InTx { - subject: None, - debtor: None, - ..gen_in_pay("ignored") - }; - register_incoming(db, &CFG, &incomplete).await.unwrap(); -} - -/// Register incoming completed malformed transaction -pub async fn malformed_completeted_in(db: &PgPool) { - let original = gen_in_pay("ignored"); - let incomplete = InTx { - subject: None, - debtor: None, - ..original.clone() - }; - register_incoming(db, &CFG, &incomplete).await.unwrap(); - register_incoming(db, &CFG, &original).await.unwrap(); -} - -/** Register an outgoing transaction */ -pub async fn malformed_out(db: &PgPool) { - register_outgoing(db, &gen_out_pay("ignored")) - .await - .unwrap(); -} - -/** Register an incomplete outgoing transaction */ -pub async fn incomplete_out(db: &PgPool) { - let incomplete = OutTx { - subject: None, - creditor: None, - ..gen_out_pay("ignored") - }; - register_outgoing(db, &incomplete).await.unwrap(); -} - -/// Register outgoing talerable transaction -pub async fn talerable_out(db: &PgPool) { - register_outgoing( - db, - &gen_out_pay(fmt_out_subject( - &Base32::rand(), - "https://exchange.test.com", - None, - )), - ) - .await - .unwrap(); -} - -#[cfg(test)] -mod ebics { - use clap::Parser as _; - use libeufin_ebics::test::{EbicsState, TestBank}; - use sqlx::PgPool; - use taler_common::config::Config; - - use crate::{Args, CHECKPOINT_KEY, CONFIG_SOURCE, db::test::db_setup, run}; - - pub async fn nexus_cmd(cfg: &Config, cmd: &str) -> anyhow::Result<()> { - let parts = shlex::split(cmd).unwrap(); - let args = std::iter::once("libeufin_nexus").chain(parts.iter().map(|it| it.as_str())); - - let cmd = Args::try_parse_from(args).unwrap(); - run(cfg.clone(), cmd.cmd).await - } - - async fn test_setup() -> (TestBank, Config, PgPool) { - let (_, db) = db_setup().await; - let test = TestBank::new().await; - let cfg = Config::from_mem_with_env( - CONFIG_SOURCE, - &format!( - " - [paths] - LIBEUFIN_NEXUS_HOME = {:?} - - {} - - [nexus-ebics] - UNIXPATH = {} - - [libeufin-nexusdb-postgres] - CONFIG = postgresql:///{} - ", - test.dir.path(), - include_str!("../../../testbench/conf/mini.conf"), - test.sock_path, - db.connect_options().get_database().unwrap() - ), - ) - .unwrap(); - test.sequences(&[ - EbicsState::hev, - EbicsState::ini, - EbicsState::hia, - EbicsState::hpb, - EbicsState::hkd, - EbicsState::receipt_ok, - ]); - nexus_cmd(&cfg, "ebics-setup --auto-accept-keys") - .await - .unwrap(); - - (test, cfg, db) - } - - #[tokio::test] - async fn setup() { - test_setup().await; - } - - #[tokio::test] - async fn fetch_pinned_date() { - let (test, cfg, db) = test_setup().await; - - let reset_checkpoint = async || { - let res = sqlx::query("DELETE FROM kv WHERE key=$1") - .bind(CHECKPOINT_KEY) - .execute(&db) - .await - .unwrap(); - assert_eq!(res.rows_affected(), 1); - }; - - // Default transient - test.sequences(&[ - EbicsState::haa, - EbicsState::receipt_ok, - EbicsState::btd_no_data, - ]); - nexus_cmd(&cfg, "ebics-fetch --transient").await.unwrap(); - - // Pinned transient - test.sequences(&[ - EbicsState::haa, - EbicsState::receipt_ok, - EbicsState::btd_no_data_pinned, - ]); - nexus_cmd(&cfg, "ebics-fetch --transient --pinned-start 2024-06-05") - .await - .unwrap(); - - // Init checkpoint - test.sequences(&[ - EbicsState::hkd, - EbicsState::receipt_ok, - EbicsState::btd_no_data, - ]); - nexus_cmd(&cfg, "ebics-fetch --transient --checkpoint") - .await - .unwrap(); - - // Default checkpoint - test.sequences(&[ - EbicsState::hkd, - EbicsState::receipt_ok, - EbicsState::btd_no_data_now, - ]); - nexus_cmd(&cfg, "ebics-fetch --transient --checkpoint") - .await - .unwrap(); - - // Pinned checkpoint - test.sequences(&[ - EbicsState::hkd, - EbicsState::receipt_ok, - EbicsState::btd_no_data_pinned, - ]); - nexus_cmd( - &cfg, - "ebics-fetch --transient --checkpoint --pinned-start 2024-06-05", - ) - .await - .unwrap(); - - // Reset checkpoint - reset_checkpoint().await; - test.sequences(&[ - EbicsState::hkd, - EbicsState::receipt_ok, - EbicsState::btd_no_data, - ]); - nexus_cmd(&cfg, "ebics-fetch --transient --checkpoint") - .await - .unwrap(); - - // Reset pinned checkpoint - reset_checkpoint().await; - test.sequences(&[ - EbicsState::hkd, - EbicsState::receipt_ok, - EbicsState::btd_no_data_pinned, - ]); - nexus_cmd( - &cfg, - "ebics-fetch --transient --checkpoint --pinned-start 2024-06-05", - ) - .await - .unwrap(); - } - - #[tokio::test] - async fn close_pending_transaction() { - let (test, cfg, _) = test_setup().await; - - // Failure before first segment - test.sequences(&[ - // Failure to perform download - EbicsState::failure, - // Then continue - EbicsState::haa, - EbicsState::receipt_ok, - EbicsState::btd_no_data, - ]); - nexus_cmd(&cfg, "ebics-fetch --transient") - .await - .unwrap_err(); - nexus_cmd(&cfg, "ebics-fetch --transient").await.unwrap(); - - // Compliant server - test.sequences(&[ - EbicsState::haa, - EbicsState::receipt_ok, - // Failure to perform download - EbicsState::init_tx, - EbicsState::failure, - // Retry fail once - EbicsState::failure, - // Retry fail twice - EbicsState::failure, - // Retry succeed - EbicsState::bad_request, - // Then continue - EbicsState::haa, - EbicsState::receipt_ok, - EbicsState::btd_no_data, - ]); - nexus_cmd(&cfg, "ebics-fetch --transient") - .await - .unwrap_err(); - nexus_cmd(&cfg, "ebics-fetch --transient") - .await - .unwrap_err(); - nexus_cmd(&cfg, "ebics-fetch --transient") - .await - .unwrap_err(); - nexus_cmd(&cfg, "ebics-fetch --transient").await.unwrap(); - } -} diff --git a/crates/testbench/Cargo.toml b/crates/testbench/Cargo.toml @@ -1,22 +0,0 @@ -[package] -name = "testbench" -version.workspace = true -edition.workspace = true -authors.workspace = true -homepage.workspace = true -repository.workspace = true -license-file.workspace = true - -[dependencies] -tokio.workspace = true -tracing.workspace = true -anyhow.workspace = true -jiff.workspace = true -clap.workspace = true -taler-common.workspace = true -libeufin-nexus = { path = "../libeufin-nexus"} -libeufin-ebics = { path = "../libeufin-ebics"} -reedline = "0.47" -shlex = "2.0" -owo-colors = "4.3" -tracing-subscriber = "0.3" -\ No newline at end of file diff --git a/crates/testbench/src/main.rs b/crates/testbench/src/main.rs @@ -1,500 +0,0 @@ -/* -* This file is part of LibEuFin. -* Copyright (C) 2026 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/> -*/ - -use std::{borrow::Cow, fmt::Display, str::FromStr}; - -use anyhow::bail; -use clap::{Parser, ValueEnum}; -use jiff::Timestamp; -use libeufin_ebics::keys::{load_bank_keys, load_client_keys}; -use libeufin_nexus::{CONFIG_SOURCE, config::NexusCfg, run}; -use owo_colors::OwoColorize as _; -use reedline::{FileBackedHistory, Prompt, Reedline, Signal}; -use taler_common::{config::Config, log::taler_logger, types::payto::TransferIbanPayto}; -use tracing::Level; -use tracing_subscriber::util::SubscriberInitExt as _; - -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] -enum Component { - Nexus, - Ebisync, -} - -#[derive(Parser)] -/// Run integration tests on banks provider -pub struct TestbenchCmd { - #[arg(value_enum)] - component: Component, - platform: String, -} - -#[derive(Parser)] -#[command(name = "shell", no_binary_name = true)] -/// Run integration tests on banks provider -pub enum NexusCmd { - #[clap(alias = "ebics-setup")] - Setup, - ResetKeys, - ResetDb, - Tx, - Fetch { - #[arg(trailing_var_arg = true, allow_hyphen_values = true)] - raw_args: Vec<String>, - }, - Submit { - #[arg(trailing_var_arg = true, allow_hyphen_values = true)] - raw_args: Vec<String>, - }, - List { - #[arg(trailing_var_arg = true, allow_hyphen_values = true)] - raw_args: Vec<String>, - }, - Wss, - TxCheck, - Exit, -} - -fn step(name: impl Display) { - println!("{}", name.magenta()) -} - -fn msg(msg: impl Display) { - println!("{}", msg.yellow()) -} - -fn err(msg: impl Display) { - println!("{}", msg.red()) -} - -fn check<R, E: Display>(res: Result<R, E>) -> bool { - match &res { - Ok(_) => println!("{}", "OK".green()), - Err(e) => { - tracing::error!(target: "testbench", "{e}"); - err("ERROR") - } - }; - res.is_ok() -} - -pub async fn nexus_cmd(cfg: &Config, cmd: &str) -> bool { - let parts = shlex::split(cmd).unwrap(); - let args = std::iter::once("libeufin_nexus").chain(parts.iter().map(|it| it.as_str())); - - match libeufin_nexus::Args::try_parse_from(args) { - Ok(cmd) => { - tokio::select! { - res = run(cfg.clone(), cmd.cmd) => check(res), - _ = tokio::signal::ctrl_c() => false - } - } - Err(e) => { - println!("Error: {}", e); - false - } - } -} - -#[tokio::main] -async fn main() -> anyhow::Result<()> { - taler_logger(Some(Level::DEBUG), false).init(); - let cmd = TestbenchCmd::parse(); - // List available platform - let platforms: Vec<_> = std::fs::read_dir("testbench/test/platform") - .unwrap() - .filter_map(|entry| { - let e = entry.unwrap(); - let filename = e.file_name(); - if filename == "config.json" { - None - } else { - Some( - filename - .to_string_lossy() - .strip_suffix(".conf") - .unwrap() - .to_owned(), - ) - } - }) - .collect(); - if !platforms.contains(&cmd.platform) { - bail!( - "Unknown platform '{}', expected one of {}", - cmd.platform, - platforms.join(", ") - ); - } - - // Augment config - let simple_cfg = - std::fs::read_to_string(format!("testbench/test/platform/{}.conf", cmd.platform)).unwrap(); - let cfg = format!( - r#" - {simple_cfg} - {} - [paths] - LIBEUFIN_NEXUS_HOME = testbench/test/{} - EBISYNC_HOME = testbench/test/{} - - [nexus-fetch] - FREQUENCY = 1h - CHECKPOINT_TIME_OF_DAY = 16:52 - - [ebisync-fetch] - FREQUENCY = 1h - CHECKPOINT_TIME_OF_DAY = 16:52 - DESTINATION = azure-blob-storage - AZURE_API_URL = http://localhost:10000/devstoreaccount1/ - AZURE_ACCOUNT_NAME = devstoreaccount1 - AZURE_ACCOUNT_KEY = Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw== - AZURE_CONTAINER = test - - [ebisync-submit] - SOURCE = ebisync-api - AUTH_METHOD = none - - [libeufin-nexusdb-postgres] - CONFIG = postgres:///libeufintestbench - - [ebisyncdb-postgres] - CONFIG = postgres:///libeufintestbench - "#, - simple_cfg - .replace("[nexus-ebics]", "[ebisync]") - .replace("[nexus-setup]", "[ebisync-setup]"), - cmd.platform, - cmd.platform - ); - - let history = Box::new( - FileBackedHistory::with_file( - 1000, - match cmd.component { - Component::Ebisync => ".ebisync_history", - Component::Nexus => ".nexus_history", - } - .into(), - ) - .expect("Error configuring history with file"), - ); - let cfg = Config::from_mem_with_env(CONFIG_SOURCE, &cfg).unwrap(); - let cfg = NexusCfg::parse(cfg).unwrap(); - let ebics = cfg.keys().unwrap(); - let (name, settings) = match cfg.host().unwrap().base_url.as_str() { - "https://isotest.postfinance.ch/ebicsweb/ebicsweb" => ( - Some("PostFinance IsoTest"), - Some("https://isotest.postfinance.ch/corporates/user/settings/ebics"), - ), - "https://iso20022test.credit-suisse.com/ebicsweb/ebicsweb" => ( - Some("Credit Suisse isoTest"), - Some("https://iso20022test.credit-suisse.com/user/settings/ebics"), - ), - "https://ebics.postfinance.ch/ebics/ebics.aspx" => (Some("PostFinance"), None), - _ => (None, None), - }; - let test = settings.is_some(); - let payto = match cfg.currency.as_ref() { - "CHF" => { - "payto://iban/GENODED1SPW/DE48330605920000686018?receiver-name=Christian%20Grothoff" - } - "EUR" => { - "payto://iban/GENODED1SPW/DE48330605920000686018?receiver-name=Christian%20Grothoff" - } - _ => todo!("{}", cfg.currency), - }; - let payto = TransferIbanPayto::from_str(payto).unwrap(); - let ebics_log = format!("--debug-ebics testbench/test/{}", cmd.platform); - - let mut line_editor = Reedline::create().with_history(history); - let prompt = BenchPrompt { - prompt: format!( - "{:?} {}{}", - cmd.component, - cmd.platform, - std::fmt::from_fn(|f| { - if let Some(name) = &name { - write!(f, " {name}") - } else { - Ok(()) - } - }) - ), - }; - loop { - // Automatic setup - { - let client = load_client_keys(ebics.client.as_ref()).unwrap(); - let bank = load_bank_keys(ebics.bank.as_ref()).unwrap(); - if settings.is_none() && client.is_none() { - msg("Manual setup is required for non test environment") - } else if client - .map(|it| !it.submitted_ini || !it.submitted_hia) - .unwrap_or(true) - || bank.map(|it| !it.accepted).unwrap_or(true) - { - step("Run EBICS setup"); - if !nexus_cmd(&cfg.cfg, &format!("ebics-setup {ebics_log}")).await { - if let Some(settings) = settings { - let client = load_client_keys(ebics.client.as_ref()).unwrap(); - if client - .map(|it| !it.submitted_ini || !it.submitted_hia) - .unwrap_or(true) - { - msg(format_args!( - "Got to {settings} and click on 'Reset EBICS user'" - )) - } else { - msg(format_args!( - "Got to {settings} and click on 'Activate EBICS user'" - )) - } - } else { - msg("Activate your keys at your bank") - } - } - } - } - let Signal::Success(buf) = line_editor.read_line(&prompt).unwrap() else { - break; - }; - match NexusCmd::try_parse_from(buf.split_whitespace()) { - Ok(cmd) => match cmd { - NexusCmd::Setup => { - nexus_cmd(&cfg.cfg, &format!("ebics-setup {ebics_log}")).await; - } - NexusCmd::ResetDb => { - nexus_cmd(&cfg.cfg, "dbinit -r").await; - } - NexusCmd::Fetch { raw_args } => { - nexus_cmd( - &cfg.cfg, - &format!( - "ebics-fetch {ebics_log} {}", - shlex::try_join(raw_args.iter().map(|it| it.as_str())).unwrap() - ), - ) - .await; - } - NexusCmd::Submit { raw_args } => { - nexus_cmd( - &cfg.cfg, - &format!( - "ebics-submit {ebics_log} {}", - shlex::try_join(raw_args.iter().map(|it| it.as_str())).unwrap() - ), - ) - .await; - } - NexusCmd::Tx => { - nexus_cmd( - &cfg.cfg, - &format!( - "initiate-payment --amount={}:0.1 --subject=\"single {}\" {payto}", - cfg.currency, - Timestamp::now() - ), - ) - .await; - } - NexusCmd::List { raw_args } => { - nexus_cmd( - &cfg.cfg, - &format!( - "list {}", - shlex::try_join(raw_args.iter().map(|it| it.as_str())).unwrap() - ), - ) - .await; - } - NexusCmd::ResetKeys => { - if test { - std::fs::remove_file(&ebics.client)?; - } - std::fs::remove_file(&ebics.bank)?; - } - NexusCmd::TxCheck => { - nexus_cmd(&cfg.cfg, &format!("testing tx-check {ebics_log}")).await; - } - NexusCmd::Wss => { - nexus_cmd(&cfg.cfg, &format!("testing wss {ebics_log}")).await; - } - NexusCmd::Exit => return Ok(()), - }, - Err(e) => { - println!("{e}"); - } - } - } - Ok(()) -} - -struct BenchPrompt { - prompt: String, -} - -impl Prompt for BenchPrompt { - fn render_prompt_left(&self) -> Cow<'_, str> { - Cow::Borrowed(&self.prompt) - } - - fn render_prompt_right(&self) -> Cow<'_, str> { - Cow::Borrowed("") - } - - fn render_prompt_indicator(&self, _: reedline::PromptEditMode) -> Cow<'_, str> { - Cow::Borrowed(">") - } - - fn render_prompt_multiline_indicator(&self) -> Cow<'_, str> { - Cow::Borrowed(":") - } - - fn render_prompt_history_search_indicator( - &self, - _: reedline::PromptHistorySearch, - ) -> Cow<'_, str> { - Cow::Borrowed(">") - } -} - -#[cfg(test)] -mod test { - use std::{fs::Permissions, io::Write, os::unix::fs::PermissionsExt as _, path::PathBuf}; - - use libeufin_ebics::keys::{BankKeys, ClientKeys, persist_bank_keys, persist_client_keys}; - use libeufin_nexus::{ - Args, CONFIG_SOURCE, - config::{NexusCfg, NexusKeysCfg}, - run, - }; - use taler_common::config::Config; - - pub fn clap_parse<T: clap::Parser>(cmd: &str) -> T { - let parts = shlex::split(cmd).unwrap(); - let args = std::iter::once("bin").chain(parts.iter().map(|it| it.as_str())); - - T::try_parse_from(args).unwrap() - } - - /** Test error format related to the keying process */ - #[tokio::test] - pub async fn keys() { - let nexus_cmds = ["ebics-submit", "ebics-fetch"]; - let nexus_all_cmds = ["ebics-submit", "ebics-fetch", "ebics-setup"]; - let conf = "../../testbench/conf/cli.conf"; - let cfg = Config::from_file(CONFIG_SOURCE, Some(conf)).unwrap(); - let cfg = NexusCfg::parse(cfg).unwrap(); - let NexusKeysCfg { client, bank } = cfg.keys().unwrap(); - - let c_path = &PathBuf::from(client); - let b_path = &PathBuf::from(bank); - std::fs::create_dir_all(c_path.parent().unwrap()).unwrap(); - std::fs::create_dir_all(b_path.parent().unwrap()).unwrap(); - - let check_cmds = async |msg: &str| { - for cmd in nexus_cmds { - let tmp: Args = clap_parse(cmd); - let res = run(cfg.cfg.clone(), tmp.cmd).await.unwrap_err(); - assert_eq!( - res.to_string(), - msg.replace("SETUPCMD", "libeufin-nexus ebics-setup") - ); - } - }; - let check_all_cmds = async |msg: &str| { - for cmd in nexus_all_cmds { - let tmp: Args = clap_parse(cmd); - let res = run(cfg.cfg.clone(), tmp.cmd).await.unwrap_err(); - assert_eq!( - res.to_string(), - msg.replace("SETUPCMD", "libeufin-nexus ebics-setup") - ); - } - }; - - // Missing client keys - std::fs::remove_file(c_path).ok(); - check_cmds(&format!( - "Missing client private keys file at '{client}', run 'SETUPCMD' first" - )) - .await; - // Empty client file - let mut cf = std::fs::File::create(c_path).unwrap(); - check_all_cmds(&format!( - "Could not read client private keys at '{client}': unexpected end of file" - )) - .await; - // Bad client json - cf.write_all(b"CORRUPTION").unwrap(); - check_all_cmds(&format!( - "Could not read client private keys at '{client}': invalid data" - )) - .await; - // Missing permission - cf.set_permissions(Permissions::from_mode(0o000)).unwrap(); - check_all_cmds(&format!( - "Could not read client private keys at '{client}': permission denied" - )) - .await; - // Unfinished client - let client_keys = ClientKeys::generate().unwrap(); - persist_client_keys(&client_keys, c_path).unwrap(); - check_cmds("Unsubmitted client private keys, run 'SETUPCMD' first").await; - - // Missing bank keys - persist_client_keys( - &ClientKeys { - submitted_ini: true, - submitted_hia: true, - ..client_keys - }, - c_path, - ) - .unwrap(); - std::fs::remove_file(b_path).ok(); - check_cmds(&format!( - "Missing bank public keys file at '{bank}', run 'SETUPCMD' first" - )) - .await; - // Empty bank file - let mut bf = std::fs::File::create(b_path).unwrap(); - check_all_cmds(&format!( - "Could not read bank public keys at '{bank}': unexpected end of file" - )) - .await; - // Bad bank json - bf.write_all(b"CORRUPTION").unwrap(); - check_all_cmds(&format!( - "Could not read bank public keys at '{bank}': invalid data" - )) - .await; - // Missing permission - bf.set_permissions(Permissions::from_mode(0o000)).unwrap(); - check_all_cmds(&format!( - "Could not read bank public keys at '{bank}': permission denied" - )) - .await; - // Unfinished bank - let bank_keys = BankKeys::generate(); - persist_bank_keys(&bank_keys, b_path).unwrap(); - check_cmds("Unaccepted bank public keys, run 'SETUPCMD' until accepting the bank keys") - .await; - } -} diff --git a/debian/control b/debian/control @@ -5,44 +5,39 @@ Maintainer: Taler Systems SA <deb@taler.net> Uploaders: Christian Grothoff <grothoff@gnu.org>, Florian Dold <dold@taler.net> Build-Depends: debhelper-compat (= 13), - default-jdk-headless | java-runtime-headless (>= 17) + rustup Standards-Version: 4.7.2 Vcs-Git: https://git.taler.net/libeufin.git Homepage: https://taler.net/ Package: libeufin-common -Architecture: all +Architecture: any Depends: ${misc:Depends} Description: Common files for other libeufin packages. Package: libeufin-bank -Architecture: all +Architecture: any Depends: - default-jre-headless | java-runtime-headless (>= 17), libeufin-common (= ${binary:Version}), - libeufin-bank-webui, - ${misc:Depends} + ${misc:Depends}, ${shlibs:Depends} Recommends: nginx | apache2 | httpd, postgresql (>= 14.0) Description: Software package to provide a regional bank with optional EBICS access. Package: libeufin-nexus -Architecture: all +Architecture: any Depends: - default-jre-headless | java-runtime-headless (>= 17), libeufin-common (= ${binary:Version}), - ${misc:Depends} + ${misc:Depends}, ${shlibs:Depends} Recommends: nginx | apache2 | httpd, postgresql (>= 14.0) Description: Software package to access a bank accounts via the EBICS protocol. -Package: libeufin-ebisync -Architecture: all -Depends: - default-jre-headless | java-runtime-headless (>= 17), - ${misc:Depends} -Recommends: - postgresql (>= 14.0) -Description: Software package to sync ISO20022 files via the EBICS protocol. +#Package: libeufin-ebisync +#Architecture: any +#Depends: ${misc:Depends}, ${shlibs:Depends} +#Recommends: +# postgresql (>= 14.0) +#Description: Software package to sync ISO20022 files via the EBICS protocol. diff --git a/debian/libeufin-bank.install b/debian/libeufin-bank.install @@ -1,7 +1,9 @@ debian/etc/libeufin/libeufin-bank.conf etc/libeufin/ debian/etc/nginx/sites-available/libeufin-bank etc/nginx/sites-available/ debian/etc/apache2/sites-available/libeufin-bank.conf etc/apache2/sites-available/ -libeufin-bank/build/install/libeufin-bank-shadow/bin/libeufin-bank usr/bin/ +debian/etc/libeufin/settings.json etc/libeufin/ + +target/release/libeufin-bank usr/bin/ contrib/libeufin-bank-dbinit usr/bin/ contrib/libeufin-tan-*.sh usr/bin/ @@ -11,9 +13,6 @@ contrib/spa/* usr/share/libeufin/spa contrib/bank.conf usr/share/libeufin/config.d/ -# FIXME: This name should be prefixed! -libeufin-bank/build/install/libeufin-bank-shadow/lib/libeufin-bank-all.jar usr/lib/ - doc/prebuilt/man/libeufin-bank.1 usr/share/man/man1 doc/prebuilt/man/libeufin-bank.conf.5 usr/share/man/man5 diff --git a/debian/libeufin-ebisync.install b/debian/libeufin-ebisync.install @@ -2,7 +2,7 @@ debian/etc/libeufin-ebisync/* etc/libeufin-ebisync/ debian/etc/nginx/sites-available/libeufin-ebisync etc/nginx/sites-available/ debian/etc/apache2/sites-available/libeufin-ebisync.conf etc/apache2/sites-available/ -libeufin-ebisync/build/install/libeufin-ebisync-shadow/bin/libeufin-ebisync usr/bin/ +target/release/libeufin-ebisync usr/bin/ contrib/libeufin-ebisync-dbconfig usr/bin/ database-versioning/versioning.sql usr/share/libeufin-ebisync/sql/ @@ -12,8 +12,6 @@ libeufin-ebisync/src/spa/* usr/share/libeufin-ebisync/spa libeufin-ebisync/ebisync.conf usr/share/libeufin-ebisync/config.d/ -libeufin-ebisync/build/install/libeufin-ebisync-shadow/lib/libeufin-ebisync-all.jar usr/lib/ - doc/prebuilt/man/libeufin-ebisync.1 usr/share/man/man1 doc/prebuilt/man/libeufin-ebisync.conf.5 usr/share/man/man5 diff --git a/debian/libeufin-nexus.install b/debian/libeufin-nexus.install @@ -1,15 +1,12 @@ debian/etc/libeufin/libeufin-nexus.conf etc/libeufin/ -libeufin-nexus/build/install/libeufin-nexus-shadow/bin/libeufin-nexus usr/bin/ +target/release/libeufin-nexus usr/bin/ contrib/libeufin-nexus-dbinit usr/bin/ database-versioning/libeufin-nexus*.sql usr/share/libeufin/sql/ contrib/nexus.conf usr/share/libeufin/config.d/ -# FIXME: This name should be prefixed! -libeufin-nexus/build/install/libeufin-nexus-shadow/lib/libeufin-nexus-all.jar usr/lib/ - doc/prebuilt/man/libeufin-nexus.1 usr/share/man/man1 doc/prebuilt/man/libeufin-nexus.conf.5 usr/share/man/man5 diff --git a/debian/rules b/debian/rules @@ -1,46 +1,24 @@ #!/usr/bin/make -f -include /usr/share/dpkg/default.mk - -SHELL := sh -e +%: + dh $@ --no-parallel -export GRADLE_USER_HOME = .gradle -LIBEUFIN_HOME = /usr/share/libeufin -DEV = -FULLVER = $(DEB_VERSION_UPSTREAM)$(DEV) +override_dh_auto_configure: + rustup default stable -%: - dh ${@} --no-parallel +override_dh_auto_build: + cargo build --release --bin libeufin-bank --bin libeufin-nexus override_dh_auto_test: true -override_dh_auto_configure: - true - override_dh_auto_install: true -override_dh_auto_build: - make build - -# Override this step because it's very slow and likely -# unnecessary for us. -override_dh_strip_nondeterminism: - true - override_dh_installsystemd: - # Need to specify units manually, since we have multiple - # and dh_installsystemd by default only looks for "<package>.service". - dh_installsystemd -p libeufin-bank --no-start --no-enable --no-stop-on-upgrade --name=libeufin-bank-gc - dh_installsystemd -p libeufin-bank --no-start --no-enable --no-stop-on-upgrade --name=libeufin-bank - dh_installsystemd -p libeufin-nexus --no-start --no-enable --no-stop-on-upgrade --name=libeufin-nexus-ebics-submit - dh_installsystemd -p libeufin-nexus --no-start --no-enable --no-stop-on-upgrade --name=libeufin-nexus-ebics-fetch - dh_installsystemd -p libeufin-nexus --no-start --no-enable --no-stop-on-upgrade --name=libeufin-nexus-httpd - dh_installsystemd -p libeufin-ebisync --no-start --no-enable --no-stop-on-upgrade --name=libeufin-ebisync-fetch - dh_installsystemd -p libeufin-ebisync --no-start --no-enable --no-stop-on-upgrade --name=libeufin-ebisync-httpd - # final invocation to generate daemon reload - dh_installsystemd --no-start --no-enable --no-stop-on-upgrade - -get-orig-source: - uscan --force-download --rename + dh_installsystemd --no-start --no-enable --no-stop-on-upgrade -p libeufin-bank --name libeufin-bank-gc + dh_installsystemd --no-start --no-enable --no-stop-on-upgrade -p libeufin-bank --name libeufin-bank + dh_installsystemd --no-start --no-enable --no-stop-on-upgrade -p libeufin-nexus --name libeufin-nexus-ebics-submit + dh_installsystemd --no-start --no-enable --no-stop-on-upgrade -p libeufin-nexus --name libeufin-nexus-ebics-fetch + dh_installsystemd --no-start --no-enable --no-stop-on-upgrade -p libeufin-nexus --name libeufin-nexus-httpd + dh_installsystemd diff --git a/gradle.properties b/gradle.properties @@ -1,6 +0,0 @@ -kotlin.code.style=official -org.gradle.caching=true -#org.gradle.parallel=true -#org.gradle.configuration-cache=true -org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled -org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true -\ No newline at end of file diff --git a/gradlew.bat b/gradlew.bat @@ -1,82 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem -@rem SPDX-License-Identifier: Apache-2.0 -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem gradlew startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables, and ensure extensions are enabled -setlocal EnableExtensions - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -"%COMSPEC%" /c exit 1 - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -"%COMSPEC%" /c exit 1 - -:execute -@rem Setup the command line - - - -@rem Execute gradlew -@rem endlocal doesn't take effect until after the line is parsed and variables are expanded -@rem which allows us to clear the local environment before executing the java command -endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel - -:exitWithErrorLevel -@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts -"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/crates/libeufin-bank/Cargo.toml b/libeufin-bank/Cargo.toml diff --git a/libeufin-bank/build.gradle b/libeufin-bank/build.gradle @@ -1,71 +0,0 @@ -plugins { - id("kotlin") - id("application") - id("com.gradleup.shadow") version "$shadow_version" - id("org.jetbrains.kotlin.plugin.serialization") version "$kotlin_version" -} - -version = rootProject.version - -java { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 -} - -compileKotlin.kotlinOptions.jvmTarget = "17" -compileTestKotlin.kotlinOptions.jvmTarget = "17" - -sourceSets.main.java.srcDirs = ["src/main/kotlin"] - -dependencies { - // Core language libraries - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version") - - implementation(project(":libeufin-common")) - - implementation("org.postgresql:postgresql:$postgres_version") - implementation("com.github.ajalt.clikt:clikt:$clikt_version") - implementation("com.github.ajalt.mordant:mordant:3.0.2") - - // Metrics - implementation("io.prometheus:prometheus-metrics-core:$prometheus_version") - implementation("io.prometheus:prometheus-metrics-instrumentation-jvm:$prometheus_version") - implementation("io.prometheus:prometheus-metrics-exposition-formats:$prometheus_version") - - implementation("io.ktor:ktor-server-core:$ktor_version") - implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor_version") - - // OpenAPI spec generation - implementation("io.github.smiley4:ktor-openapi:5.6.0") - implementation("io.github.smiley4:schema-kenerator-core:2.6.0") - - // UNIX domain sockets support (used to connect to PostgreSQL) - implementation("com.kohlschutter.junixsocket:junixsocket-core:$junixsocket_version") - - testImplementation("org.jetbrains.kotlin:kotlin-test:$kotlin_version") - testImplementation("io.ktor:ktor-server-test-host:$ktor_version") - - testImplementation(project(":libeufin-common")) -} - -application { - mainClass = "tech.libeufin.bank.MainKt" - // junixsocket loads its native library through System.loadLibrary(). - applicationDefaultJvmArgs = ["--enable-native-access=ALL-UNNAMED"] -} - -shadowJar { - version = "" - minimize { - // The OpenAPI schema generator discovers model properties at runtime. - exclude(dependency("org.jetbrains.kotlin:kotlin-reflect:.*")) - // Kotlin serialization - exclude(dependency("io.ktor:ktor-serialization-kotlinx-json:.*")) - // Postgres unix socket driver - exclude(dependency("com.kohlschutter.junixsocket:junixsocket-core:.*")) - // CLI - exclude(dependency("com.github.ajalt.mordant:mordant:.*")) - // Crypto - exclude(dependency("org.bouncycastle:.*")) - } -} diff --git a/libeufin-bank/conf/test.conf b/libeufin-bank/conf/test.conf @@ -15,4 +15,4 @@ tan_email = libeufin-tan-file.sh PWD_HASH_CONFIG = { "cost": 4 } [libeufin-bankdb-postgres] -CONFIG = postgresql:///libeufincheck +CONFIG = postgresql:///taler_rust_check diff --git a/libeufin-bank/conf/test_bonus.conf b/libeufin-bank/conf/test_bonus.conf @@ -9,4 +9,4 @@ ALLOW_ACCOUNT_DELETION = yes PWD_HASH_CONFIG = { "cost": 4 } [libeufin-bankdb-postgres] -CONFIG = postgresql:///libeufincheck -\ No newline at end of file +CONFIG = postgresql:///taler_rust_check +\ No newline at end of file diff --git a/libeufin-bank/conf/test_no_conversion.conf b/libeufin-bank/conf/test_no_conversion.conf @@ -8,4 +8,4 @@ ALLOW_ACCOUNT_DELETION = yes PWD_HASH_CONFIG = { "cost": 4 } [libeufin-bankdb-postgres] -CONFIG = postgresql:///libeufincheck -\ No newline at end of file +CONFIG = postgresql:///taler_rust_check +\ No newline at end of file diff --git a/libeufin-bank/conf/test_no_password_check.conf b/libeufin-bank/conf/test_no_password_check.conf @@ -9,4 +9,4 @@ PWD_HASH_CONFIG = { "cost": 4 } PWD_CHECK = no [libeufin-bankdb-postgres] -CONFIG = postgresql:///libeufincheck -\ No newline at end of file +CONFIG = postgresql:///taler_rust_check +\ No newline at end of file diff --git a/libeufin-bank/conf/test_restrict.conf b/libeufin-bank/conf/test_restrict.conf @@ -9,4 +9,4 @@ FIAT_CURRENCY = EUR PWD_HASH_CONFIG = { "cost": 4 } [libeufin-bankdb-postgres] -CONFIG = postgresql:///libeufincheck -\ No newline at end of file +CONFIG = postgresql:///taler_rust_check +\ No newline at end of file diff --git a/libeufin-bank/conf/test_tan_err.conf b/libeufin-bank/conf/test_tan_err.conf @@ -14,7 +14,7 @@ tan_sms = libeufin-tan-fail.sh PWD_HASH_CONFIG = { "cost": 4 } [libeufin-bankdb-postgres] -CONFIG = postgresql:///libeufincheck +CONFIG = postgresql:///taler_rust_check [nexus-ebics] currency = EUR diff --git a/libeufin-bank/conf/test_with_fees.conf b/libeufin-bank/conf/test_with_fees.conf @@ -19,4 +19,4 @@ max_wire_transfer_amount = KUDOS:100 PWD_HASH_CONFIG = { "cost": 4 } [libeufin-bankdb-postgres] -CONFIG = postgresql:///libeufincheck +CONFIG = postgresql:///taler_rust_check diff --git a/libeufin-bank/conf/test_x_taler_bank.conf b/libeufin-bank/conf/test_x_taler_bank.conf @@ -12,4 +12,4 @@ ALLOW_EDIT_CASHOUT_PAYTO_URI = yes PWD_HASH_CONFIG = { "cost": 4 } [libeufin-bankdb-postgres] -CONFIG = postgresql:///libeufincheck +CONFIG = postgresql:///taler_rust_check diff --git a/libeufin-bank/src/api.rs b/libeufin-bank/src/api.rs @@ -0,0 +1,934 @@ +/* +* This file is part of LibEuFin. +* Copyright (C) 2026 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/> +*/ + +use std::sync::Arc; + +use axum::{ + Json, Router, + extract::State, + response::{IntoResponse, Redirect, Response}, + routing::get, +}; +use serde::{Deserialize, Serialize}; +use sqlx::PgPool; +use taler_api::{ + api::TalerRouter as _, error::ApiResult, extract::Query, notification::NotificationChannel, +}; +use taler_common::{ + api::LibtoolVersion, + types::amount::{Amount, Currency}, +}; +use taler_macros::{EnumMeta, api_config}; +use tower_http::services::ServeDir; +use uuid::Uuid; + +use crate::{ + TanChannel, + api::{ + account::account_api, + cashout::cashout_api, + conversion::conversion_api, + prepared::prepared_api, + revenue::revenue_api, + tan::tan_api, + token::token_api, + tx::tx_api, + wire::wire_api, + withdrawal::{WithdrawalStatus, withdrawal_api}, + }, + auth::AdminRAuth, + config::{BankCfg, CurrencySpecification}, + db::{monitor, notification_listener}, +}; + +pub mod account; +pub mod cashout; +pub mod conversion; +pub mod prepared; +pub mod revenue; +pub mod tan; +pub mod token; +pub mod tx; +pub mod wire; +pub mod withdrawal; + +const IMPLEMENTATION: &str = "urn:net:taler:specs:libeufin-bank:taler-rust"; +const COREBANK_API_VERSION: LibtoolVersion = LibtoolVersion::new(12, 1, 0); +const CONVERSION_API_VERSION: LibtoolVersion = LibtoolVersion::new(2, 1, 1); +const INTEGRATION_API_VERSION: LibtoolVersion = LibtoolVersion::new(5, 1, 5); + +pub struct BankState { + pub db: PgPool, + pub cfg: BankCfg, + pub tx_channel: NotificationChannel<u64, i64>, + pub taler_out_channel: NotificationChannel<u64, i64>, + pub taler_in_channel: NotificationChannel<u64, i64>, + pub revenue_channel: NotificationChannel<u64, i64>, + pub withdrawal_channel: NotificationChannel<Uuid, WithdrawalStatus>, +} + +impl BankState { + pub async fn start(pool: PgPool, cfg: BankCfg) -> Self { + let tx_channel = NotificationChannel::new(); + let taler_in_channel = NotificationChannel::new(); + let taler_out_channel = NotificationChannel::new(); + let revenue_channel = NotificationChannel::new(); + let withdrawal_channel = NotificationChannel::new(); + tokio::spawn(notification_listener( + pool.clone(), + tx_channel.clone(), + taler_out_channel.clone(), + taler_in_channel.clone(), + revenue_channel.clone(), + withdrawal_channel.clone(), + )); + Self { + cfg, + db: pool, + tx_channel, + taler_in_channel, + taler_out_channel, + revenue_channel, + withdrawal_channel, + } + } +} + +#[api_config("taler-corebank")] +#[derive(Debug, Clone, Serialize)] +pub struct Config<'a> { + pub currency: Currency, + pub currency_specification: &'a CurrencySpecification, + pub base_url: &'a str, + pub bank_name: &'a str, + pub allow_conversion: bool, + pub allow_registrations: bool, + pub allow_deletions: bool, + pub allow_edit_name: bool, + pub allow_edit_cashout_payto_uri: bool, + pub default_debit_threshold: Amount, + pub supported_tan_channels: Vec<TanChannel>, + pub wire_type: &'a str, + pub wire_transfer_fees: Amount, + pub min_wire_transfer_amount: Amount, + pub max_wire_transfer_amount: Amount, +} + +#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, sqlx::Type, EnumMeta)] +#[sqlx(type_name = "stat_timeframe_enum")] +#[enum_meta(Str)] +#[allow(non_camel_case_types)] +pub enum Timeframe { + #[default] + hour, + day, + month, + year, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MonitorParams { + #[serde(default)] + pub timeframe: Timeframe, + pub date_s: Option<u64>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type")] +pub enum MonitorResponse { + #[serde(rename = "no-conversions", rename_all = "camelCase")] + Simple { + taler_in_count: u64, + taler_in_volume: Amount, + taler_out_count: u64, + taler_out_volume: Amount, + }, + #[serde(rename = "with-conversions", rename_all = "camelCase")] + Conversion { + cashin_count: u64, + cashin_regional_volume: Amount, + cashin_fiat_volume: Amount, + cashout_count: u64, + cashout_regional_volume: Amount, + cashout_fiat_volume: Amount, + taler_in_count: u64, + taler_in_volume: Amount, + taler_out_count: u64, + taler_out_volume: Amount, + }, +} + +async fn config(State(state): State<Arc<BankState>>) -> Response { + Json(Config { + name: (), + version: COREBANK_API_VERSION, + implementation: Some(IMPLEMENTATION), + currency: state.cfg.regional_currency, + currency_specification: &state.cfg.regional_currency_spec, + base_url: state.cfg.base_url.as_str(), + bank_name: &state.cfg.name, + allow_conversion: state.cfg.fiat.is_some(), + allow_registrations: state.cfg.allow_registration, + allow_deletions: state.cfg.allow_account_deletion, + allow_edit_name: state.cfg.allow_edit_name, + allow_edit_cashout_payto_uri: state.cfg.allow_edit_cashout, + default_debit_threshold: state.cfg.default_debt_limit, + supported_tan_channels: state.cfg.tan_channels.keys().copied().collect::<Vec<_>>(), + wire_type: state.cfg.wire_method.as_ref(), + wire_transfer_fees: state.cfg.wire_transfer_fees, + min_wire_transfer_amount: state.cfg.min_amount, + max_wire_transfer_amount: state.cfg.max_amount, + }) + .into_response() +} + +pub fn bank_api(state: Arc<BankState>) -> Router { + let router = Router::new().route("/config", get(config)); // TODO monitor + if let Some(path) = &state.cfg.spa_path { + router + .nest_service("/webui/", ServeDir::new(path)) + .route("/", get(async || Redirect::permanent("/webui/"))) + } else { + router + } + .route( + "/monitor", + get( + async |Query(params): Query<MonitorParams>, + _: AdminRAuth, + State(state): State<Arc<BankState>>| { + ApiResult::Ok(Json( + monitor( + &state.db, + &state.cfg.regional_currency, + state.cfg.fiat_currency(), + &params, + ) + .await?, + )) + }, + ), + ) + .merge(token_api()) + .merge(account_api()) + .merge(tx_api()) + .merge(withdrawal_api()) + .merge(cashout_api(state.clone())) + .merge(tan_api()) + .merge(conversion_api(state.clone())) + .merge(wire_api()) + .merge(prepared_api()) + .merge(revenue_api()) + .with_state(state) + .finalize() +} + +#[cfg(test)] +pub mod test { + use std::{collections::BTreeMap, fmt::Display, ops::Deref, sync::Arc}; + + use axum::{ + Router, + extract::{Request, State}, + http::{HeaderValue, Method, StatusCode, header::AUTHORIZATION}, + middleware::{self, Next}, + }; + use compact_str::CompactString; + use jiff::Timestamp; + use rand::{random_range, seq::IndexedRandom}; + use taler_api::db::BindHelper; + use taler_common::{ + api::{EddsaPublicKey, HashCode, ShortHashCode}, + config::Config, + db::dbinit, + error_code::ErrorCode, + types::{ + amount::{Amount, Decimal, decimal}, + payto::PaytoURI, + }, + }; + use taler_test_utils::{ + db::db_test_setup_manual, + json, + server::{TestRequest, TestResponse, TestServer as _}, + }; + use uuid::Uuid; + + use crate::{ + CONFIG_SOURCE, + api::{ + BankState, MonitorResponse, + account::{ + AccountData, Balance, CreditDebitInfo, create_admin_account, rand_iban_payto, + }, + bank_api, + conversion::{ConversionRateClassResponse, ConversionResponse}, + tan::ChallengeResponse, + withdrawal::BankAccountCreateWithdrawalResponse, + }, + config::BankCfg, + db::{self, account::CreationResult}, + mfa::TALER_CHALLENGE_IDS, + payto::BankPayto, + }; + + pub enum Auth { + Admin, + Optional, + UserOrAdmin, + UserOnly, + Token, + } + + pub struct BankTestCtx { + pub merchant_payto: BankPayto, + pub exchange_payto: BankPayto, + pub customer_payto: BankPayto, + pub unknown_payto: BankPayto, + pub tmp_payto: BankPayto, + pub admin_payto: BankPayto, + pub server: Router, + pub state: Arc<BankState>, + tokens: BTreeMap<CompactString, String>, + } + + impl BankTestCtx { + pub async fn new(conf: &str) -> Self { + let cfg = Config::from_file( + CONFIG_SOURCE, + Some(format!("conf/{conf}")), + ) + .unwrap(); + let cfg = BankCfg::parse(cfg).unwrap(); + let dir = cfg.db_cfg.sql_dir.as_ref(); + let (mut conn, db) = db_test_setup_manual(dir, "libeufin-bank").await; + dbinit(&mut conn, dir, "libeufin-bank", true).await.unwrap(); + dbinit(&mut conn, dir, "libeufin-nexus", true) + .await + .unwrap(); + let procedure = + std::fs::read_to_string(dir.join("libeufin-conversion-setup.sql")).unwrap(); + sqlx::raw_sql(&procedure).execute(&mut *conn).await.unwrap(); + drop(conn); + + let state = Arc::new(BankState::start(db.clone(), cfg).await); + let server = bank_api(state.clone()); + + let merchant_payto = db::account::create( + &db, + &state.cfg.ctx, + &state.cfg.pw_crypto, + "merchant", + "merchant-password", + "Merchant", + None, + None, + None, + &rand_iban_payto().into_inner().into(), + false, + false, + Decimal::new(10, 0).to_amount(&state.cfg.regional_currency), + Amount::zero(&state.cfg.regional_currency), + &[], + false, + None, + ) + .await + .unwrap() + .assert_success(); + let exchange_payto = db::account::create( + &db, + &state.cfg.ctx, + &state.cfg.pw_crypto, + "exchange", + "exchange-password", + "Exchange", + None, + None, + None, + &rand_iban_payto().into_inner().into(), + false, + true, + Decimal::new(10, 0).to_amount(&state.cfg.regional_currency), + Amount::zero(&state.cfg.regional_currency), + &[], + false, + None, + ) + .await + .unwrap() + .assert_success(); + let customer_payto = db::account::create( + &db, + &state.cfg.ctx, + &state.cfg.pw_crypto, + "customer", + "customer-password", + "Customer", + None, + None, + None, + &rand_iban_payto().into_inner().into(), + false, + false, + Decimal::new(10, 0).to_amount(&state.cfg.regional_currency), + Amount::zero(&state.cfg.regional_currency), + &[], + false, + None, + ) + .await + .unwrap() + .assert_success(); + + let res = create_admin_account(&db, &state.cfg, Some("admin-password")) + .await + .unwrap(); + + let admin_payto = match res { + CreationResult::Success(payto) => payto, + _ => unreachable!(), + }; + + let mut ctx = BankTestCtx { + merchant_payto: merchant_payto.into(), + exchange_payto: exchange_payto.into(), + customer_payto: customer_payto.into(), + unknown_payto: rand_iban_payto().convert(), + tmp_payto: rand_iban_payto().convert(), + admin_payto: admin_payto.into(), + server, + state, + tokens: BTreeMap::new(), + }; + ctx.cache_tokens(&["admin", "merchant", "exchange", "customer"]) + .await; + + if ctx.state.cfg.fiat.is_some() { + // Set conversion rates + ctx.post_admin("/conversion-info/conversion-rate") + .json(json!({ + "cashin_ratio" :"0.8", + "cashin_fee" :"KUDOS:0.02", + "cashin_tiny_amount" :"KUDOS:0.01", + "cashin_rounding_mode" :"nearest", + "cashin_min_amount" :"EUR:0", + "cashout_ratio" :"1.26", + "cashout_fee" :"EUR:0.003", + "cashout_tiny_amount" :"EUR:0.01", + "cashout_rounding_mode" :"zero", + "cashout_min_amount" :"KUDOS:0.1" + })) + .await + .assert_no_content(); + ctx.create_conversion_rate_class().await; + } + + ctx + } + + pub async fn swap_cfg(mut self, conf: &str) -> Self { + let cfg = Config::from_file( + CONFIG_SOURCE, + Some(format!("conf/{conf}")), + ) + .unwrap(); + + let state = Arc::new( + BankState::start(self.state.db.clone(), BankCfg::parse(cfg).unwrap()).await, + ); + self.server = bank_api(state.clone()); + self.state = state; + self + } + + fn pw_auth(req: TestRequest, username: Option<&str>) -> TestRequest { + let username: CompactString = username + .unwrap_or_else(|| Self::extract_username(req.url.path())) + .into(); + req.basic_auth(&username, &format!("{username}-password")) + } + + fn extract_username(path: &str) -> &str { + if path.contains("admin") { + "admin" + } else { + path.split('/').nth(2).unwrap() + } + } + + pub async fn cache_tokens(&mut self, usernames: &[&'static str]) { + let tasks = usernames + .iter() + .map(|username| { + let username = CompactString::from(*username); + async { + let res = Self::pw_auth( + self.server.post(format!("/accounts/{username}/token")), + Some("admin"), + ) + .json(json!({ + "scope": "readwrite", + "duration": { + "d_us": "forever" + } + })) + .await + .assert_ok_json::<serde_json::Value>(); + let token = res["access_token"].as_str().unwrap(); + (username, format!("Bearer {token}")) + } + }) + .collect::<Vec<_>>(); + for (username, token) in futures::future::join_all(tasks).await { + self.tokens.insert(username, token); + } + } + + pub async fn admin_router(&self) -> Router { + self.server.clone().layer(middleware::from_fn_with_state( + Arc::new(self.tokens.clone()), + async |State(tokens): State<Arc<BTreeMap<CompactString, String>>>, + mut req: Request, + next: Next| { + let path = req.uri().path(); + if path.starts_with("/accounts") && !path.contains("admin") { + let username = Self::extract_username(req.uri().path()); + let header = HeaderValue::from_str(&tokens[username]).unwrap(); + req.headers_mut().insert(AUTHORIZATION, header); + } else { + req.headers_mut().insert( + AUTHORIZATION, + HeaderValue::from_str(&tokens["admin"]).unwrap(), + ); + } + next.run(req).await + }, + )) + } + + fn requesta( + &self, + method: Method, + path: impl AsRef<str>, + username: Option<&str>, + ) -> TestRequest { + let path = path.as_ref(); + let username = username.unwrap_or_else(|| Self::extract_username(path)); + let token = &self.tokens[username]; + self.server + .request(method, path) + .header(AUTHORIZATION, token) + } + + pub fn postpw(&self, path: impl AsRef<str>) -> TestRequest { + Self::pw_auth(self.server.request(Method::POST, path), None) + } + + pub fn geta(&self, path: impl AsRef<str>) -> TestRequest { + self.requesta(Method::GET, path, None) + } + + pub fn posta(&self, path: impl AsRef<str>) -> TestRequest { + self.requesta(Method::POST, path, None) + } + + pub fn patcha(&self, path: impl AsRef<str>) -> TestRequest { + self.requesta(Method::PATCH, path, None) + } + + pub fn deletea(&self, path: impl AsRef<str>) -> TestRequest { + self.requesta(Method::DELETE, path, None) + } + + pub fn get_admin(&self, path: impl AsRef<str>) -> TestRequest { + self.requesta(Method::GET, path, Some("admin")) + } + + pub fn post_admin(&self, path: impl AsRef<str>) -> TestRequest { + self.requesta(Method::POST, path, Some("admin")) + } + + pub fn patch_admin(&self, path: impl AsRef<str>) -> TestRequest { + self.requesta(Method::PATCH, path, Some("admin")) + } + + pub fn delete_admin(&self, path: impl AsRef<str>) -> TestRequest { + self.requesta(Method::DELETE, path, Some("admin")) + } + + pub async fn auth_routine(&self, method: Method, path: impl AsRef<str>, auth: Auth) { + let path = path.as_ref(); + // Bad header + self.request(method.clone(), path) + .header(AUTHORIZATION, "WTF") + .await + .assert_error(ErrorCode::GENERIC_UNAUTHORIZED); + + if !matches!(auth, Auth::Token) { + if !matches!(auth, Auth::Optional) { + // No header + self.request(method.clone(), path) + .await + .assert_error_status( + ErrorCode::GENERIC_PARAMETER_MISSING, + StatusCode::UNAUTHORIZED, + ); + } + + // Other account + self.requesta(method.clone(), path, Some("customer")) + .await + .assert_error(ErrorCode::GENERIC_FORBIDDEN); + } + + match auth { + Auth::Admin => { + self.requesta(method.clone(), path, Some("merchant")) + .await + .assert_error(ErrorCode::GENERIC_FORBIDDEN); + } + Auth::UserOrAdmin | Auth::Token | Auth::Optional => {} + Auth::UserOnly => { + self.requesta(method.clone(), path, Some("admin")) + .await + .assert_error(ErrorCode::GENERIC_FORBIDDEN); + } + } + } + + pub async fn fill_tan_info(&self, username: &str) { + self.patch_admin(format!("/accounts/{username}")) + .json(json!({ + "contact_data": { + "phone": format!("+{}", random_range(0..10000)) + }, + "tan_channel": "sms" + })) + .await + .assert_no_content(); + } + + pub async fn fill_cashout_info(&self, username: &str) { + self.patch_admin(format!("/accounts/{username}")) + .json(json!({ + "cashout_payto_uri": self.unknown_payto, + })) + .await + .assert_no_content(); + } + + pub async fn tmp_payto(&mut self) -> PaytoURI { + self.tmp_payto = rand_iban_payto().convert(); + self.tmp_payto.as_uri() + } + + pub async fn tx_s(&self, from: &str, amount: &str, to: &str, subject: impl Display) { + let payto = match to { + "admin" => &self.admin_payto, + "merchant" => &self.merchant_payto, + "customer" => &self.customer_payto, + "exchange" => &self.exchange_payto, + _ => &self.tmp_payto, + }; + self.posta(format!("/accounts/{from}/transactions")) + .json(json!({ + "payto_uri": format!("{payto}?message={subject}"), + "amount": format!("{}:{amount}", self.state.cfg.regional_currency), + })) + .await + .maybe_challenge(self) + .await + .assert_ok(); + } + + pub async fn tx(&self, from: &str, amount: &str, to: &str) { + self.tx_s(from, amount, to, "payout").await + } + + pub async fn transfer( + &self, + amount: &str, + payto: &BankPayto, + metadata: Option<CompactString>, + ) { + self.posta("/accounts/exchange/taler-wire-gateway/transfer") + .json({ + json!({ + "request_uid": HashCode::rand(), + "amount": format!("{}:{amount}", self.state.cfg.regional_currency), + "exchange_base_url": "http://exchange.example.com/", + "wtid": ShortHashCode::rand(), + "credit_account": payto, + "metadata": metadata, + }) + }) + .await + .assert_ok() + } + + /** Perform a taler incoming transaction of [amount] from merchant to exchange */ + pub async fn add_incoming(&self, amount: &str) { + self.post_admin("/accounts/exchange/taler-wire-gateway/admin/add-incoming") + .json({ + json!({ + "amount": format!("{}:{amount}", self.state.cfg.regional_currency), + "reserve_pub": EddsaPublicKey::rand(), + "debit_account": self.merchant_payto + }) + }) + .await + .assert_ok() + } + + pub async fn add_kyc(&self, amount: &str) { + self.post_admin("/accounts/exchange/taler-wire-gateway/admin/add-kycauth") + .json({ + json!({ + "amount": format!("{}:{amount}", self.state.cfg.regional_currency), + "account_pub": EddsaPublicKey::rand(), + "debit_account": self.merchant_payto + }) + }) + .await + .assert_ok() + } + + pub async fn cashout(&self, amount: &str) { + let amount = format!("{}:{amount}", self.state.cfg.regional_currency); + self.posta("/accounts/customer/cashouts") + .json({ + json!({ + "request_uid": ShortHashCode::rand(), + "amount_debit": amount, + "amount_credit": self.convert(&amount).await + }) + }) + .await + .assert_ok() + } + + pub async fn cashin(&self, amount: &str) { + sqlx::query("SELECT 0 FROM cashin($1, $2, $3, $4)") + .bind_timestamp(&Timestamp::now()) + .bind(ShortHashCode::rand()) + .bind(decimal(amount)) + .bind("") + .fetch_one(&self.state.db) + .await + .unwrap(); + } + + pub async fn withdrawal(&self, amount: &str) { + let uuid = self + .posta("/accounts/merchant/withdrawals") + .json({ + json!({ + "amount": format!("{}:{amount}", self.state.cfg.regional_currency) + }) + }) + .await + .assert_ok_json::<BankAccountCreateWithdrawalResponse>() + .withdrawal_id; + self.withdraw_select(uuid).await; + self.posta(format!("/accounts/merchant/withdrawals/{uuid}/confirm")) + .json(json!({})) + .await + .assert_no_content(); + } + + pub async fn withdraw_select(&self, uuid: Uuid) -> EddsaPublicKey { + let key = EddsaPublicKey::rand(); + self.post(format!("/taler-integration/withdrawal-operation/{uuid}")) + .json(json!({ + "reserve_pub": key, + "selected_exchange": self.exchange_payto + })) + .await + .assert_ok(); + key + } + + pub async fn create_conversion_rate_class(&self) -> u64 { + self.post_admin("/conversion-rate-classes") + .json(json!({ + "name": format!("Gen class {}", Timestamp::now()) + })) + .await + .assert_ok_json::<ConversionRateClassResponse>() + .conversion_rate_class_id + } + + pub async fn convert(&self, amount: &str) -> Amount { + self.get(format!( + "/conversion-info/cashout-rate?amount_debit={amount}" + )) + .await + .assert_ok_json::<ConversionResponse>() + .amount_credit + } + + /** Set [account] debit threshold to [maxDebt] amount */ + pub async fn set_max_debt(&self, username: &str, amount: &str) { + self.patch_admin(format!("/accounts/{username}")) + .json(json!({ + "debit_threshold": format!("{}:{amount}", self.state.cfg.regional_currency) + })) + .await + .assert_no_content(); + } + + /** Check [account] balance is [amount], [amount] is prefixed with + for credit and - for debit */ + pub async fn assert_balance(&self, username: &str, expected: &str) { + let res: AccountData = self + .get_admin(format!("/accounts/{username}")) + .await + .assert_ok_json(); + let Balance { + amount, + credit_debit_indicator, + } = res.balance; + let prefix = match credit_debit_indicator { + CreditDebitInfo::credit => "", + CreditDebitInfo::debit => "-", + }; + pretty_assertions::assert_eq!(format!("{prefix}{}", amount.decimal()), expected); + } + } + + impl Deref for BankTestCtx { + type Target = Router; + + fn deref(&self) -> &Self::Target { + &self.server + } + } + + pub async fn bank_setup() -> BankTestCtx { + bank_setup_conf("test.conf").await + } + + pub async fn bank_setup_conf(conf: &str) -> BankTestCtx { + BankTestCtx::new(conf).await + } + + pub fn tan_code(info: &str) -> Option<CompactString> { + let path = format!("/tmp/tan-{}.txt", info); + let code = match std::fs::read_to_string(&path) { + Ok(f) => f, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => return None, + Err(e) => panic!("{:?}", e), + }; + std::fs::remove_file(path).unwrap(); + Some(code.split(' ').next().unwrap().into()) + } + + pub trait MfaRequest { + fn assert_challenge_check( + &self, + ctx: &BankTestCtx, + check: impl AsyncFnOnce(&ChallengeResponse), + ) -> impl std::future::Future<Output = Self>; + + fn assert_challenge(&self, ctx: &BankTestCtx) -> impl std::future::Future<Output = Self> { + self.assert_challenge_check(ctx, async |_| {}) + } + + fn maybe_challenge(self, ctx: &BankTestCtx) -> impl std::future::Future<Output = Self>; + } + + impl MfaRequest for TestResponse { + async fn assert_challenge_check( + &self, + ctx: &BankTestCtx, + check: impl AsyncFnOnce(&ChallengeResponse), + ) -> TestResponse { + let res: ChallengeResponse = self.assert_accepted_json(); + let username = self.uri.path().split('/').nth(2).unwrap(); + + let challenges = if res.combi_and { + &res.challenges + } else { + std::slice::from_ref(res.challenges.choose(&mut rand::rng()).unwrap()) + }; + + for challenge in challenges { + ctx.posta(format!( + "/accounts/{username}/challenge/{}", + challenge.challenge_id + )) + .await + .assert_ok(); + } + check(&res).await; + + for challenge in challenges { + let code = tan_code(&challenge.tan_info).unwrap(); + ctx.posta(format!( + "/accounts/{username}/challenge/{}/confirm", + challenge.challenge_id + )) + .json(json!({ "tan": code })) + .await + .assert_no_content(); + } + // Recover body from request + let ids = res + .challenges + .into_iter() + .map(|it| it.challenge_id) + .collect::<Vec<_>>() + .join(", "); + ctx.requesta(self.method.clone(), self.uri.path(), Some(username)) + .header(TALER_CHALLENGE_IDS, ids) + .raw_json(self.req_body.clone()) + .await + } + + async fn maybe_challenge(self, ctx: &BankTestCtx) -> Self { + if self.status == StatusCode::ACCEPTED { + self.assert_challenge(ctx).await + } else { + self + } + } + } + + #[tokio::test] + async fn corebank() { + let ctx = bank_setup().await; + + ctx.auth_routine(Method::GET, "/monitor", Auth::Admin).await; + + ctx.get("/config").await.assert_ok(); + + ctx.get_admin("/monitor?timeframe=day&which=25") + .await + .assert_ok_json::<MonitorResponse>(); + ctx.get_admin("/monitor?timeframe=day=which=25") + .await + .assert_error(ErrorCode::GENERIC_PARAMETER_MALFORMED); + + ctx.fill_cashout_info("customer").await; + ctx.cashout("1").await; + ctx.get_admin("/monitor") + .await + .assert_ok_json::<MonitorResponse>(); + } +} diff --git a/libeufin-bank/src/api/account.rs b/libeufin-bank/src/api/account.rs @@ -0,0 +1,1929 @@ +/* +* This file is part of LibEuFin. +* Copyright (C) 2026 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/> +*/ + +use std::{ + fmt::Debug, + str::FromStr, + sync::{Arc, LazyLock}, +}; + +use axum::{ + Json, Router, + extract::State, + http::StatusCode, + response::{IntoResponse, NoContent}, + routing::{get, patch, post}, +}; +use compact_str::{CompactString, format_compact}; +use regex::Regex; +use serde::{Deserialize, Serialize}; +use sqlx::{Database, PgPool}; +use taler_api::{ + error::{ApiResult, bad_request, failure, failure_code, failure_status}, + extract::{Query, Req}, +}; +use taler_common::{ + api::params::{Page, PageParams, ParamsErr}, + error_code::ErrorCode::{self}, + types::{ + amount::Amount, + base32::Base32, + iban::{Country, IBAN}, + payto::{BankID, IbanPayto, Payto, PaytoURI}, + }, +}; +use taler_macros::EnumMeta; + +use crate::{ + TanChannel, + api::{BankState, conversion::ConversionRate}, + auth::{AdminRAuth, RegistrationAuth, UserAuth, UserRAuth, require_admin}, + config::{BankCfg, WireMethod}, + db::{ + self, + account::{ + CreationResult, DeletionResult, PatchAuthResult, PatchResult, by_username, page_admin, + page_public, reconfig, reconfig_password, + }, + }, + mfa::{AccountDeletionOp, AccountPasswordOp, AccountReconfigOp, MfaReq, Tans}, + payto::{FullBankPayto, LibeufinId, XTalerBank}, + pw::checkpw, +}; + +#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ChallengeContactData { + #[serde(default)] + pub email: Maybe<CompactString>, + #[serde(default)] + pub phone: Maybe<CompactString>, +} + +impl ChallengeContactData { + pub fn validate(&self) -> ApiResult<()> { + static EMAIL_PATTERN: LazyLock<Regex> = LazyLock::new(|| { + Regex::new("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,4}$").unwrap() + }); + static PHONE_PATTERN: LazyLock<Regex> = + LazyLock::new(|| Regex::new("^\\+?[0-9]+$").unwrap()); + + if let Maybe::Some(email) = &self.email + && !EMAIL_PATTERN.is_match(email) + { + return Err(bad_request(format_args!( + "email contact data '{email}' is malformed" + ))); + } + if let Maybe::Some(phone) = &self.phone + && !PHONE_PATTERN.is_match(phone) + { + return Err(bad_request(format_args!( + "phone contact data '{phone}' is malformed" + ))); + } + Ok(()) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RegisterAccountRequest { + pub username: CompactString, + pub password: CompactString, + pub name: CompactString, + #[serde(default)] + pub is_public: bool, + #[serde(default)] + pub is_taler_exchange: bool, + #[serde(default)] + pub contact_data: ChallengeContactData, + pub cashout_payto_uri: Option<IbanPayto>, + pub payto_uri: Option<Payto<LibeufinId>>, + pub debit_threshold: Option<Amount>, + pub tan_channel: Option<TanChannel>, + pub tan_channels: Option<Vec<TanChannel>>, + pub conversion_rate_class_id: Option<u64>, +} + +impl RegisterAccountRequest { + pub fn validate(&self) -> ApiResult<()> { + static USERNAME_REGEX: LazyLock<Regex> = + LazyLock::new(|| Regex::new("^[a-zA-Z0-9-._~]{1,126}$").unwrap()); + if !USERNAME_REGEX.is_match(&self.username) { + return Err(bad_request(format_args!( + "username '{}' is malformed, must match [a-zA-Z0-9-._~]{{1,126}}", + self.username + ))); + } + if self.tan_channel.is_some() && self.tan_channels.is_some() { + return Err(bad_request( + "you must only use either tan_channel or tan_channels", + )); + } + self.contact_data.validate()?; + Ok(()) + } + + pub fn channels(&self) -> &[TanChannel] { + if let Some(many) = &self.tan_channels { + many + } else if let Some(one) = &self.tan_channel { + std::slice::from_ref(one) + } else { + &[] + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RegisterAccountResponse { + pub internal_payto_uri: FullBankPayto, +} + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub enum Maybe<T> { + #[default] + Missing, + Null, + Some(T), +} + +impl<T> Maybe<T> { + pub fn opt(&self) -> Option<&T> { + match self { + Maybe::Missing | Maybe::Null => None, + Maybe::Some(v) => Some(v), + } + } + + pub fn is_some(&self) -> bool { + matches!(self, Maybe::Some(_)) + } + + pub fn inner(&self) -> Option<Option<&T>> { + match self { + Maybe::Missing => None, + Maybe::Null => Some(None), + Maybe::Some(v) => Some(Some(v)), + } + } +} + +impl<T: FromStr> FromStr for Maybe<T> { + type Err = T::Err; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + if s.is_empty() { + Ok(Self::Null) + } else { + Ok(Self::Some(s.parse()?)) + } + } +} + +impl<'de, T: Deserialize<'de>> Deserialize<'de> for Maybe<T> { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: serde::Deserializer<'de>, + { + Ok(match Option::<T>::deserialize(deserializer)? { + None => Maybe::Null, + Some(v) => Maybe::Some(v), + }) + } +} + +impl<DB: Database, T: sqlx::Type<DB>> sqlx::Type<DB> for Maybe<T> { + fn type_info() -> DB::TypeInfo { + Option::<T>::type_info() + } +} + +impl<'r, DB: Database, T: sqlx::Decode<'r, DB>> sqlx::Decode<'r, DB> for Maybe<T> { + fn decode(value: <DB as Database>::ValueRef<'r>) -> Result<Self, sqlx::error::BoxDynError> { + Ok(match Option::<T>::decode(value)? { + None => Maybe::Null, + Some(v) => Maybe::Some(v), + }) + } +} + +impl<T: Serialize> Serialize for Maybe<T> { + fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> { + self.opt().serialize(serializer) + } +} + +impl<T> From<Option<T>> for Maybe<T> { + fn from(value: Option<T>) -> Self { + match value { + None => Maybe::Null, + Some(v) => Maybe::Some(v), + } + } +} + +impl<T> From<Option<Maybe<T>>> for Maybe<T> { + fn from(value: Option<Maybe<T>>) -> Self { + match value { + None => Maybe::Missing, + Some(v) => v, + } + } +} + +pub trait TanInfo: Debug { + fn phone(&self) -> Option<&str>; + fn email(&self) -> Option<&str>; + fn channels(&self) -> &[TanChannel]; + fn mfa(&self) -> Tans { + self.channels() + .iter() + .filter_map(|it| { + match it { + TanChannel::email => self.email(), + TanChannel::sms => self.phone(), + } + .map(|info| (*it, info.into())) + }) + .collect() + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct AccountReconfiguration { + #[serde(default)] + pub contact_data: ChallengeContactData, + #[serde(default)] + pub cashout_payto_uri: Maybe<IbanPayto>, + pub name: Option<CompactString>, + pub is_public: Option<bool>, + pub debit_threshold: Option<Amount>, + #[serde(default)] + pub tan_channel: Maybe<TanChannel>, + #[serde(default)] + pub tan_channels: Option<Vec<TanChannel>>, + pub is_taler_exchange: Option<bool>, + #[serde(default)] + pub conversion_rate_class_id: Maybe<u64>, +} + +impl TanInfo for AccountReconfiguration { + fn phone(&self) -> Option<&str> { + self.contact_data.phone.opt().map(|it| it.as_str()) + } + + fn email(&self) -> Option<&str> { + self.contact_data.email.opt().map(|it| it.as_str()) + } + + fn channels(&self) -> &[TanChannel] { + if let Some(many) = &self.tan_channels { + many + } else if let Some(one) = self.tan_channel.opt() { + std::slice::from_ref(one) + } else { + &[] + } + } +} + +impl AccountReconfiguration { + pub fn check(&self) -> ApiResult<()> { + if self.tan_channel.is_some() && self.tan_channels.is_some() { + return Err(bad_request( + "you must only use either tan_channel or tan_channels", + )); + } + + Ok(()) + } + + pub fn channels(&self) -> Option<&[TanChannel]> { + if let Some(many) = &self.tan_channels { + Some(many) + } else if let Some(one) = self.tan_channel.opt() { + Some(std::slice::from_ref(one)) + } else { + None + } + } + + pub fn required_validation(&self, current: &impl TanInfo) -> ApiResult<Tans> { + // Tan channels are either the new ones or the current one + let channels = self.channels().unwrap_or_else(|| current.channels()); + let validated = current.mfa(); + + channels + .iter() + .filter_map(|channel| { + // Info are either the new one or the current ones + let info = match channel { + TanChannel::sms => self.phone().or(current.phone()), + TanChannel::email => self.email().or(current.email()), + }; + + let Some(info) = info else { + return Some(Err(failure( + ErrorCode::BANK_MISSING_TAN_INFO, + format_args!("missing info for tan channel {channel}"), + ))); + }; + + let tan = (*channel, info.into()); + + // Check if tan is already used and therefore already validated + (!(validated.contains(&tan))).then_some(Ok(tan)) + }) + .collect::<Result<Vec<_>, _>>() + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, EnumMeta)] +#[enum_meta(Str)] +#[allow(non_camel_case_types)] +pub enum CreditDebitInfo { + credit, + debit, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Balance { + pub amount: Amount, + pub credit_debit_indicator: CreditDebitInfo, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, EnumMeta, sqlx::Type)] +#[enum_meta(Str)] +#[sqlx(type_name = "TEXT")] +#[allow(non_camel_case_types)] +pub enum AccountStatus { + active, + locked, + deleted, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct AccountData { + pub name: CompactString, + pub payto_uri: FullBankPayto, + pub balance: Balance, + pub debit_threshold: Amount, + pub contact_data: ChallengeContactData, + pub cashout_payto_uri: Option<PaytoURI>, + pub tan_channel: Option<TanChannel>, + pub tan_channels: Vec<TanChannel>, + pub is_public: bool, + pub is_taler_exchange: bool, + pub is_locked: bool, + pub status: AccountStatus, + pub conversion_rate_class_id: Option<u64>, + pub conversion_rate: Option<ConversionRate>, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct AccountMinimalData { + pub username: CompactString, + pub name: CompactString, + pub payto_uri: FullBankPayto, + pub balance: Balance, + pub debit_threshold: Amount, + pub is_public: bool, + pub is_taler_exchange: bool, + pub is_locked: bool, + pub row_id: u64, + pub status: AccountStatus, + pub conversion_rate_class_id: Option<u64>, + pub conversion_rate: Option<ConversionRate>, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ListBankAccountsResponse { + pub accounts: Vec<AccountMinimalData>, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PublicAccount { + pub username: CompactString, + pub payto_uri: FullBankPayto, + pub balance: Balance, + pub is_taler_exchange: bool, + pub row_id: u64, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PublicAccountsResponse { + pub public_accounts: Vec<PublicAccount>, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct AccountPasswordChange { + pub new_password: CompactString, + pub old_password: Option<CompactString>, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct AccountParams { + #[serde(flatten)] + pub page: PageParams, + pub filter_name: Option<CompactString>, + pub conversion_rate_class_id: Option<u64>, +} + +impl AccountParams { + pub fn check(self) -> Result<Account, ParamsErr> { + Ok(Account { + page: self.page.check()?, + filter_name: self.filter_name.map(|it| format_compact!("%{it}%")), + conversion_rate_class_id: self.conversion_rate_class_id, + }) + } +} + +#[derive(Debug)] +pub struct Account { + pub page: Page, + pub filter_name: Option<CompactString>, + pub conversion_rate_class_id: Option<u64>, +} + +pub fn account_api() -> Router<Arc<BankState>> { + Router::new() + .route( + "/accounts", + post( + async |RegistrationAuth { is_admin }: RegistrationAuth, + State(state): State<Arc<BankState>>, + Req(req): Req<RegisterAccountRequest>| + -> ApiResult<Json<RegisterAccountResponse>> { + match create_account(&state.db, &state.cfg, &req, is_admin).await? { + CreationResult::BonusBalanceInsufficient => { + Err(failure_code(ErrorCode::BANK_UNALLOWED_DEBIT)) + } + CreationResult::UsernameReuse => { + Err(failure_code(ErrorCode::BANK_REGISTER_USERNAME_REUSE)) + } + CreationResult::PayToReuse => { + Err(failure_code(ErrorCode::BANK_REGISTER_PAYTO_URI_REUSE)) + } + CreationResult::UnknownConversionClass => { + Err(failure_code(ErrorCode::BANK_CONVERSION_RATE_CLASS_UNKNOWN)) + } + CreationResult::Success(payto) => Ok(Json(RegisterAccountResponse { + internal_payto_uri: payto, + })), + } + }, + ) + .get( + async |State(state): State<Arc<BankState>>, + _: AdminRAuth, + Query(params): Query<AccountParams>| { + let params = params.check()?; + let accounts = page_admin( + &state.db, + &state.cfg.ctx, + &state.cfg.regional_currency, + state.cfg.fiat_currency(), + &params, + ) + .await?; + if accounts.is_empty() { + ApiResult::Ok(NoContent.into_response()) + } else { + Ok(Json(ListBankAccountsResponse { accounts }).into_response()) + } + }, + ), + ) + .route( + "/public-accounts", + get( + async |State(state): State<Arc<BankState>>, Query(params): Query<AccountParams>| { + let params = params.check()?; + let accounts = page_public( + &state.db, + &state.cfg.ctx, + &state.cfg.regional_currency, + &params, + ) + .await?; + if accounts.is_empty() { + ApiResult::Ok(NoContent.into_response()) + } else { + Ok(Json(PublicAccountsResponse { + public_accounts: accounts, + }) + .into_response()) + } + }, + ), + ) + .route( + "/accounts/{username}", + patch( + async |State(state): State<Arc<BankState>>, + MfaReq { mut auth, req, mfa }: MfaReq<AccountReconfigOp>| { + if let Some(tans) = mfa.pending_mfa() { + return mfa.response_validation(&auth, &state.db, tans).await; + } + match patch_account( + &state.db, + &state.cfg, + &req, + &auth.username, + auth.is_admin(), + mfa.is_2fa(), + ) + .await? + { + PatchResult::Success => Ok(NoContent.into_response()), + PatchResult::MissingTanInfo(e) => Err(e), + PatchResult::Challenges(tans) => { + if tans.is_empty() { + mfa.response_mfa(&mut auth, &state.db, &state.cfg.ctx).await + } else { + mfa.response_validation(&auth, &state.db, &tans).await + } + } + PatchResult::UnknownAccount => todo!(), + PatchResult::NonAdminName => { + Err(failure_code(ErrorCode::BANK_NON_ADMIN_PATCH_LEGAL_NAME)) + } + PatchResult::NonAdminCashout => { + Err(failure_code(ErrorCode::BANK_NON_ADMIN_PATCH_CASHOUT)) + } + PatchResult::NonAdminDebtLimit => { + Err(failure_code(ErrorCode::BANK_NON_ADMIN_PATCH_DEBT_LIMIT)) + } + PatchResult::NonAdminConversionRateClass => Err(failure_code( + ErrorCode::BANK_NON_ADMIN_SET_CONVERSION_RATE_CLASS, + )), + PatchResult::UnknownConversionClass => { + Err(failure_code(ErrorCode::BANK_CONVERSION_RATE_CLASS_UNKNOWN)) + } + } + }, + ) + .delete( + async |State(state): State<Arc<BankState>>, + MfaReq { mut auth, mfa, .. }: MfaReq<AccountDeletionOp>| { + if !state.cfg.allow_account_deletion && !auth.is_admin() { + return Err(require_admin()); + } + // Not deleting reserved names + if matches!(auth.username.as_str(), "admin" | "bank") { + return Err(failure( + ErrorCode::BANK_RESERVED_USERNAME_CONFLICT, + "Cannot delete reserved account", + )); + } else if auth.username == "exchange" && state.cfg.fiat.is_some() { + return Err(failure( + ErrorCode::BANK_RESERVED_USERNAME_CONFLICT, + "Cannot delete 'exchange' accounts when conversion is enabled", + )); + } + match db::account::delete(&state.db, &auth.username, mfa.is_2fa()).await? { + DeletionResult::Success => Ok(NoContent.into_response()), + DeletionResult::UnknownAccount => { + Err(failure_code(ErrorCode::BANK_UNKNOWN_ACCOUNT)) + } + DeletionResult::BalanceNotZero => { + Err(failure_code(ErrorCode::BANK_ACCOUNT_BALANCE_NOT_ZERO)) + } + DeletionResult::TanRequired => Ok(mfa + .response_mfa(&mut auth, &state.db, &state.cfg.ctx) + .await + .into_response()), + } + }, + ) + .get( + async |State(state): State<Arc<BankState>>, + UserAuth { username, .. }: UserRAuth| { + match by_username( + &state.db, + &state.cfg.ctx, + &state.cfg.regional_currency, + state.cfg.fiat_currency(), + &username, + ) + .await? + { + Some(acc) => Ok(Json(acc)), + None => Err(failure_code(ErrorCode::BANK_UNKNOWN_ACCOUNT)), + } + }, + ), + ) + .route( + "/accounts/{username}/auth", + patch( + async |State(state): State<Arc<BankState>>, + MfaReq { mut auth, req, mfa }: MfaReq<AccountPasswordOp>| { + if !auth.is_admin() && req.old_password.is_none() { + return Err(failure_code( + ErrorCode::BANK_NON_ADMIN_PATCH_MISSING_OLD_PASSWORD, + )); + } + checkpw(&req.new_password, state.cfg.pwd_check_quality)?; + + match reconfig_password( + &state.db, + &state.cfg.pw_crypto, + &auth.username, + &req.new_password, + req.old_password.as_deref(), + auth.is_admin() || mfa.is_2fa(), + ) + .await? + { + PatchAuthResult::UnknownAccount => { + Err(failure_code(ErrorCode::BANK_UNKNOWN_ACCOUNT)) + } + PatchAuthResult::OldPasswordMismatch => { + Err(failure_code(ErrorCode::BANK_PATCH_BAD_OLD_PASSWORD)) + } + PatchAuthResult::TanRequired => Ok(mfa + .response_mfa(&mut auth, &state.db, &state.cfg.ctx) + .await? + .into_response()), + PatchAuthResult::Success => Ok(NoContent.into_response()), + } + }, + ), + ) +} + +pub fn rand_iban_payto() -> IbanPayto { + let iban = IBAN::random(Country::DE); + IbanPayto::new(BankID { iban, bic: None }) +} + +pub async fn create_account( + db: &PgPool, + cfg: &BankCfg, + req: &RegisterAccountRequest, + is_admin: bool, +) -> ApiResult<CreationResult> { + req.validate()?; + + if matches!(req.username.as_str(), "admin" | "bank") { + return Err(failure_code(ErrorCode::BANK_RESERVED_USERNAME_CONFLICT)); + } + + let channels = req.channels(); + + if !is_admin { + if req.debit_threshold.is_some() { + return Err(failure_code(ErrorCode::BANK_NON_ADMIN_PATCH_DEBT_LIMIT)); + } else if req.conversion_rate_class_id.is_some() { + return Err(failure_code( + ErrorCode::BANK_NON_ADMIN_SET_CONVERSION_RATE_CLASS, + )); + } else if !channels.is_empty() { + return Err(failure_code(ErrorCode::BANK_NON_ADMIN_SET_TAN_CHANNEL)); + } + } else { + if let Some(amount) = req.debit_threshold { + cfg.check_regio(&amount)?; + } + } + + for channel in channels { + if !cfg.tan_channels.contains_key(channel) { + return Err(failure( + ErrorCode::BANK_TAN_CHANNEL_NOT_SUPPORTED, + format_args!("unsupported tan channel {channel}"), + )); + } + + let info = match channel { + TanChannel::sms => req.contact_data.phone.opt(), + TanChannel::email => req.contact_data.email.opt(), + }; + + if info.is_none() { + return Err(failure( + ErrorCode::BANK_MISSING_TAN_INFO, + format_args!("missing info for tan channel {channel}"), + )); + } + } + + if req.username == "exchange" && !req.is_taler_exchange { + return Err(failure_status( + ErrorCode::END, + "'exchange' account must be a taler exchange account", + StatusCode::CONFLICT, + )); + } + + checkpw(&req.password, cfg.pwd_check_quality)?; + + let create = async |payto: LibeufinId| { + crate::db::account::create( + db, + &cfg.ctx, + &cfg.pw_crypto, + &req.username, + &req.password, + &req.name, + req.contact_data.email.opt().map(|it| it.as_str()), + req.contact_data.phone.opt().map(|it| it.as_str()), + req.cashout_payto_uri.as_ref(), + &payto, + req.is_public, + req.is_taler_exchange, + req.debit_threshold.unwrap_or(cfg.default_debt_limit), + if req.is_taler_exchange { + Amount::zero(&cfg.regional_currency) + } else { + cfg.registration_bonus + }, + channels, + req.payto_uri.is_some(), + req.conversion_rate_class_id, + ) + .await + }; + + match cfg.wire_method { + WireMethod::iban => { + if let Some(payto) = &req.payto_uri { + let bank_id = payto.expect_iban()?; + Ok(create(LibeufinId::IBAN(*bank_id)).await?) + } else { + let mut retry = 5; + loop { + let payto = rand_iban_payto(); + let res = create(LibeufinId::IBAN(payto.into_inner())).await?; + if res == CreationResult::PayToReuse && retry > 0 { + retry -= 1; + continue; + } + return Ok(res); + } + } + } + WireMethod::x_taler_bank => { + if let Some(payto) = &req.payto_uri { + let libeufin_id = payto.expect_xtaler_bank()?; + if libeufin_id.username != req.username { + return Err(bad_request(format_args!( + "Expected a payto uri for '{}' got one for '{}'", + req.username, libeufin_id.username + ))); + } + } + Ok(create(LibeufinId::XTalerBank(XTalerBank { + hostname: cfg.ctx.hostname.clone(), + username: req.username.clone(), + })) + .await?) + } + } +} + +/** + * This function creates the admin account ONLY IF it was + * NOT found in the database. It sets it to a random password that + * is only meant to be overridden by a dedicated CLI tool + */ +pub async fn create_admin_account( + db: &PgPool, + cfg: &BankCfg, + pw: Option<&str>, +) -> anyhow::Result<CreationResult> { + let pw = pw + .map(|it| it.to_owned()) + .unwrap_or_else(|| Base32::<32>::secure_rand().to_string()); + + let payto = match cfg.wire_method { + WireMethod::iban => LibeufinId::IBAN(rand_iban_payto().into_inner()), + WireMethod::x_taler_bank => LibeufinId::XTalerBank(XTalerBank { + hostname: cfg.ctx.hostname.clone(), + username: CompactString::const_new("admin"), + }), + }; + + Ok(crate::db::account::create( + db, + &cfg.ctx, + &cfg.pw_crypto, + "admin", + &pw, + "Bank administrator", + None, + None, + None, + &payto, + false, + false, + cfg.default_debt_limit, + Amount::zero(&cfg.regional_currency), + &[], + false, + None, + ) + .await?) +} + +pub async fn patch_account( + db: &PgPool, + cfg: &BankCfg, + req: &AccountReconfiguration, + username: &str, + is_admin: bool, + is2fa: bool, +) -> ApiResult<PatchResult> { + if let Some(amount) = req.debit_threshold { + cfg.check_regio(&amount)?; + } + + if username == "admin" && req.is_public == Some(true) { + return Err(failure_status( + ErrorCode::END, + "'admin' account cannot be public", + StatusCode::CONFLICT, + )); + } + + if username == "exchange" && req.is_taler_exchange == Some(false) { + return Err(failure_status( + ErrorCode::END, + "'exchange' account must be a taler exchange account", + StatusCode::CONFLICT, + )); + } + + if let Some(channels) = req.channels() { + for channel in channels { + if !cfg.tan_channels.contains_key(channel) { + return Err(failure( + ErrorCode::BANK_TAN_CHANNEL_NOT_SUPPORTED, + format_args!("unsupported tan channel {channel}"), + )); + } + } + } + + Ok(reconfig( + db, + &cfg.regional_currency, + username, + req, + is_admin, + is2fa, + cfg.allow_edit_name, + cfg.allow_edit_cashout, + ) + .await?) +} + +#[cfg(test)] +pub mod test { + + use axum::http::{Method, StatusCode}; + use jiff::Timestamp; + use serde::Serialize; + use taler_common::{ + error_code::ErrorCode, + types::{amount::amount, payto::PaytoImpl}, + }; + use taler_test_utils::{json, server::TestServer as _}; + + use crate::{ + TanChannel, + api::{ + account::{ + AccountData, AccountStatus, Balance, ChallengeContactData, CreditDebitInfo, + ListBankAccountsResponse, Maybe, PublicAccountsResponse, RegisterAccountResponse, + rand_iban_payto, + }, + conversion::ConversionRateClass, + test::{Auth, BankTestCtx, MfaRequest, bank_setup, bank_setup_conf}, + }, + db::gc::collect, + payto::FullBankPayto, + }; + + #[tokio::test] + async fn create() { + let mut ctx = bank_setup().await; + + let admin_only = async |body: serde_json::Value, error: ErrorCode| { + // Check restricted + ctx.post("/accounts").json(&body).await.assert_error(error); + ctx.post_admin("/accounts").json(&body).await.assert_ok(); + }; + ctx.auth_routine(Method::GET, "/accounts/merchant", Auth::UserOrAdmin) + .await; + + // Check generated payto + { + let body = json!({ + "username": "john", + "password": "password", + "name": "John" + }); + // Check ok + let payto = ctx + .post("/accounts") + .json(&body) + .await + .assert_ok_json::<RegisterAccountResponse>() + .internal_payto_uri; + // Check idempotency + assert_eq!( + payto, + ctx.post("/accounts") + .json(&body) + .await + .assert_ok_json::<RegisterAccountResponse>() + .internal_payto_uri + ); + // Check idempotency with payto + ctx.post("/accounts") + .json(&json!(body + { + "payto_uri": payto + })) + .await + .assert_ok_json::<RegisterAccountResponse>(); + // Check payto conflict + ctx.post("/accounts") + .json(&json!(body + { + "payto_uri": rand_iban_payto() + })) + .await + .assert_error(ErrorCode::BANK_REGISTER_USERNAME_REUSE); + } + + let name = "Jane"; + let payto = &rand_iban_payto(); + let req = json!({ + "username": "foo", + "password": "password", + "name": name, + "is_public": true, + "payto_uri": &payto, + "is_taler_exchange": true + }); + // Check given payto + { + let full: FullBankPayto = payto.full(name).convert(); + // Check ok + assert_eq!( + full, + ctx.post("/accounts") + .json(&req) + .await + .assert_ok_json::<RegisterAccountResponse>() + .internal_payto_uri + ); + // Check idempotency + assert_eq!( + full, + ctx.post("/accounts") + .json(&req) + .await + .assert_ok_json::<RegisterAccountResponse>() + .internal_payto_uri + ); + } + + // Check admin only debit_threshold + admin_only( + json!({ + "username": "bat", + "password": "password", + "name": "Bat", + "debit_threshold": "KUDOS:42" + }), + ErrorCode::BANK_NON_ADMIN_PATCH_DEBT_LIMIT, + ) + .await; + + // Check admin only conversion_rate_class_id + let conv_class_id = ctx.create_conversion_rate_class().await; + admin_only( + json!({ + "username": "bat2", + "password": "password", + "name": "Bat", + "conversion_rate_class_id": conv_class_id + }), + ErrorCode::BANK_NON_ADMIN_SET_CONVERSION_RATE_CLASS, + ) + .await; + + // Check admin only tan_channel + admin_only( + json!({ + "username": "bat3", + "password": "password", + "name": "Bat", + "contact_data" : { + "phone" : "+456" + }, + "tan_channel": "sms" + }), + ErrorCode::BANK_NON_ADMIN_SET_TAN_CHANNEL, + ) + .await; + + // Check both tan channels + ctx.post("/accounts") + .json(json!({ + "username": "bat3", + "password": "password", + "name": "Bat", + "tan_channel": "sms", + "tan_channels": [] + })) + .await + .assert_bad_request(); + + // Check tan info + for channel in TanChannel::entries { + ctx.post_admin("/accounts") + .json(json!({ + "username": "bat", + "password": "password", + "name": "Bat", + "tan_channel": channel + })) + .await + .assert_error(ErrorCode::BANK_MISSING_TAN_INFO); + ctx.post_admin("/accounts") + .json(json!({ + "username": "bat", + "password": "password", + "name": "Bat", + "tan_channels": [channel] + })) + .await + .assert_error(ErrorCode::BANK_MISSING_TAN_INFO); + } + ctx.post_admin("/accounts") + .json(json!({ + "username": "bat", + "password": "password", + "name": "Bat", + "tan_channels": TanChannel::entries + })) + .await + .assert_error(ErrorCode::BANK_MISSING_TAN_INFO); + + // Check unknown conversion rate class + ctx.post_admin("/accounts") + .json(json!({ + "username": "bat4", + "password": "password", + "name": "Bat", + "conversion_rate_class_id": 42 + })) + .await + .assert_error(ErrorCode::BANK_CONVERSION_RATE_CLASS_UNKNOWN); + + for username in ["admin", "bank"] { + ctx.post("/accounts") + .json(json!({ + "username": username, + "password": "password", + "name": "John Smith" + })) + .await + .assert_error(ErrorCode::BANK_RESERVED_USERNAME_CONFLICT); + } + + for username in [ + "bad@username", + "bad/username", + " spaces ", + &"long".repeat(40), + ] { + ctx.post("/accounts") + .json(json!({ + "username": username, + "password": "password", + "name": "John Smith" + })) + .await + .assert_bad_request(); + } + + // Non exchange account + ctx.post("/accounts") + .json(json!({ + "username": "exchange", + "password": "password", + "name": "Exchange" + })) + .await + .assert_error_status(ErrorCode::END, StatusCode::CONFLICT); + + // Username conflict + ctx.post("/accounts") + .json(json!(req + { "name" : "Foo" })) + .await + .assert_error(ErrorCode::BANK_REGISTER_USERNAME_REUSE); + // Payto conflict + ctx.post("/accounts") + .json(json!(req + { "username" : "bar" })) + .await + .assert_error(ErrorCode::BANK_REGISTER_PAYTO_URI_REUSE); + ctx.get_admin("/accounts/bar") + .await + .assert_error(ErrorCode::BANK_UNKNOWN_ACCOUNT); + // Bad payto kind + ctx.post("/accounts") + .json(json!(req +{ + "payto_uri": "payto://x-taler-bank/bank.hostname.test/bar" + })) + .await + .assert_bad_request(); + // Short password + ctx.post("/accounts") + .json(json!(req + { "password" : "short" })) + .await + .assert_error(ErrorCode::BANK_PASSWORD_TOO_SHORT); + // Long password + ctx.post("/accounts") + .json(json!(req +{ + "password": "loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong-password" + })) + .await + .assert_error(ErrorCode::BANK_PASSWORD_TOO_LONG); + // Check currency + ctx.post_admin("/accounts") + .json(json!(req + { "debit_threshold": "EUR:100" })) + .await + .assert_error(ErrorCode::GENERIC_CURRENCY_MISMATCH); + + // Check cashout payto receiver name logic + ctx.post("/accounts") + .json(json!({ + "username": "cashout_guess", + "password": "cashout_guess-password", + "name": "Mr Guess My Name", + "cashout_payto_uri": &payto + })) + .await + .assert_ok(); + ctx.post("/accounts") + .json(json!({ + "username": "cashout_keep", + "password": "cashout_keep-password", + "name": "Mr Keep My Name", + "cashout_payto_uri": payto.as_full_uri("Santa Clauss") + })) + .await + .assert_ok(); + ctx.cache_tokens(&["cashout_guess", "cashout_keep"]).await; + let acc: AccountData = ctx.geta("/accounts/cashout_guess").await.assert_ok_json(); + assert_eq!( + acc.cashout_payto_uri.unwrap(), + payto.as_full_uri("Mr Guess My Name") + ); + let acc: AccountData = ctx.geta("/accounts/cashout_keep").await.assert_ok_json(); + assert_eq!( + acc.cashout_payto_uri.unwrap(), + payto.as_full_uri("Mr Keep My Name") + ); + + // Check input restriction + { + let req = json!({ + "username": "username", + "password": "password", + "name": "Name" + }); + ctx.post("/accounts") + .json(json!(req + { + "contact_data": { "phone" : " +456" } + })) + .await + .assert_bad_request(); + ctx.post("/accounts") + .json(json!(req + { + "contact_data": { "phone" : "test@gmail.com" } + })) + .await + .assert_bad_request(); + } + + // Create -> get + ctx.post("/accounts") + .json(json!({ + "username": "minimal", + "password": "minimal-password", + "name": "John Smith", + })) + .await + .assert_ok(); + let new = rand_iban_payto(); + ctx.post_admin("/accounts") + .json(json!({ + "username": "maximal", + "password": "maximal-password", + "name": "John Smith", + "is_public": true, + "is_taler_exchange": true, + "contact_data": { + "phone": "+456", + "email": "test@gmail.com" + }, + "cashout_payto_uri": payto, + "payto_uri": new, + "debit_threshold": "KUDOS:12", + "tan_channels": ["sms", "email"], + "conversion_rate_class_id": conv_class_id + })) + .await + .assert_ok(); + ctx.cache_tokens(&["minimal", "maximal"]).await; + let acc: AccountData = ctx.geta("/accounts/minimal").await.assert_ok_json(); + pretty_assertions::assert_eq!( + AccountData { + name: "John Smith".into(), + balance: Balance { + amount: amount("KUDOS:0"), + credit_debit_indicator: CreditDebitInfo::credit, + }, + cashout_payto_uri: None, + contact_data: ChallengeContactData { + phone: Maybe::Null, + email: Maybe::Null, + }, + conversion_rate: Some(acc.conversion_rate.clone().unwrap()), + conversion_rate_class_id: None, + debit_threshold: acc.debit_threshold, + is_locked: false, + is_public: false, + is_taler_exchange: false, + payto_uri: acc.payto_uri.clone(), + status: AccountStatus::active, + tan_channel: None, + tan_channels: Vec::new(), + }, + acc + ); + let acc: AccountData = ctx.geta("/accounts/maximal").await.assert_ok_json(); + pretty_assertions::assert_eq!( + AccountData { + name: "John Smith".into(), + balance: Balance { + amount: amount("KUDOS:0"), + credit_debit_indicator: CreditDebitInfo::credit, + }, + cashout_payto_uri: Some(payto.as_full_uri("John Smith")), + contact_data: ChallengeContactData { + phone: Maybe::Some("+456".into()), + email: Maybe::Some("test@gmail.com".into()) + }, + conversion_rate: Some(acc.conversion_rate.clone().unwrap()), + conversion_rate_class_id: Some(conv_class_id), + debit_threshold: acc.debit_threshold, + is_locked: false, + is_public: true, + is_taler_exchange: true, + payto_uri: new.full("John Smith").convert(), + status: AccountStatus::active, + tan_channel: Some(TanChannel::sms), + tan_channels: vec![TanChannel::sms, TanChannel::email] + }, + acc + ); + + let ctx = ctx.swap_cfg("test_bonus.conf").await; + // Create bonus + { + let req = json!({ + "password": "password", + "name": "Mallory" + }); + + ctx.set_max_debt("admin", "1000").await; + + // Check OK + for i in 0..10 { + let username = format!("foo{i}"); + ctx.post_admin("/accounts") + .json(json!(req + { "username": username })) + .await + .assert_ok(); + ctx.assert_balance(&username, "100").await; + } + ctx.assert_balance("admin", "-1000").await; + + // Check insufficient fund + ctx.post_admin("/accounts") + .json(json!(req + { "username": "bar" })) + .await + .assert_error(ErrorCode::BANK_UNALLOWED_DEBIT); + ctx.get_admin("/accounts/bar") + .await + .assert_error(ErrorCode::BANK_UNKNOWN_ACCOUNT); + } + + // Restricted account creation + let ctx = ctx.swap_cfg("test_restrict.conf").await; + ctx.auth_routine(Method::POST, "/accounts", Auth::UserOrAdmin) + .await; + ctx.post_admin("/accounts") + .json(json!({ + "username": "foobar", + "password": "password-xyz", + "name": "Mallory" + })) + .await + .assert_ok(); + + // Unsupported tan + let ctx = ctx.swap_cfg("test_tan_err.conf").await; + ctx.post_admin("/accounts") + .json(json!({ + "username": "foo", + "password": "password-xyz", + "name": "Mallory", + "tan_channel": "email" + })) + .await + .assert_error(ErrorCode::BANK_TAN_CHANNEL_NOT_SUPPORTED); + + // No password check + let ctx = ctx.swap_cfg("test_no_password_check.conf").await; + // Short password + ctx.post("/accounts") + .json(json!({ + "username": "short", + "password": "short", + "name": "John Smith", + })) + .await + .assert_ok(); + // Long password + ctx.post("/accounts") + .json(json!({ + "username": "long", + "password": "loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong-password", + "name": "John Smith" + })) + .await + .assert_ok(); + } + + #[tokio::test] + async fn delete() { + let mut ctx = bank_setup().await; + ctx.auth_routine(Method::DELETE, "/accounts/merchant", Auth::Token) + .await; + + // Reserved accounts + for username in ["admin", "bank"] { + ctx.delete_admin(format!("/accounts/{username}")) + .await + .assert_error(ErrorCode::BANK_RESERVED_USERNAME_CONFLICT); + } + ctx.deletea("/accounts/exchange") + .await + .assert_error(ErrorCode::BANK_RESERVED_USERNAME_CONFLICT); + + let payto = ctx.tmp_payto().await; + ctx.post("/accounts") + .json(json!({ + "username": "john", + "password": "john-password", + "name": "John", + "payto_uri": payto + })) + .await + .assert_ok(); + ctx.cache_tokens(&["john"]).await; + ctx.fill_tan_info("john").await; + // Fail to delete, due to a non-zero balance. + ctx.tx("customer", "1", "john").await; + ctx.deletea("/accounts/john") + .await + .assert_error(ErrorCode::BANK_ACCOUNT_BALANCE_NOT_ZERO); + // Successful deletion + ctx.tx("john", "1", "customer").await; + ctx.deletea("/accounts/john") + .await + .assert_challenge(&ctx) + .await + .assert_no_content(); + // Account no longer exists + ctx.deletea("/accounts/john") + .await + .assert_error(ErrorCode::GENERIC_TOKEN_UNKNOWN); + ctx.delete_admin("/accounts/john") + .await + .assert_error(ErrorCode::BANK_UNKNOWN_ACCOUNT); + + // GC + { + let now = Timestamp::now(); + collect(&ctx.state.db, &now, &now, &now).await.unwrap(); + // TODO need more operations + } + + // Test admin-only account deletion + let ctx = ctx.swap_cfg("test_restrict.conf").await; + ctx.auth_routine(Method::DELETE, "/accounts/merchant", Auth::Admin) + .await; + // Exchange is still restricted + ctx.delete_admin("/accounts/exchange") + .await + .assert_error(ErrorCode::BANK_RESERVED_USERNAME_CONFLICT); + + // Test delete exchange account + let ctx = ctx.swap_cfg("test_no_conversion.conf").await; + // Exchange is no longer restricted + ctx.deletea("/accounts/exchange").await.assert_no_content(); + } + + async fn check_admin_only(ctx: &BankTestCtx, req: impl Serialize, error: ErrorCode) { + // Check restricted + ctx.patcha("/accounts/merchant") + .json(&req) + .await + .assert_error(error); + // Check admin always can + ctx.patch_admin("/accounts/merchant") + .json(&req) + .await + .assert_no_content(); + // Check idempotent + ctx.patch_admin("/accounts/merchant") + .json(&req) + .await + .assert_no_content(); + } + + #[tokio::test] + async fn reconfig() { + let mut ctx = bank_setup().await; + + ctx.auth_routine(Method::PATCH, "/accounts/merchant", Auth::UserOrAdmin) + .await; + + for channel in TanChannel::entries { + ctx.patcha("/accounts/merchant") + .json(json!({ "tan_channel": channel })) + .await + .assert_error(ErrorCode::BANK_MISSING_TAN_INFO); + ctx.patcha("/accounts/merchant") + .json(json!({ "tan_channels": [channel] })) + .await + .assert_error(ErrorCode::BANK_MISSING_TAN_INFO); + } + ctx.patcha("/accounts/merchant") + .json(json!({ "tan_channels": TanChannel::entries })) + .await + .assert_error(ErrorCode::BANK_MISSING_TAN_INFO); + + // Successful attempt now + let cashout = rand_iban_payto(); + let req = json!({ + "cashout_payto_uri": cashout, + "name": "Roger", + "is_public": true, + "is_taler_exchange": true, + "contact_data": { + "phone": "+99", + "email": "foo@example.com" + } + }); + ctx.patcha("/accounts/merchant") + .json(&req) + .await + .assert_no_content(); + // Checking idempotent + ctx.patcha("/accounts/merchant") + .json(&req) + .await + .assert_no_content(); + + // Check admin only + check_admin_only( + &ctx, + json!(req + { "debit_threshold": "KUDOS:100" }), + ErrorCode::BANK_NON_ADMIN_PATCH_DEBT_LIMIT, + ) + .await; + check_admin_only( + &ctx, + json!(req + { "conversion_rate_class_id": 1 }), + ErrorCode::BANK_NON_ADMIN_SET_CONVERSION_RATE_CLASS, + ) + .await; + + // Check unknown conversion rate class + ctx.patch_admin("/accounts/merchant") + .json(json!(req + { "conversion_rate_class_id": 42 })) + .await + .assert_error(ErrorCode::BANK_CONVERSION_RATE_CLASS_UNKNOWN); + + // Check currency + ctx.patch_admin("/accounts/merchant") + .json(json!(req + { "debit_threshold": "EUR:100" })) + .await + .assert_error(ErrorCode::GENERIC_CURRENCY_MISMATCH); + + // Check patch + let acc: AccountData = ctx.geta("/accounts/merchant").await.assert_ok_json(); + let expected = AccountData { + name: "Roger".into(), + balance: Balance { + amount: amount("KUDOS:0"), + credit_debit_indicator: CreditDebitInfo::credit, + }, + cashout_payto_uri: Some(acc.cashout_payto_uri.clone().unwrap()), + contact_data: ChallengeContactData { + phone: Maybe::Some("+99".into()), + email: Maybe::Some("foo@example.com".into()), + }, + conversion_rate: Some(acc.conversion_rate.clone().unwrap()), + conversion_rate_class_id: Some(1), + debit_threshold: acc.debit_threshold, + is_locked: false, + is_public: true, + is_taler_exchange: true, + payto_uri: ctx.merchant_payto.clone().into_inner().full("Roger"), + status: AccountStatus::active, + tan_channel: None, + tan_channels: Vec::new(), + }; + pretty_assertions::assert_eq!(acc, expected); + + // Check keep values when there is no changes + ctx.patcha("/accounts/merchant") + .json(json!({})) + .await + .assert_no_content(); + pretty_assertions::assert_eq!( + ctx.geta("/accounts/merchant") + .await + .assert_ok_json::<AccountData>(), + expected + ); + + // Admin cannot be public + ctx.patcha("/accounts/admin") + .json(json!({ + "is_public": true + })) + .await + .assert_error_status(ErrorCode::END, StatusCode::CONFLICT); + + // Exchange must be exchange + ctx.patcha("/accounts/exchange") + .json(json!({ + "is_taler_exchange": false + })) + .await + .assert_error_status(ErrorCode::END, StatusCode::CONFLICT); + + // Check cashout payto receiver name logic + ctx.post("/accounts") + .json(json!({ + "username": "cashout", + "password": "cashout-password", + "name": "Mr Cashout Cashout" + })) + .await + .assert_ok(); + ctx.cache_tokens(&["cashout"]).await; + + for (cashout, name, expect) in [ + ( + cashout.as_uri(), + None, + cashout.as_full_uri("Mr Cashout Cashout"), + ), + ( + cashout.as_uri(), + Some("New name"), + cashout.as_full_uri("New name"), + ), + ( + cashout.as_full_uri("Full name"), + None, + cashout.as_full_uri("New name"), + ), + ( + cashout.as_full_uri("Full second name"), + Some("Another name"), + cashout.as_full_uri("Another name"), + ), + ] { + let mut v = json!({ + "cashout_payto_uri": cashout, + }); + if let Some(name) = name { + v["name"] = name.into(); + } + ctx.patch_admin("/accounts/cashout") + .json(v) + .await + .assert_no_content(); + let acc: AccountData = ctx.geta("/accounts/cashout").await.assert_ok_json(); + assert_eq!(expect, acc.cashout_payto_uri.unwrap()); + } + + // Check 2FA + ctx.fill_tan_info("customer").await; + ctx.patcha("/accounts/customer") + .json(json!({ "is_public": true })) + .await + .assert_challenge_check(&ctx, async |_| { + let acc: AccountData = ctx.geta("/accounts/customer").await.assert_ok_json(); + assert!(!acc.is_public); + }) + .await + .assert_no_content(); + + let acc: AccountData = ctx.geta("/accounts/customer").await.assert_ok_json(); + assert!(acc.is_public); + + // Restriction + let ctx = ctx.swap_cfg("test_restrict.conf").await; + { + check_admin_only( + &ctx, + json!({ "name": "Another Foo" }), + ErrorCode::BANK_NON_ADMIN_PATCH_LEGAL_NAME, + ) + .await; + check_admin_only( + &ctx, + json!({ "cashout_payto_uri": rand_iban_payto() }), + ErrorCode::BANK_NON_ADMIN_PATCH_CASHOUT, + ) + .await; + + // Check idempotent + let acc: AccountData = ctx.geta("/accounts/merchant").await.assert_ok_json(); + ctx.patcha("/accounts/merchant") + .json(json!({ + "name": acc.name, + "cashout_payto_uri": acc.cashout_payto_uri, + "debit_threshold": acc.debit_threshold + })) + .await + .assert_no_content(); + } + + // Unsupported tan + let ctx = ctx.swap_cfg("test_tan_err.conf").await; + ctx.patcha("/accounts/customer") + .json(json!({ + "tan_channel": "email" + })) + .await + .assert_error(ErrorCode::BANK_TAN_CHANNEL_NOT_SUPPORTED); + } + + #[tokio::test] + async fn password() { + let ctx = bank_setup().await; + ctx.auth_routine(Method::PATCH, "/accounts/merchant/auth", Auth::UserOrAdmin) + .await; + + // Changing the password + ctx.patcha("/accounts/customer/auth") + .json(json!({ + "old_password": "customer-password", + "new_password": "new-password" + })) + .await + .assert_no_content(); + // Previous password should fail. + ctx.post("/accounts/customer/token") + .basic_auth("customer", "customer-password") + .await + .assert_status(StatusCode::UNAUTHORIZED); + // New password should succeed + ctx.post("/accounts/customer/token") + .basic_auth("customer", "new-password") + .json(json!({ "scope": "readonly" })) + .await + .assert_ok(); + ctx.patcha("/accounts/customer/auth") + .json(json!({ + "old_password": "new-password", + "new_password": "customer-password" + })) + .await + .assert_no_content(); + + // Check require test old password + ctx.patcha("/accounts/customer/auth") + .json(json!({ + "old_password": "bad-password", + "new_password": "new-password" + })) + .await + .assert_error(ErrorCode::BANK_PATCH_BAD_OLD_PASSWORD); + // Check require old password for user + ctx.patcha("/accounts/customer/auth") + .json(json!({ + "new_password": "new-password" + })) + .await + .assert_error(ErrorCode::BANK_NON_ADMIN_PATCH_MISSING_OLD_PASSWORD); + // Testing short password + ctx.patcha("/accounts/customer/auth") + .json(json!({ + "old_password": "ignored", + "new_password": "short" + })) + .await + .assert_error(ErrorCode::BANK_PASSWORD_TOO_SHORT); + // Testing long password + ctx.patcha("/accounts/customer/auth") + .json(json!({ + "old_password": "ignored", + "new_password": "loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong-password" + })) + .await + .assert_error(ErrorCode::BANK_PASSWORD_TOO_LONG); + + // Check admin + ctx.patch_admin("/accounts/customer/auth") + .json(json!({ + "new_password": "customer-password" + })) + .await + .assert_no_content(); + + // Check 2FA + ctx.fill_tan_info("customer").await; + ctx.patcha("/accounts/customer/auth") + .json(json!({ + "old_password": "customer-password", + "new_password": "new-password" + })) + .await + .assert_challenge(&ctx) + .await + .assert_no_content(); + ctx.patch_admin("/accounts/customer/auth") + .json(json!({ + "new_password": "customer-password" + })) + .await + .assert_no_content(); + + // Check 2FA after password check + ctx.patcha("/accounts/customer/auth") + .json(json!({ + "old_password": "bad-password", + "new_password": "new-password" + })) + .await + .assert_error(ErrorCode::BANK_PATCH_BAD_OLD_PASSWORD); + + // No password check + let ctx = ctx.swap_cfg("test_no_password_check.conf").await; + ctx.patcha("/accounts/merchant/auth") + .json(json!({ + "old_password": "merchant-password", + "new_password": "short" + })) + .await + .assert_no_content(); + // Testing long password + ctx.patcha("/accounts/merchant/auth") + .json(json!({ + "old_password": "short", + "new_password": "loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong-password" + })) + .await + .assert_no_content(); + } + + #[tokio::test] + async fn list() { + let ctx = bank_setup_conf("test_no_conversion.conf").await; + ctx.auth_routine(Method::GET, "/accounts", Auth::Admin) + .await; + + // Remove default accounts + let accounts = ["merchant", "exchange", "customer"]; + for username in accounts { + ctx.delete_admin(format!("/accounts/{username}")) + .await + .assert_no_content(); + } + let req: ListBankAccountsResponse = ctx.get_admin("/accounts").await.assert_ok_json(); + for account in req.accounts { + assert_eq!(account.conversion_rate, None); + if accounts.contains(&account.username.as_str()) { + assert_eq!(account.status, AccountStatus::deleted); + } else { + assert_eq!(account.status, AccountStatus::active); + } + } + + // Hard delete accounts + let now = Timestamp::now(); + collect(&ctx.state.db, &now, &now, &now).await.unwrap(); + ctx.get("/public-accounts").await.assert_no_content(); + ctx.get_admin("/accounts").await.assert_ok(); + + let ctx = ctx.swap_cfg("test.conf").await; + + for _ in 0..3 { + ctx.create_conversion_rate_class().await; + } + + // Gen some public and private accounts + for i in 0..5 { + let m = i % 3; + ctx.post_admin("/accounts") + .json(json!({ + "username": format!("{i}"), + "password": "password", + "name": format!("Mr 1{i}"), + "is_public": i % 2 == 0, + "conversion_rate_class_id": if m > 0 && m <= 3 { + Some(m) + } else { + None + } + })) + .await + .assert_ok(); + } + // All public + let res: PublicAccountsResponse = ctx.get("/public-accounts").await.assert_ok_json(); + assert_eq!(res.public_accounts.len(), 3); + for acc in res.public_accounts { + assert_eq!(0, acc.username.parse::<u8>().unwrap() % 2) + } + // Conversion rate + let res: ListBankAccountsResponse = ctx.get_admin("/accounts").await.assert_ok_json(); + for acc in res.accounts { + assert_eq!( + acc.conversion_rate, + Some( + ctx.get_admin(format!("/accounts/{}/conversion-info/rate", acc.username)) + .await + .assert_ok_json() + ) + ); + } + // Filtering + let check_ids = async |query: &str, ids: &[&str]| { + let res = ctx.get_admin(format!("/accounts?{query}")).await; + if ids.is_empty() { + res.assert_no_content(); + } else { + let res: ListBankAccountsResponse = res.assert_ok_json(); + assert_eq!( + ids, + res.accounts + .into_iter() + .map(|it| it.username) + .collect::<Vec<_>>() + ) + } + }; + check_ids("", &["4", "3", "2", "1", "0", "admin"]).await; + check_ids("filter_name=1", &["4", "3", "2", "1", "0"]).await; + check_ids("filter_name=3", &["3"]).await; + check_ids("conversion_rate_class_id=1", &["4", "1"]).await; + check_ids("conversion_rate_class_id=2", &["2"]).await; + check_ids("conversion_rate_class_id=3", &[]).await; + check_ids("conversion_rate_class_id=4", &[]).await; + check_ids("conversion_rate_class_id=0", &["3", "0", "admin"]).await; + check_ids("conversion_rate_class_id=0&filter_name=1", &["3", "0"]).await; + for (id, num) in [(1, 2), (2, 1), (3, 0)] { + assert_eq!( + num, + ctx.get_admin(format!("/conversion-rate-classes/{id}")) + .await + .assert_ok_json::<ConversionRateClass>() + .num_users + ); + } + } +} diff --git a/libeufin-bank/src/api/cashout.rs b/libeufin-bank/src/api/cashout.rs @@ -0,0 +1,457 @@ +/* +* This file is part of LibEuFin. +* Copyright (C) 2026 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/> +*/ + +use std::sync::Arc; + +use axum::{ + Json, Router, + extract::State, + middleware::{self, Next}, + response::{IntoResponse, NoContent}, + routing::{get, post}, +}; +use compact_str::CompactString; +use jiff::Timestamp; +use serde::{Deserialize, Serialize}; +use taler_api::{ + error::{ApiResult, failure_code, not_implemented}, + extract::{Path, Query}, +}; +use taler_common::{ + api::{ShortHashCode, params::PageParams}, + error_code::ErrorCode, + types::{amount::Amount, timestamp::TalerTimestamp}, +}; + +use crate::{ + api::BankState, + auth::{AdminRAuth, UserRWAuth}, + db::cashout::{CreationResult, create, get_for_user, page_all, page_for_user}, + mfa::{CashoutOp, MfaReq}, +}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[allow(non_camel_case_types)] +pub enum CashoutStatus { + pending, + aborted, + confirmed, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct CashoutRequest { + pub request_uid: ShortHashCode, + pub subject: Option<String>, + pub amount_debit: Amount, + pub amount_credit: Amount, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct CashoutResponse { + pub cashout_id: u64, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Cashouts { + pub cashouts: Vec<CashoutInfo>, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct CashoutInfo { + pub cashout_id: u64, + pub status: CashoutStatus, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct GlobalCashouts { + pub cashouts: Vec<GlobalCashoutInfo>, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct GlobalCashoutInfo { + pub cashout_id: u64, + pub username: CompactString, + pub status: CashoutStatus, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct CashoutStatusResponse { + pub amount_debit: Amount, + pub amount_credit: Amount, + pub subject: String, + pub creation_time: TalerTimestamp, + pub confirmation_time: Option<TalerTimestamp>, +} + +pub fn cashout_api(state: Arc<BankState>) -> Router<Arc<BankState>> { + Router::new() + .route( + "/cashouts", + get( + async |Query(params): Query<PageParams>, + _: AdminRAuth, + State(state): State<Arc<BankState>>| { + let params = params.check()?; + + let cashouts = page_all(&state.db, &params).await?; + if cashouts.is_empty() { + ApiResult::Ok(NoContent.into_response()) + } else { + Ok(Json(GlobalCashouts { cashouts }).into_response()) + } + }, + ), + ) + .route( + "/accounts/{username}/cashouts", + post( + async |State(state): State<Arc<BankState>>, + MfaReq { mut auth, req, mfa }: MfaReq<CashoutOp>| { + state.cfg.check_regio(&req.amount_debit)?; + state.cfg.check_fiat(&req.amount_credit)?; + match create( + &state.db, + &auth.username, + &req.request_uid, + &req.amount_debit, + &req.amount_credit, + &req.subject.unwrap_or_default(), + &Timestamp::now(), + mfa.is_2fa(), + ) + .await? + { + CreationResult::Success(cashout_id) => { + Ok(Json(CashoutResponse { cashout_id }).into_response()) + } + CreationResult::UnderMin => { + Err(failure_code(ErrorCode::BANK_CONVERSION_AMOUNT_TO_SMALL)) + } + CreationResult::BadConversion => { + Err(failure_code(ErrorCode::BANK_BAD_CONVERSION)) + } + CreationResult::AccountNotFound => { + Err(failure_code(ErrorCode::BANK_UNKNOWN_ACCOUNT)) + } + CreationResult::AccountIsExchange => { + Err(failure_code(ErrorCode::BANK_ACCOUNT_IS_EXCHANGE)) + } + CreationResult::BalanceInsufficient => { + Err(failure_code(ErrorCode::BANK_UNALLOWED_DEBIT)) + } + CreationResult::RequestUidReuse => { + Err(failure_code(ErrorCode::BANK_TRANSFER_REQUEST_UID_REUSED)) + } + CreationResult::NoCashoutPayto => { + Err(failure_code(ErrorCode::BANK_CONFIRM_INCOMPLETE)) + } + CreationResult::TanRequired => Ok(mfa + .response_mfa(&mut auth, &state.db, &state.cfg.ctx) + .await? + .into_response()), + } + }, + ) + .get( + async |Query(params): Query<PageParams>, + auth: UserRWAuth, + State(state): State<Arc<BankState>>| { + let params = params.check()?; + + let cashouts = page_for_user(&state.db, &auth.username, &params).await?; + if cashouts.is_empty() { + ApiResult::Ok(NoContent.into_response()) + } else { + Ok(Json(Cashouts { cashouts }).into_response()) + } + }, + ), + ) + .route( + "/accounts/{username}/cashouts/{id}", + get( + async |Path((_, id)): Path<((), u64)>, + auth: UserRWAuth, + State(state): State<Arc<BankState>>| { + match get_for_user( + &state.db, + &state.cfg.regional_currency, + state.cfg.fiat_currency().unwrap(), + &auth.username, + id, + ) + .await? + { + Some(res) => Ok(Json(res)), + None => Err(failure_code(ErrorCode::BANK_TRANSACTION_NOT_FOUND)), + } + }, + ), + ) + .route_layer(middleware::from_fn_with_state( + state, + async |State(state): State<Arc<BankState>>, req, next: Next| { + if state.cfg.fiat.is_none() { + not_implemented().into_response() + } else { + next.run(req).await + } + }, + )) +} + +#[cfg(test)] +pub mod test { + use axum::http::Method; + use taler_common::{api::ShortHashCode, error_code::ErrorCode, types::amount::amount}; + use taler_test_utils::{ + json, + routine::{Page, routine_pagination}, + server::TestServer, + tasks, + }; + + use crate::api::{ + cashout::{CashoutResponse, CashoutStatusResponse, Cashouts, GlobalCashouts}, + conversion::ConversionRateClassResponse, + test::{Auth, MfaRequest, bank_setup}, + }; + + #[tokio::test] + async fn cashout() { + let ctx = bank_setup().await; + + ctx.auth_routine(Method::POST, "/accounts/merchant/cashouts", Auth::UserOnly) + .await; + ctx.auth_routine( + Method::GET, + "/accounts/merchant/cashouts/42", + Auth::UserOrAdmin, + ) + .await; + ctx.auth_routine( + Method::GET, + "/accounts/merchant/cashouts", + Auth::UserOrAdmin, + ) + .await; + ctx.auth_routine(Method::GET, "/cashouts", Auth::Admin) + .await; + + let str = "KUDOS:1.5"; + let debit = amount(str); + let credit = ctx.convert(str).await; + + let req = json!({ + "request_uid": ShortHashCode::rand(), + "amount_debit": debit, + "amount_credit": credit, + "subject": "test subject" + }); + + // Missing info + ctx.posta("/accounts/customer/cashouts") + .json(&req) + .await + .assert_error(ErrorCode::BANK_CONFIRM_INCOMPLETE); + + ctx.fill_cashout_info("customer").await; + + // Ok + let res = ctx + .posta("/accounts/customer/cashouts") + .json(&req) + .await + .assert_ok_json::<CashoutResponse>(); + // Idempotent + assert_eq!( + res, + ctx.posta("/accounts/customer/cashouts") + .json(&req) + .await + .assert_ok_json::<CashoutResponse>() + ); + + // Trigger conflict due to reused request_uid + ctx.posta("/accounts/customer/cashouts") + .json(json!(req + { + "amount_debit": "KUDOS:2", + "amount_credit": ctx.convert("KUDOS:2").await + })) + .await + .assert_error(ErrorCode::BANK_TRANSFER_REQUEST_UID_REUSED); + + // Check exchange account + ctx.posta("/accounts/exchange/cashouts") + .json(&req) + .await + .assert_error(ErrorCode::BANK_ACCOUNT_IS_EXCHANGE); + + // Check insufficient fund + ctx.posta("/accounts/customer/cashouts") + .json(json!({ + "request_uid": ShortHashCode::rand(), + "amount_debit": "KUDOS:75", + "amount_credit": ctx.convert("KUDOS:75").await, + })) + .await + .assert_error(ErrorCode::BANK_UNALLOWED_DEBIT); + + // Check wrong conversion + ctx.posta("/accounts/customer/cashouts") + .json(json!(req + { + "amount_credit": ctx.convert("KUDOS:2").await + })) + .await + .assert_error(ErrorCode::BANK_BAD_CONVERSION); + + // Check min amount + ctx.posta("/accounts/customer/cashouts") + .json(json!(req + { "amount_debit": "KUDOS:0.09" })) + .await + .assert_error(ErrorCode::BANK_CONVERSION_AMOUNT_TO_SMALL); + + // Check custom min account + let id = ctx + .post_admin("/conversion-rate-classes") + .json(json!({ + "name": "Custom class", + "cashout_min_amount": "KUDOS:10" + })) + .await + .assert_ok_json::<ConversionRateClassResponse>() + .conversion_rate_class_id; + ctx.patch_admin("/accounts/customer") + .json(json!({ + "conversion_rate_class_id": id + })) + .await + .assert_no_content(); + ctx.posta("/accounts/customer/cashouts") + .json(json!(req + { + "amount_debit": "KUDOS:5", + "amount_credit": ctx.convert("KUDOS:5").await + })) + .await + .assert_error(ErrorCode::BANK_CONVERSION_AMOUNT_TO_SMALL); + ctx.patch_admin("/accounts/customer") + .json(json!({ + "conversion_rate_class_id": () + })) + .await + .assert_no_content(); + + // Wrong currency + ctx.posta("/accounts/customer/cashouts") + .json(json!(req + { "amount_debit": "EUR:1" })) + .await + .assert_error(ErrorCode::GENERIC_CURRENCY_MISMATCH); + ctx.posta("/accounts/customer/cashouts") + .json(json!(req + { "amount_credit": "KUDOS:1" })) + .await + .assert_error(ErrorCode::GENERIC_CURRENCY_MISMATCH); + + // 2fa + ctx.fill_tan_info("merchant").await; + ctx.fill_cashout_info("merchant").await; + ctx.assert_balance("merchant", "0").await; + ctx.posta("/accounts/merchant/cashouts") + .json(json!(req + { "request_uid": ShortHashCode::rand() })) + .await + .assert_challenge_check(&ctx, async |_| ctx.assert_balance("merchant", "0").await) + .await + .assert_ok_json::<CashoutResponse>(); + ctx.assert_balance("merchant", "-1.5").await; + + // Get + let c = ctx + .geta(format!("/accounts/customer/cashouts/{}", res.cashout_id)) + .await + .assert_ok_json::<CashoutStatusResponse>(); + assert_eq!( + c, + CashoutStatusResponse { + amount_debit: debit, + amount_credit: credit, + subject: "test subject".into(), + creation_time: c.creation_time, + confirmation_time: Some(c.creation_time) + } + ); + + // Bad ID + ctx.geta("/accounts/customer/cashouts/chocolate") + .await + .assert_error(ErrorCode::GENERIC_PATH_SEGMENT_MALFORMED); + + // Unknown + ctx.geta("/accounts/customer/cashouts/42") + .await + .assert_error(ErrorCode::BANK_TRANSACTION_NOT_FOUND); + + // Another user's operation + ctx.geta(format!("/accounts/merchant/cashouts/{}", res.cashout_id)) + .await + .assert_error(ErrorCode::BANK_TRANSACTION_NOT_FOUND); + + // History + let router = ctx.admin_router().await; + let c = &ctx; + routine_pagination::<Cashouts>( + &router, + "/accounts/customer/cashouts", + tasks!({ + c.cashout("0.1").await; + }), + ) + .await; + routine_pagination::<Cashouts>( + &router, + "/cashouts", + tasks!({ + c.cashout("0.1").await; + }), + ) + .await; + + // Not implemented + let ctx = ctx.swap_cfg("test_no_conversion.conf").await; + ctx.get("/accounts/customer/cashouts") + .await + .assert_not_implemented(); + } + + impl Page for Cashouts { + fn ids(&self) -> Vec<i64> { + self.cashouts + .iter() + .map(|it| it.cashout_id as i64) + .collect() + } + } + + impl Page for GlobalCashouts { + fn ids(&self) -> Vec<i64> { + self.cashouts + .iter() + .map(|it| it.cashout_id as i64) + .collect() + } + } +} diff --git a/libeufin-bank/src/api/conversion.rs b/libeufin-bank/src/api/conversion.rs @@ -0,0 +1,1015 @@ +/* +* This file is part of LibEuFin. +* Copyright (C) 2026 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/> +*/ + +use std::sync::Arc; + +use axum::{ + Json, Router, + extract::{FromRequestParts, State}, + middleware::{self, Next}, + response::{IntoResponse, NoContent, Response}, + routing::{get, patch, post}, +}; +use compact_str::{CompactString, format_compact}; +use serde::{Deserialize, Serialize}; +use sqlx::prelude::Type; +use taler_api::{ + error::{ApiError, ApiResult, bad_request, failure, failure_code, forbidden, not_implemented}, + extract::{Path, Query, Req}, +}; +use taler_common::{ + api::params::{Page, PageParams, ParamsErr}, + error_code::ErrorCode, + types::amount::{Amount, Currency, Decimal}, +}; +use taler_macros::{EnumMeta, api_config}; + +use crate::{ + api::{BankState, CONVERSION_API_VERSION, IMPLEMENTATION}, + auth::{AdminRAuth, AdminRWAuth, UserOptRAuth, UserRAuth}, + config::{BankCfg, CurrencySpecification}, + db::conversion::{ + ConversionResult, CreateResult, PatchResult, conversion, create, delete_class, get_class, + get_class_rate, get_default_rate, get_user_rate, page_class, patch_class, update_config, + user_conversion, + }, +}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, EnumMeta, Type)] +#[enum_meta(Str)] +#[sqlx(type_name = "rounding_mode")] +#[allow(non_camel_case_types)] +pub enum RoundingMode { + zero, + up, + nearest, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ConversionRate { + pub cashin_ratio: Decimal, + pub cashin_fee: Amount, + pub cashin_tiny_amount: Amount, + pub cashin_rounding_mode: RoundingMode, + pub cashin_min_amount: Amount, + pub cashout_ratio: Decimal, + pub cashout_fee: Amount, + pub cashout_tiny_amount: Amount, + pub cashout_rounding_mode: RoundingMode, + pub cashout_min_amount: Amount, +} + +impl ConversionRate { + pub fn check(&self, cfg: &BankCfg) -> ApiResult<()> { + let Self { + cashin_fee, + cashin_tiny_amount, + cashin_min_amount, + cashout_fee, + cashout_tiny_amount, + cashout_min_amount, + .. + } = self; + for amount in &[cashin_fee, cashin_tiny_amount, cashout_min_amount] { + cfg.check_regio(amount)?; + } + for amount in &[cashout_fee, cashout_tiny_amount, cashin_min_amount] { + cfg.check_fiat(amount)?; + } + + if cashout_tiny_amount.is_zero() { + Err(bad_request("cashout_tiny_amount must be > 0")) + } else if cashin_tiny_amount.is_zero() { + Err(bad_request("cashin_tiny_amount must be > 0")) + } else if cashout_tiny_amount.is_sub_cent() { + Err(bad_request( + "Sub-cent amounts no supported by cashout, cashout_tiny_amount must be >= 0.01", + )) + } else { + Ok(()) + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ConversionResponse { + pub amount_debit: Amount, + pub amount_credit: Amount, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ConversionRateClassInput { + pub name: CompactString, + pub description: Option<CompactString>, + pub cashin_ratio: Option<Decimal>, + pub cashin_fee: Option<Amount>, + pub cashin_rounding_mode: Option<RoundingMode>, + pub cashin_min_amount: Option<Amount>, + pub cashout_ratio: Option<Decimal>, + pub cashout_fee: Option<Amount>, + pub cashout_rounding_mode: Option<RoundingMode>, + pub cashout_min_amount: Option<Amount>, +} + +impl ConversionRateClassInput { + pub fn check(&self, cfg: &BankCfg) -> ApiResult<()> { + let Self { + cashin_fee, + cashin_min_amount, + cashout_fee, + cashout_min_amount, + .. + } = self; + for amount in [cashin_fee, cashout_min_amount].into_iter().flatten() { + cfg.check_regio(amount)?; + } + for amount in [cashout_fee, cashin_min_amount].into_iter().flatten() { + cfg.check_fiat(amount)?; + } + Ok(()) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ConversionRateClassResponse { + pub conversion_rate_class_id: u64, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ConversionRateClass { + pub conversion_rate_class_id: u64, + pub name: CompactString, + pub description: Option<CompactString>, + pub num_users: u64, + pub cashin_ratio: Option<Decimal>, + pub cashin_fee: Option<Amount>, + pub cashin_rounding_mode: Option<RoundingMode>, + pub cashin_min_amount: Option<Amount>, + pub cashout_ratio: Option<Decimal>, + pub cashout_fee: Option<Amount>, + pub cashout_rounding_mode: Option<RoundingMode>, + pub cashout_min_amount: Option<Amount>, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ConversionRateClasses { + pub classes: Vec<ConversionRateClass>, +} + +#[api_config("taler-conversion-info")] +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct ConversionConfig<'a> { + pub regional_currency: &'a Currency, + pub regional_currency_specification: &'a CurrencySpecification, + pub fiat_currency: &'a Currency, + pub fiat_currency_specification: &'a CurrencySpecification, + pub conversion_rate: &'a ConversionRate, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct ClassParams { + #[serde(flatten)] + pub page: PageParams, + pub filter_name: Option<CompactString>, +} + +impl ClassParams { + pub fn check(self) -> Result<Class, ParamsErr> { + Ok(Class { + page: self.page.check()?, + filter_name: self.filter_name.map(|it| format_compact!("%{it}%")), + }) + } +} + +#[derive(Debug)] +pub struct Class { + pub page: Page, + pub filter_name: Option<CompactString>, +} + +pub struct Conversion; + +impl FromRequestParts<Arc<BankState>> for Conversion { + type Rejection = ApiError; + + async fn from_request_parts( + _: &mut axum::http::request::Parts, + state: &Arc<BankState>, + ) -> Result<Self, Self::Rejection> { + if state.cfg.fiat.is_none() { + Err(not_implemented()) + } else { + Ok(Conversion) + } + } +} + +#[derive(Debug, Clone, Deserialize)] +pub struct RateParams { + amount_debit: Option<Amount>, + amount_credit: Option<Amount>, +} + +impl RateParams { + pub fn check(self, cfg: &BankCfg, cashin: bool) -> ApiResult<Rate> { + let rate: Rate = { + match (self.amount_credit, self.amount_debit) { + (None, None) => { + return Err(failure( + ErrorCode::GENERIC_PARAMETER_MISSING, + "Either param 'amount_debit' or 'amount_credit' is require", + )); + } + (Some(_), Some(_)) => { + return Err(failure( + ErrorCode::GENERIC_PARAMETER_MALFORMED, + "Cannot have both 'amount_debit' and 'amount_credit' params", + )); + } + (None, Some(amount_debit)) => Rate::Debit { amount_debit }, + (Some(amount_credit), None) => Rate::Credit { amount_credit }, + } + }; + if cashin { + match &rate { + Rate::Debit { amount_debit } => cfg.check_fiat(amount_debit)?, + Rate::Credit { amount_credit } => cfg.check_regio(amount_credit)?, + } + } else { + match &rate { + Rate::Debit { amount_debit } => cfg.check_regio(amount_debit)?, + Rate::Credit { amount_credit } => cfg.check_fiat(amount_credit)?, + } + }; + Ok(rate) + } +} + +#[derive(Debug, Clone)] +pub enum Rate { + Debit { amount_debit: Amount }, + Credit { amount_credit: Amount }, +} + +async fn config(State(state): State<Arc<BankState>>) -> ApiResult<Response> { + let (fiat, spec) = state.cfg.fiat.as_ref().unwrap(); + let rate = get_default_rate(&state.db, &state.cfg.regional_currency, fiat).await?; + ApiResult::Ok( + Json(ConversionConfig { + name: (), + version: CONVERSION_API_VERSION, + implementation: Some(IMPLEMENTATION), + conversion_rate: &rate, + fiat_currency: fiat, + fiat_currency_specification: spec, + regional_currency: &state.cfg.regional_currency, + regional_currency_specification: &state.cfg.regional_currency_spec, + }) + .into_response(), + ) +} + +async fn update_cfg( + State(state): State<Arc<BankState>>, + _: AdminRWAuth, + Req(req): Req<ConversionRate>, +) -> ApiResult<NoContent> { + req.check(&state.cfg)?; + update_config(&state.db, &req).await?; + ApiResult::Ok(NoContent) +} + +async fn convert( + cfg: &BankCfg, + params: RateParams, + cashin: bool, + lambda: impl AsyncFnOnce(bool, &Amount) -> sqlx::Result<ConversionResult>, +) -> ApiResult<Response> { + let params = params.check(cfg, cashin)?; + + fn err_handling(res: sqlx::Result<ConversionResult>) -> ApiResult<Amount> { + match res? { + ConversionResult::Success(amount) => Ok(amount), + ConversionResult::ToSmall => Err(failure( + ErrorCode::BANK_BAD_CONVERSION, + "amount is too small to be converted", + )), + ConversionResult::IsExchange => Err(failure( + ErrorCode::BANK_ACCOUNT_IS_EXCHANGE, + "exchange accounts cannot cashout", + )), + ConversionResult::NotExchange => Err(failure( + ErrorCode::BANK_ACCOUNT_IS_NOT_EXCHANGE, + "only exchange accounts can cashin", + )), + } + } + + let (amount_debit, amount_credit) = match params { + Rate::Debit { amount_debit } => ( + amount_debit, + err_handling(lambda(true, &amount_debit).await)?, + ), + Rate::Credit { amount_credit } => ( + err_handling(lambda(false, &amount_credit).await)?, + amount_credit, + ), + }; + Ok(Json(ConversionResponse { + amount_debit, + amount_credit, + }) + .into_response()) +} + +pub fn conversion_api(state: Arc<BankState>) -> Router<Arc<BankState>> { + Router::new() + .route("/conversion-info/config", get(config)) + .route( + "/conversion-rate-classes/{id}/conversion-info/config", + get(config), + ) + .route("/accounts/{username}/conversion-info/config", get(config)) + .route("/conversion-info/conversion-rate", post(update_cfg)) + .route( + "/conversion-rate-classes/{id}/conversion-info/conversion-rate", + post(update_cfg), + ) + .route( + "/accounts/{username}/conversion-info/conversion-rate", + post(update_cfg), + ) + .route( + "/conversion-info/rate", + get(async |State(state): State<Arc<BankState>>| { + ApiResult::Ok(Json( + get_default_rate( + &state.db, + &state.cfg.regional_currency, + state.cfg.fiat_currency().unwrap(), + ) + .await?, + )) + }), + ) + .route( + "/conversion-info/cashout-rate", + get( + async |State(state): State<Arc<BankState>>, Query(params): Query<RateParams>| { + let regio = &state.cfg.regional_currency; + let fiat = state.cfg.fiat_currency().unwrap(); + convert(&state.cfg, params, false, async |to, amount| { + conversion(&state.db, regio, fiat, amount, to, false, None).await + }) + .await + }, + ), + ) + .route( + "/conversion-info/cashin-rate", + get( + async |State(state): State<Arc<BankState>>, Query(params): Query<RateParams>| { + let regio = &state.cfg.regional_currency; + let fiat = state.cfg.fiat_currency().unwrap(); + convert(&state.cfg, params, true, async |to, amount| { + conversion(&state.db, regio, fiat, amount, to, true, None).await + }) + .await + }, + ), + ) + .route( + "/conversion-rate-classes/{id}/conversion-info/rate", + get( + async |_: AdminRAuth, Path(id): Path<u64>, State(state): State<Arc<BankState>>| { + ApiResult::Ok(Json( + get_class_rate( + &state.db, + &state.cfg.regional_currency, + state.cfg.fiat_currency().unwrap(), + id, + ) + .await?, + )) + }, + ), + ) + .route( + "/conversion-rate-classes/{id}/conversion-info/cashout-rate", + get( + async |_: AdminRAuth, + Path(id): Path<u64>, + Query(params): Query<RateParams>, + State(state): State<Arc<BankState>>| { + let regio = &state.cfg.regional_currency; + let fiat = state.cfg.fiat_currency().unwrap(); + convert(&state.cfg, params, false, async |to, amount| { + conversion(&state.db, regio, fiat, amount, to, false, Some(id)).await + }) + .await + }, + ), + ) + .route( + "/conversion-rate-classes/{id}/conversion-info/cashin-rate", + get( + async |_: AdminRAuth, + Path(id): Path<u64>, + Query(params): Query<RateParams>, + State(state): State<Arc<BankState>>| { + let regio = &state.cfg.regional_currency; + let fiat = state.cfg.fiat_currency().unwrap(); + convert(&state.cfg, params, true, async |to, amount| { + conversion(&state.db, regio, fiat, amount, to, true, Some(id)).await + }) + .await + }, + ), + ) + .route( + "/accounts/{username}/conversion-info/rate", + get( + async |auth: UserOptRAuth, State(state): State<Arc<BankState>>| { + let (is_exchange, rate) = get_user_rate( + &state.db, + &state.cfg.regional_currency, + state.cfg.fiat_currency(), + &auth.username, + ) + .await?; + if !is_exchange && !auth.is_authenticated() { + return Err(forbidden("Non exchange account rates are private")); + } + Ok(Json(rate)) + }, + ), + ) + .route( + "/accounts/{username}/conversion-info/cashout-rate", + get( + async |auth: UserRAuth, + Query(params): Query<RateParams>, + State(state): State<Arc<BankState>>| { + let regio = &state.cfg.regional_currency; + let fiat = state.cfg.fiat_currency().unwrap(); + convert(&state.cfg, params, false, async |to, amount| { + user_conversion(&state.db, regio, fiat, amount, to, false, &auth.username) + .await + }) + .await + }, + ), + ) + .route( + "/accounts/{username}/conversion-info/cashin-rate", + get( + async |Path(username): Path<CompactString>, + Query(params): Query<RateParams>, + State(state): State<Arc<BankState>>| { + let regio = &state.cfg.regional_currency; + let fiat = state.cfg.fiat_currency().unwrap(); + convert(&state.cfg, params, true, async |to, amount| { + user_conversion(&state.db, regio, fiat, amount, to, true, &username).await + }) + .await + }, + ), + ) + .route( + "/conversion-rate-classes", + post( + async |_: AdminRWAuth, + State(state): State<Arc<BankState>>, + Req(req): Req<ConversionRateClassInput>| { + req.check(&state.cfg)?; + match create(&state.db, &req).await? { + CreateResult::Success(id) => Ok(Json(ConversionRateClassResponse { + conversion_rate_class_id: id, + })), + CreateResult::NameReuse => Err(failure_code(ErrorCode::BANK_NAME_REUSE)), + } + }, + ) + .get( + async |_: AdminRAuth, + Query(params): Query<ClassParams>, + State(state): State<Arc<BankState>>| { + let params = params.check()?; + let classes = page_class( + &state.db, + &state.cfg.regional_currency, + state.cfg.fiat_currency().unwrap(), + &params, + ) + .await?; + if classes.is_empty() { + ApiResult::Ok(NoContent.into_response()) + } else { + ApiResult::Ok(Json(ConversionRateClasses { classes }).into_response()) + } + }, + ), + ) + .route( + "/conversion-rate-classes/{id}", + patch( + async |_: AdminRWAuth, + Path(id): Path<u64>, + State(state): State<Arc<BankState>>, + Req(req): Req<ConversionRateClassInput>| { + req.check(&state.cfg)?; + match patch_class(&state.db, id, &req).await? { + PatchResult::Success => Ok(NoContent), + PatchResult::Unknown => { + Err(failure_code(ErrorCode::BANK_TRANSACTION_NOT_FOUND)) + } + PatchResult::NameReuse => Err(failure_code(ErrorCode::BANK_NAME_REUSE)), + } + }, + ) + .delete( + async |_: AdminRWAuth, Path(id): Path<u64>, State(state): State<Arc<BankState>>| { + if delete_class(&state.db, id).await? { + Ok(NoContent) + } else { + Err(failure_code(ErrorCode::BANK_TRANSACTION_NOT_FOUND)) + } + }, + ) + .get( + async |_: AdminRAuth, Path(id): Path<u64>, State(state): State<Arc<BankState>>| { + if let Some(class) = get_class( + &state.db, + &state.cfg.regional_currency, + state.cfg.fiat_currency().unwrap(), + id, + ) + .await? + { + Ok(Json(class)) + } else { + Err(failure_code(ErrorCode::BANK_TRANSACTION_NOT_FOUND)) + } + }, + ), + ) + .route_layer(middleware::from_fn_with_state( + state, + async |State(state): State<Arc<BankState>>, req, next: Next| { + if state.cfg.fiat.is_none() { + not_implemented().into_response() + } else { + next.run(req).await + } + }, + )) +} + +#[cfg(test)] +pub mod test { + + use axum::http::Method; + use taler_common::{error_code::ErrorCode, types::amount::amount}; + use taler_test_utils::{json, server::TestServer}; + + use crate::api::{ + conversion::{ + ConversionRate, ConversionRateClass, ConversionRateClassResponse, + ConversionRateClasses, ConversionResponse, + }, + test::{Auth, bank_setup}, + }; + + #[tokio::test] + async fn rates() { + let ctx = bank_setup().await; + + // Auth + for path in [ + "/conversion-rate-classes", + "/conversion-info/conversion-rate", + "/accounts/merchant/conversion-info/conversion-rate", + "/conversion-rate-classes/1/conversion-info/conversion-rate", + ] { + ctx.auth_routine(Method::POST, path, Auth::Admin).await; + } + ctx.auth_routine( + Method::GET, + "/accounts/merchant/conversion-info/rate", + Auth::Optional, + ) + .await; + ctx.auth_routine( + Method::GET, + "/conversion-rate-classes/1/conversion-info/rate", + Auth::Admin, + ) + .await; + for path in [ + "/conversion-rate-classes", + "/conversion-rate-classes/1", + "/conversion-rate-classes/1/conversion-info/cashout-rate?amount_debit=KUDOS:1", + ] { + ctx.auth_routine(Method::GET, path, Auth::Admin).await; + } + ctx.auth_routine( + Method::GET, + "/accounts/merchant/conversion-info/cashout-rate?amount_debit=KUDOS:1", + Auth::UserOrAdmin, + ) + .await; + + // Conversion rate + { + let ok = json!({ + "cashin_ratio": "0.8", + "cashin_fee": "KUDOS:0.02", + "cashin_tiny_amount": "KUDOS:0.01", + "cashin_rounding_mode": "nearest", + "cashin_min_amount": "EUR:0", + "cashout_ratio": "1.25", + "cashout_fee": "EUR:0.003", + "cashout_tiny_amount": "EUR:0.01", + "cashout_rounding_mode": "zero", + "cashout_min_amount": "KUDOS:0.1" + }); + for prefix in ["", "/conversion-rate-classes/1", "/accounts/merchant"] { + // Good rates + ctx.post_admin(format!("{prefix}/conversion-info/conversion-rate")) + .json(&ok) + .await + .assert_no_content(); + // Bad currency + ctx.post_admin(format!("{prefix}/conversion-info/conversion-rate")) + .json(json!(ok + { "cashout_fee": "CHF:0.003" })) + .await + .assert_error(ErrorCode::GENERIC_CURRENCY_MISMATCH); + // Zero tiny amount + ctx.post_admin(format!("{prefix}/conversion-info/conversion-rate")) + .json(json!(ok + { "cashout_tiny_amount": "EUR:0" })) + .await + .assert_bad_request(); + ctx.post_admin(format!("{prefix}/conversion-info/conversion-rate")) + .json(json!(ok + { "cashin_tiny_amount": "KUDOS:0" })) + .await + .assert_bad_request(); + // Subcent cashout tiny amount + ctx.post_admin(format!("{prefix}/conversion-info/conversion-rate")) + .json(json!(ok + { "cashout_tiny_amount": "EUR:0.0001" })) + .await + .assert_bad_request() + } + } + + // User rate + { + ctx.get("/conversion-info/rate") + .await + .assert_ok_json::<ConversionRate>(); + ctx.geta("/accounts/merchant/conversion-info/rate") + .await + .assert_ok_json::<ConversionRate>(); + ctx.get("/accounts/exchange/conversion-info/rate") + .await + .assert_ok_json::<ConversionRate>(); + ctx.get_admin("/conversion-rate-classes/1/conversion-info/rate") + .await + .assert_ok_json::<ConversionRate>(); + } + + // Cashout rate + { + for prefix in ["", "/conversion-rate-classes/1", "/accounts/merchant"] { + // Check conversion to + let res: ConversionResponse = ctx + .get_admin(format!( + "{prefix}/conversion-info/cashout-rate?amount_debit=KUDOS:1" + )) + .await + .assert_ok_json(); + assert_eq!(res.amount_debit, amount("KUDOS:1")); + assert_eq!(res.amount_credit, amount("EUR:1.25")); + + // Check conversion from + let res: ConversionResponse = ctx + .get_admin(format!( + "{prefix}/conversion-info/cashout-rate?amount_credit=EUR:1.257" + )) + .await + .assert_ok_json(); + assert_eq!(res.amount_debit, amount("KUDOS:1.01")); + assert_eq!(res.amount_credit, amount("EUR:1.257")); + + // Too small + ctx.get_admin(format!( + "{prefix}/conversion-info/cashout-rate?amount_debit=KUDOS:0.0008" + )) + .await + .assert_error(ErrorCode::BANK_BAD_CONVERSION); + // No amount + ctx.get_admin(format!("{prefix}/conversion-info/cashout-rate")) + .await + .assert_error(ErrorCode::GENERIC_PARAMETER_MISSING); + // Both amount + ctx.get_admin(format!( + "{prefix}/conversion-info/cashout-rate?amount_debit=EUR:1&amount_credit=KUDOS:1" + )) + .await + .assert_error(ErrorCode::GENERIC_PARAMETER_MALFORMED); + // Wrong format + ctx.get_admin(format!( + "{prefix}/conversion-info/cashout-rate?amount_debit=1" + )) + .await + .assert_error(ErrorCode::GENERIC_PARAMETER_MALFORMED); + ctx.get_admin(format!( + "{prefix}/conversion-info/cashout-rate?amount_credit=1" + )) + .await + .assert_error(ErrorCode::GENERIC_PARAMETER_MALFORMED); + // Wrong currency + ctx.get_admin(format!( + "{prefix}/conversion-info/cashout-rate?amount_debit=EUR:1" + )) + .await + .assert_error(ErrorCode::GENERIC_CURRENCY_MISMATCH); + ctx.get_admin(format!( + "{prefix}/conversion-info/cashout-rate?amount_credit=KUDOS:1" + )) + .await + .assert_error(ErrorCode::GENERIC_CURRENCY_MISMATCH); + } + ctx.geta("/accounts/exchange/conversion-info/cashout-rate?amount_debit=KUDOS:1") + .await + .assert_error(ErrorCode::BANK_ACCOUNT_IS_EXCHANGE); + } + + // Cashin rate + { + for prefix in ["", "/conversion-rate-classes/1", "/accounts/exchange"] { + for (from, to) in [(0.75, 0.58), (0.32, 0.24), (0.66, 0.51)] { + // Check conversion to + let res: ConversionResponse = ctx + .get_admin(format!( + "{prefix}/conversion-info/cashin-rate?amount_debit=EUR:{from}" + )) + .await + .assert_ok_json(); + assert_eq!(res.amount_credit, amount(format!("KUDOS:{to}"))); + assert_eq!(res.amount_debit, amount(format!("EUR:{from}"))); + + // Check conversion from + let res: ConversionResponse = ctx + .get_admin(format!( + "{prefix}/conversion-info/cashin-rate?amount_credit=KUDOS:{to}" + )) + .await + .assert_ok_json(); + assert_eq!(res.amount_credit, amount(format!("KUDOS:{to}"))); + assert_eq!(res.amount_debit, amount(format!("EUR:{from}"))); + } + + // No amount + ctx.get_admin(format!("{prefix}/conversion-info/cashin-rate")) + .await + .assert_error(ErrorCode::GENERIC_PARAMETER_MISSING); + // Both amount + ctx.get_admin(format!( + "{prefix}/conversion-info/cashin-rate?amount_debit=EUR:1&amount_credit=KUDOS:1" + )) + .await + .assert_error(ErrorCode::GENERIC_PARAMETER_MALFORMED); + // Wrong format + ctx.get_admin(format!( + "{prefix}/conversion-info/cashin-rate?amount_debit=1" + )) + .await + .assert_error(ErrorCode::GENERIC_PARAMETER_MALFORMED); + ctx.get_admin(format!( + "{prefix}/conversion-info/cashin-rate?amount_credit=1" + )) + .await + .assert_error(ErrorCode::GENERIC_PARAMETER_MALFORMED); + // Wrong currency + ctx.get_admin(format!( + "{prefix}/conversion-info/cashin-rate?amount_debit=KUDOS:1" + )) + .await + .assert_error(ErrorCode::GENERIC_CURRENCY_MISMATCH); + ctx.get_admin(format!( + "{prefix}/conversion-info/cashin-rate?amount_credit=EUR:1" + )) + .await + .assert_error(ErrorCode::GENERIC_CURRENCY_MISMATCH); + } + ctx.geta("/accounts/merchant/conversion-info/cashin-rate?amount_debit=EUR:1") + .await + .assert_error(ErrorCode::BANK_ACCOUNT_IS_NOT_EXCHANGE); + } + + let prefixes = ["", "/conversion-rate-classes/1", "/accounts/merchant"]; + + // No rate + sqlx::query("DELETE FROM config WHERE key='conversion_rate'") + .execute(&ctx.state.db) + .await + .unwrap(); + for prefix in prefixes { + ctx.get(format!("{prefix}/conversion-info/config")) + .await + .assert_ok(); + ctx.get_admin(format!("{prefix}/conversion-info/cashin-rate")) + .await + .assert_error(ErrorCode::GENERIC_PARAMETER_MISSING); + ctx.get_admin(format!( + "{prefix}/conversion-info/cashout-rate?amount_credit=EUR:1" + )) + .await + .assert_error(ErrorCode::BANK_BAD_CONVERSION); + } + + // Not implemented + let ctx = ctx.swap_cfg("test_no_conversion.conf").await; + ctx.get_admin("/conversion-rate-classes") + .await + .assert_not_implemented(); + ctx.get_admin("/conversion-rate-classes/1") + .await + .assert_not_implemented(); + + for prefix in prefixes { + ctx.get(format!("{prefix}/conversion-info/rate")) + .await + .assert_not_implemented(); + ctx.get(format!("{prefix}/conversion-info/cashin-rate")) + .await + .assert_not_implemented(); + ctx.get(format!("{prefix}/conversion-info/cashout-rate")) + .await + .assert_not_implemented(); + } + } + + #[tokio::test] + async fn classes() { + let ctx = bank_setup().await; + + // Config + for prefix in ["", "/conversion-rate-classes/1", "/accounts/merchant"] { + ctx.get(format!("{prefix}/conversion-info/config")) + .await + .assert_ok(); + } + + let full = json!({ + "description": "A nice little class", + "cashin_ratio": "0.1", + "cashin_fee": "KUDOS:0.2", + "cashin_tiny_amount": "KUDOS:0.3", + "cashin_rounding_mode": "nearest", + "cashin_min_amount": "EUR:0", + "cashout_ratio": "0.4", + "cashout_fee": "EUR:0.5", + "cashout_tiny_amount": "EUR:0.6", + "cashout_rounding_mode": "zero", + "cashout_min_amount": "KUDOS:0.7" + }); + + // Create full + let res: ConversionRateClassResponse = ctx + .post_admin("/conversion-rate-classes") + .json(json!(full + { "name": "Class n°1" })) + .await + .assert_ok_json(); + assert_eq!(res.conversion_rate_class_id, 2); + let rate: ConversionRateClass = ctx + .get_admin("/conversion-rate-classes/2") + .await + .assert_ok_json(); + ctx.patch_admin("/conversion-rate-classes/2") + .json(json!(full + { "name": "Class n°1" })) + .await + .assert_no_content(); + assert_eq!( + rate, + ctx.get_admin("/conversion-rate-classes/2") + .await + .assert_ok_json() + ); + + // Create empty + let res: ConversionRateClassResponse = ctx + .post_admin("/conversion-rate-classes") + .json(json!({ "name": "Class n°2" })) + .await + .assert_ok_json(); + assert_eq!(res.conversion_rate_class_id, 3); + let rate: ConversionRateClass = ctx + .get_admin("/conversion-rate-classes/3") + .await + .assert_ok_json(); + ctx.patch_admin("/conversion-rate-classes/3") + .json(json!({ "name": "Class n°2" })) + .await + .assert_no_content(); + assert_eq!( + rate, + ctx.get_admin("/conversion-rate-classes/3") + .await + .assert_ok_json() + ); + + // Bad currency + ctx.post_admin("/conversion-rate-classes") + .json(json!(full + { + "name": "Bad currency", + "cashout_fee": "CHF:0.003" + })) + .await + .assert_error(ErrorCode::GENERIC_CURRENCY_MISMATCH); + + // Name reuse currency + ctx.post_admin("/conversion-rate-classes") + .json(json!(full + { "name": "Class n°1" })) + .await + .assert_error(ErrorCode::BANK_NAME_REUSE); + ctx.patch_admin("/conversion-rate-classes/3") + .json(json!(full + { "name": "Class n°1" })) + .await + .assert_error(ErrorCode::BANK_NAME_REUSE); + + // Page + assert_eq!( + 3, + ctx.get_admin("/conversion-rate-classes") + .await + .assert_ok_json::<ConversionRateClasses>() + .classes + .len() + ); + for _ in 0..4 { + ctx.create_conversion_rate_class().await; + } + assert_eq!( + 7, + ctx.get_admin("/conversion-rate-classes") + .await + .assert_ok_json::<ConversionRateClasses>() + .classes + .len() + ); + assert_eq!( + 5, + ctx.get_admin("/conversion-rate-classes?filter_name=Gen") + .await + .assert_ok_json::<ConversionRateClasses>() + .classes + .len() + ); + + // Delete all + for id in (1..4).chain(5..9) { + ctx.delete_admin(format!("/conversion-rate-classes/{id}")) + .await + .assert_no_content(); + ctx.delete_admin(format!("/conversion-rate-classes/{id}")) + .await + .assert_error(ErrorCode::BANK_TRANSACTION_NOT_FOUND); + } + ctx.get_admin("/conversion-rate-classes?filter_name=Gen") + .await + .assert_no_content(); + + // Check no classes + ctx.get_admin("/conversion-rate-classes") + .await + .assert_no_content(); + ctx.get_admin("/conversion-rate-classes/1") + .await + .assert_error(ErrorCode::BANK_TRANSACTION_NOT_FOUND); + ctx.patch_admin("/conversion-rate-classes/1") + .json(json!(full + { "name": "Class" })) + .await + .assert_error(ErrorCode::BANK_TRANSACTION_NOT_FOUND); + ctx.delete_admin("/conversion-rate-classes/1") + .await + .assert_error(ErrorCode::BANK_TRANSACTION_NOT_FOUND); + } +} diff --git a/crates/libeufin-bank/src/api/prepared.rs b/libeufin-bank/src/api/prepared.rs diff --git a/crates/libeufin-bank/src/api/revenue.rs b/libeufin-bank/src/api/revenue.rs diff --git a/crates/libeufin-bank/src/api/tan.rs b/libeufin-bank/src/api/tan.rs diff --git a/crates/libeufin-bank/src/api/token.rs b/libeufin-bank/src/api/token.rs diff --git a/crates/libeufin-bank/src/api/tx.rs b/libeufin-bank/src/api/tx.rs diff --git a/libeufin-bank/src/api/wire.rs b/libeufin-bank/src/api/wire.rs @@ -0,0 +1,719 @@ +/* +* This file is part of LibEuFin. +* Copyright (C) 2026 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/> +*/ + +use std::sync::Arc; + +use axum::{ + Json, Router, + extract::State, + response::{IntoResponse, NoContent, Response}, + routing::{get, post}, +}; +use jiff::Timestamp; +use serde::Deserialize; +use sqlx::PgPool; +use taler_api::{ + api::Validation, + constants::WIRE_GATEWAY_API_VERSION, + error::{ApiResult, failure_code}, + extract::{Path, Query, Req}, + subject::{IncomingSubject, fmt_in_subject}, +}; +use taler_common::{ + api::{ + params::{HistoryParams, Page, PageParams}, + wire::{ + AddIncomingRequest, AddIncomingResponse, AddKycauthRequest, AddMappedRequest, + IncomingHistory, OutgoingHistory, TransferList, TransferRequest, TransferResponse, + TransferState, WireConfig, + }, + }, + error_code::ErrorCode, + types::{amount::Amount, payto::PaytoURI}, +}; + +use crate::{ + api::{BankState, IMPLEMENTATION}, + auth::{UserAdminAuth, WireRAuth, WireRWAuth}, + db::{ + account::check_info, + exchange::{ + AddIncomingResult, TransferResult, add_incoming, incoming_history, outgoing_history, + page_transfer, transfer, transfer_by_id, + }, + }, + payto::BankPayto, +}; + +#[derive(Debug, Clone, Deserialize)] +pub struct TransferParams { + #[serde(flatten)] + pub page: PageParams, + pub status: Option<TransferState>, +} + +impl TransferParams { + pub fn check(self) -> ApiResult<Transfer> { + Ok(Transfer { + page: self.page.check()?, + status: self.status, + }) + } +} + +pub struct Transfer { + pub page: Page, + pub status: Option<TransferState>, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct AccountCheckParams { + pub account: BankPayto, +} + +async fn admin_add_incoming( + db: &PgPool, + username: &str, + amount: &Amount, + debtor: &PaytoURI, + metadata: &IncomingSubject, +) -> ApiResult<Response> { + let payto = BankPayto::try_from(debtor)?; + match add_incoming( + db, + amount, + &payto, + &format!( + "Admin incoming {}", + fmt_in_subject(metadata.ty(), metadata.key()) + ), + username, + &Timestamp::now(), + metadata, + ) + .await? + { + AddIncomingResult::Success { id, timestamp, .. } => Ok(Json(AddIncomingResponse { + row_id: id, + timestamp: timestamp.into(), + }) + .into_response()), + AddIncomingResult::NotAnExchange => { + Err(failure_code(ErrorCode::BANK_ACCOUNT_IS_NOT_EXCHANGE)) + } + AddIncomingResult::UnknownExchange => Err(failure_code(ErrorCode::BANK_UNKNOWN_ACCOUNT)), + AddIncomingResult::UnknownDebtor => Err(failure_code(ErrorCode::BANK_UNKNOWN_DEBTOR)), + AddIncomingResult::BothPartyAreExchange => { + Err(failure_code(ErrorCode::BANK_ACCOUNT_IS_EXCHANGE)) + } + AddIncomingResult::ReservePubReuse => { + Err(failure_code(ErrorCode::BANK_DUPLICATE_RESERVE_PUB_SUBJECT)) + } + AddIncomingResult::UnknownMapping => { + Err(failure_code(ErrorCode::BANK_TRANSFER_MAPPING_UNKNOWN)) + } + AddIncomingResult::MappingReuse => { + Err(failure_code(ErrorCode::BANK_TRANSFER_MAPPING_REUSED)) + } + AddIncomingResult::BalanceInsufficient => { + Err(failure_code(ErrorCode::BANK_UNALLOWED_DEBIT)) + } + } +} + +pub fn wire_api() -> Router<Arc<BankState>> { + Router::new() + .route( + "/accounts/{username}/taler-wire-gateway/config", + get(async |State(state): State<Arc<BankState>>| { + Json(WireConfig { + name: (), + version: WIRE_GATEWAY_API_VERSION, + implementation: Some(IMPLEMENTATION), + currency: state.cfg.regional_currency, + support_account_check: true, + }) + }), + ) + .route( + "/accounts/{username}/taler-wire-gateway/transfer", + post( + async |auth: WireRWAuth, + State(state): State<Arc<BankState>>, + Req(req): Req<TransferRequest>| { + req.check(&state.cfg.regional_currency)?; + let payto = BankPayto::try_from(&req.credit_account)?; + match transfer( + &state.db, + &auth.username, + &req, + &payto, + &Timestamp::now(), + state.cfg.fiat.is_some(), + ) + .await? + { + TransferResult::Success { id, timestamp } => Ok(Json(TransferResponse { + timestamp: timestamp.into(), + row_id: id, + })), + TransferResult::NotAnExchange => { + Err(failure_code(ErrorCode::BANK_ACCOUNT_IS_NOT_EXCHANGE)) + } + TransferResult::UnknownExchange => { + Err(failure_code(ErrorCode::BANK_UNKNOWN_ACCOUNT)) + } + TransferResult::BothPartyAreExchange => { + Err(failure_code(ErrorCode::BANK_ACCOUNT_IS_EXCHANGE)) + } + TransferResult::BalanceInsufficient => { + Err(failure_code(ErrorCode::BANK_UNALLOWED_DEBIT)) + } + TransferResult::ReserveUidReuse => { + Err(failure_code(ErrorCode::BANK_TRANSFER_REQUEST_UID_REUSED)) + } + TransferResult::WtidReuse => { + Err(failure_code(ErrorCode::BANK_TRANSFER_WTID_REUSED)) + } + TransferResult::AdminCreditor => { + Err(failure_code(ErrorCode::BANK_ADMIN_CREDITOR)) + } + } + }, + ), + ) + .route( + "/accounts/{username}/taler-wire-gateway/transfers", + get( + async |Query(params): Query<TransferParams>, + mut auth: WireRAuth, + State(state): State<Arc<BankState>>| { + let params = params.check()?; + let info = auth.bank_info(&state.db, &state.cfg.ctx).await?; + if !info.is_exchange { + return Err(failure_code(ErrorCode::BANK_ACCOUNT_IS_NOT_EXCHANGE)); + } + let transfers = page_transfer( + &state.db, + &state.cfg.ctx, + &state.cfg.regional_currency, + &params.page, + info.bank_account_id, + params.status, + ) + .await?; + if transfers.is_empty() { + ApiResult::Ok(NoContent.into_response()) + } else { + Ok(Json(TransferList { + transfers, + debit_account: info.payto.as_uri(), + }) + .into_response()) + } + }, + ), + ) + .route( + "/accounts/{username}/taler-wire-gateway/transfers/{id}", + get( + async |Path((_, id)): Path<((), u64)>, + mut auth: WireRAuth, + State(state): State<Arc<BankState>>| { + let info = auth.bank_info(&state.db, &state.cfg.ctx).await?; + if !info.is_exchange { + return Err(failure_code(ErrorCode::BANK_ACCOUNT_IS_NOT_EXCHANGE)); + } + match transfer_by_id( + &state.db, + &state.cfg.ctx, + &state.cfg.regional_currency, + info.bank_account_id, + id, + ) + .await? + { + Some(t) => Ok(Json(t)), + None => Err(failure_code(ErrorCode::BANK_TRANSACTION_NOT_FOUND)), + } + }, + ), + ) + .route( + "/accounts/{username}/taler-wire-gateway/history/incoming", + get( + async |Query(params): Query<HistoryParams>, + mut auth: WireRAuth, + State(state): State<Arc<BankState>>| { + let params = params.check()?; + let info = auth.bank_info(&state.db, &state.cfg.ctx).await?; + if !info.is_exchange { + return Err(failure_code(ErrorCode::BANK_ACCOUNT_IS_NOT_EXCHANGE)); + } + let incoming_transactions = incoming_history( + &state.db, + &state.cfg.ctx, + &state.taler_in_channel, + &state.cfg.regional_currency, + &params, + info.bank_account_id, + ) + .await?; + if incoming_transactions.is_empty() { + ApiResult::Ok(NoContent.into_response()) + } else { + Ok(Json(IncomingHistory { + credit_account: info.payto.as_uri(), + incoming_transactions, + }) + .into_response()) + } + }, + ), + ) + .route( + "/accounts/{username}/taler-wire-gateway/history/outgoing", + get( + async |Query(params): Query<HistoryParams>, + mut auth: WireRAuth, + State(state): State<Arc<BankState>>| { + let params = params.check()?; + let info = auth.bank_info(&state.db, &state.cfg.ctx).await?; + if !info.is_exchange { + return Err(failure_code(ErrorCode::BANK_ACCOUNT_IS_NOT_EXCHANGE)); + } + let outgoing_transactions = outgoing_history( + &state.db, + &state.cfg.ctx, + &state.taler_out_channel, + &state.cfg.regional_currency, + &params, + info.bank_account_id, + ) + .await?; + if outgoing_transactions.is_empty() { + ApiResult::Ok(NoContent.into_response()) + } else { + Ok(Json(OutgoingHistory { + debit_account: info.payto.as_uri(), + outgoing_transactions, + }) + .into_response()) + } + }, + ), + ) + .route( + "/accounts/{username}/taler-wire-gateway/admin/add-incoming", + post( + async |auth: UserAdminAuth, + State(state): State<Arc<BankState>>, + Req(req): Req<AddIncomingRequest>| { + req.check(&state.cfg.regional_currency)?; + admin_add_incoming( + &state.db, + &auth.username, + &req.amount, + &req.debit_account, + &IncomingSubject::Reserve(req.reserve_pub), + ) + .await + }, + ), + ) + .route( + "/accounts/{username}/taler-wire-gateway/admin/add-kycauth", + post( + async |auth: UserAdminAuth, + State(state): State<Arc<BankState>>, + Req(req): Req<AddKycauthRequest>| { + req.check(&state.cfg.regional_currency)?; + admin_add_incoming( + &state.db, + &auth.username, + &req.amount, + &req.debit_account, + &IncomingSubject::Kyc(req.account_pub), + ) + .await + }, + ), + ) + .route( + "/accounts/{username}/taler-wire-gateway/admin/add-mapped", + post( + async |auth: UserAdminAuth, + State(state): State<Arc<BankState>>, + Req(req): Req<AddMappedRequest>| { + req.check(&state.cfg.regional_currency)?; + admin_add_incoming( + &state.db, + &auth.username, + &req.amount, + &req.debit_account, + &IncomingSubject::Map(req.authorization_pub), + ) + .await + }, + ), + ) + .route( + "/accounts/{username}/taler-wire-gateway/account/check", + get( + async |Query(params): Query<AccountCheckParams>, + mut auth: WireRAuth, + State(state): State<Arc<BankState>>| { + let info = auth.bank_info(&state.db, &state.cfg.ctx).await?; + if !info.is_exchange { + return Err(failure_code(ErrorCode::BANK_ACCOUNT_IS_NOT_EXCHANGE)); + } + match check_info(&state.db, &params.account).await? { + Some(t) => Ok(Json(t)), + None => Err(failure_code(ErrorCode::BANK_UNKNOWN_ACCOUNT)), + } + }, + ), + ) +} + +#[cfg(test)] +pub mod test { + use axum::http::Method; + use taler_api::subject::fmt_out_subject; + use taler_common::{ + api::{ + EddsaPublicKey, HashCode, ShortHashCode, + wire::{AccountInfo, TransferResponse, TransferState, TransferStatus}, + }, + db::IncomingType, + error_code::ErrorCode, + types::{amount::amount, payto::PaytoImpl}, + }; + use taler_test_utils::{ + json, + routine::{ + admin_add_incoming_routine, in_history_routine, out_history_routine, transfer_routine, + }, + server::TestServer as _, + tasks, + }; + + use crate::api::test::{Auth, bank_setup}; + + #[tokio::test] + async fn transfer() { + let ctx = bank_setup().await; + + ctx.auth_routine( + Method::POST, + "/accounts/merchant/taler-wire-gateway/transfer", + Auth::UserOnly, + ) + .await; + ctx.auth_routine( + Method::GET, + "/accounts/merchant/taler-wire-gateway/transfers/32", + Auth::UserOnly, + ) + .await; + ctx.auth_routine( + Method::GET, + "/accounts/merchant/taler-wire-gateway/transfers", + Auth::UserOnly, + ) + .await; + + let req = json!({ + "request_uid": HashCode::rand(), + "amount": "KUDOS:55", + "exchange_base_url": "http://exchange.example.com/", + "wtid": ShortHashCode::rand(), + "credit_account": ctx.merchant_payto + }); + + ctx.posta("/accounts/exchange/taler-wire-gateway/transfer") + .json(&req) + .await + .assert_error(ErrorCode::BANK_UNALLOWED_DEBIT); + + ctx.set_max_debt("exchange", "1000").await; + transfer_routine( + &ctx.admin_router().await.prefix("/accounts/exchange"), + TransferState::success, + &ctx.customer_payto.as_uri(), + ) + .await; + + let admin_req = json!(req + { + "credit_account" : ctx.admin_payto + }); + + // Check conversion bounce + for _ in 0..2 { + ctx.posta("/accounts/exchange/taler-wire-gateway/transfer") + .json(&admin_req) + .await + .assert_ok(); + } + + let ctx = ctx.swap_cfg("test_no_conversion.conf").await; + // Transfer works for common accounts + + let req = json!(req + { + "request_uid": HashCode::rand(), + "wtid": ShortHashCode::rand(), + }); + for _ in 0..2 { + ctx.posta("/accounts/exchange/taler-wire-gateway/transfer") + .json(&req) + .await + .assert_ok(); + } + // But fails to admin accounts + ctx.posta("/accounts/exchange/taler-wire-gateway/transfer") + .json(json!(admin_req + { + "request_uid": HashCode::rand(), + "wtid": ShortHashCode::rand(), + })) + .await + .assert_error(ErrorCode::BANK_ADMIN_CREDITOR); + + // Dot now fail for unknown account + let res = ctx + .posta("/accounts/exchange/taler-wire-gateway/transfer") + .json(json!(req + { + "request_uid": HashCode::rand(), + "wtid": ShortHashCode::rand(), + "credit_account": ctx.unknown_payto + })) + .await + .assert_ok_json::<TransferResponse>(); + let s: TransferStatus = ctx + .geta(format!( + "/accounts/exchange/taler-wire-gateway/transfers/{}", + res.row_id + )) + .await + .assert_ok_json(); + assert_eq!( + TransferStatus { + status: TransferState::permanent_failure, + status_msg: Some("Unknown account".into()), + amount: amount("KUDOS:55"), + origin_exchange_url: s.origin_exchange_url.clone(), + metadata: None, + wtid: s.wtid.clone(), + credit_account: s.credit_account.clone(), + timestamp: s.timestamp, + }, + s + ); + + // Not an exchange + ctx.geta(format!( + "/accounts/merchant/taler-wire-gateway/transfers/{}", + res.row_id + )) + .await + .assert_error(ErrorCode::BANK_ACCOUNT_IS_NOT_EXCHANGE); + // Another account + ctx.patcha("/accounts/merchant") + .json(json!({ + "is_taler_exchange": true + })) + .await + .assert_no_content(); + ctx.geta(format!( + "/accounts/merchant/taler-wire-gateway/transfers/{}", + res.row_id + )) + .await + .assert_error(ErrorCode::BANK_TRANSACTION_NOT_FOUND); + } + + #[tokio::test] + async fn outgoing_history() { + let ctx = &bank_setup().await; + ctx.auth_routine( + Method::GET, + "/accounts/merchant/taler-wire-gateway/history/outgoing", + Auth::UserOnly, + ) + .await; + ctx.set_max_debt("exchange", "1000").await; + out_history_routine( + &ctx.admin_router().await.prefix("/accounts/exchange"), + tasks!( + // Transactions using clean add incoming logic + { ctx.transfer("10", &ctx.customer_payto, None).await }, + // And with metadata + { + ctx.transfer("12", &ctx.customer_payto, Some("CON:ID".into())) + .await + } + ), + tasks!( + // // Failed transfer + { ctx.transfer("10", &ctx.unknown_payto, None).await }, + // Ignore manual outgoing transaction + { + ctx.tx_s( + "exchange", + "10", + "merchant", + fmt_out_subject( + &ShortHashCode::rand(), + "https://exchange.example.com/", + None, + ), + ) + .await + }, + // Ignore malformed incoming transaction + { ctx.tx("merchant", "10", "exchange",).await }, + // Ignore malformed outgoing transaction + { ctx.tx("exchange", "10", "merchant",).await }, + ), + ) + .await; + } + + #[tokio::test] + async fn incoming_history() { + let ctx = &bank_setup().await; + ctx.auth_routine( + Method::GET, + "/accounts/merchant/taler-wire-gateway/history/incoming", + Auth::UserOnly, + ) + .await; + ctx.set_max_debt("customer", "1000").await; + ctx.set_max_debt("merchant", "1000").await; + in_history_routine( + &ctx.admin_router().await.prefix("/accounts/exchange"), + &ctx.customer_payto.as_uri(), + false, + tasks!( + // Reserve transactions using raw bank transaction logic + { + ctx.tx_s( + "merchant", + "10", + "exchange", + format!("history test with {} reserve pub", EddsaPublicKey::rand()), + ) + .await + }, + // Reserve transactions using withdraw logic + { ctx.withdrawal("9").await }, + // KYC transactions using raw bank transaction logic + { + ctx.tx_s( + "merchant", + "10", + "exchange", + format!( + "history test with KYC:{} account pub", + EddsaPublicKey::rand() + ), + ) + .await + } + ), + tasks!( + // Ignore malformed incoming transaction + { ctx.tx("merchant", "10", "exchange",).await }, + // Ignore malformed outgoing transaction + { ctx.tx("exchange", "10", "merchant",).await }, + ), + ) + .await; + } + + #[tokio::test] + async fn admin_add_incoming() { + let ctx = bank_setup().await; + ctx.auth_routine( + Method::GET, + "/accounts/merchant/taler-wire-gateway/history/incoming", + Auth::UserOnly, + ) + .await; + for ty in IncomingType::entries { + let (path, key) = match ty { + IncomingType::reserve => ("incoming", "reserve_pub"), + IncomingType::kyc => ("kycauth", "account_pub"), + IncomingType::map => ("mapped", "authorization_pub"), + }; + let path = format!("/accounts/exchange/taler-wire-gateway/admin/add-{path}"); + ctx.auth_routine(Method::POST, &path, Auth::Admin).await; + let req = json!({ + "amount": format!("KUDOS:44"), + "debit_account": ctx.customer_payto, + key: EddsaPublicKey::rand(), + }); + + ctx.post_admin(path.replace("exchange", "merchant")) + .json(&req) + .await + .assert_error(ErrorCode::BANK_ACCOUNT_IS_NOT_EXCHANGE); + ctx.post_admin(&path) + .json(json!(req + { + "debit_account": ctx.exchange_payto + })) + .await + .assert_error(ErrorCode::BANK_ACCOUNT_IS_EXCHANGE); + if *ty != IncomingType::map { + ctx.post_admin(&path) + .json(json!(req + { + "amount": format!("KUDOS:44"), + })) + .await + .assert_error(ErrorCode::BANK_UNALLOWED_DEBIT); + } + } + ctx.set_max_debt("customer", "1000").await; + admin_add_incoming_routine( + &ctx.admin_router().await.prefix("/accounts/exchange"), + &ctx.customer_payto.as_full_uri("Customer"), + true, + ) + .await; + } + + #[tokio::test] + async fn account_check() { + let ctx = bank_setup().await; + ctx.geta("/accounts/exchange/taler-wire-gateway/account/check") + .await + .assert_error(ErrorCode::GENERIC_PARAMETER_MISSING); + ctx.geta(format!( + "/accounts/exchange/taler-wire-gateway/account/check?account={}", + ctx.unknown_payto + )) + .await + .assert_error(ErrorCode::BANK_UNKNOWN_ACCOUNT); + ctx.geta(format!( + "/accounts/exchange/taler-wire-gateway/account/check?account={}", + ctx.merchant_payto + )) + .await + .assert_ok_json::<AccountInfo>(); + } +} diff --git a/crates/libeufin-bank/src/api/withdrawal.rs b/libeufin-bank/src/api/withdrawal.rs diff --git a/crates/libeufin-bank/src/auth.rs b/libeufin-bank/src/auth.rs diff --git a/crates/libeufin-bank/src/config.rs b/libeufin-bank/src/config.rs diff --git a/crates/libeufin-bank/src/db.rs b/libeufin-bank/src/db.rs diff --git a/crates/libeufin-bank/src/db/account.rs b/libeufin-bank/src/db/account.rs diff --git a/crates/libeufin-bank/src/db/cashout.rs b/libeufin-bank/src/db/cashout.rs diff --git a/crates/libeufin-bank/src/db/conversion.rs b/libeufin-bank/src/db/conversion.rs diff --git a/crates/libeufin-bank/src/db/exchange.rs b/libeufin-bank/src/db/exchange.rs diff --git a/crates/libeufin-bank/src/db/gc.rs b/libeufin-bank/src/db/gc.rs diff --git a/crates/libeufin-bank/src/db/prepared.rs b/libeufin-bank/src/db/prepared.rs diff --git a/crates/libeufin-bank/src/db/tan.rs b/libeufin-bank/src/db/tan.rs diff --git a/crates/libeufin-bank/src/db/token.rs b/libeufin-bank/src/db/token.rs diff --git a/crates/libeufin-bank/src/db/tx.rs b/libeufin-bank/src/db/tx.rs diff --git a/crates/libeufin-bank/src/db/withdrawal.rs b/libeufin-bank/src/db/withdrawal.rs diff --git a/libeufin-bank/src/lib.rs b/libeufin-bank/src/lib.rs @@ -0,0 +1,560 @@ +/* +* This file is part of LibEuFin. +* Copyright (C) 2026 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/> +*/ + +use std::{ + sync::Arc, + time::{Duration, Instant}, +}; + +use anyhow::{anyhow, bail}; +use compact_str::CompactString; +use dialoguer::Password; +use jiff::{Timestamp, Zoned}; +use rand::{RngExt as _, distr::Alphanumeric}; +use serde::{Deserialize, Serialize}; +use taler_api::api::TalerRouter; +use taler_build::long_version; +use taler_common::{ + CommonArgs, + cli::ConfigCmd, + config::{Config, parser::ConfigSource}, + types::{ + amount::Amount, + base32::Base32, + payto::IbanPayto, + timestamp::{RelativeTime, TalerTimestamp}, + }, +}; +use taler_macros::EnumMeta; +use tracing::info; + +use crate::{ + api::{ + BankState, + account::{ + AccountReconfiguration, ChallengeContactData, Maybe, RegisterAccountRequest, + create_account, create_admin_account, + }, + bank_api, + }, + auth::{TOKEN_PREFIX, TokenScope}, + config::BankCfg, + db::{ + account::{ + CreationResult, PatchAuthResult, PatchResult, bank_info, reconfig, reconfig_password, + }, + dbinit, + gc::collect, + pool, + token::{TokenCreationResult, access}, + }, + payto::{BankPayto, PaytoCtx}, + pw::{PwCrypto, checkpw}, +}; + +pub mod api; +pub mod auth; +pub mod config; +pub mod db; +pub mod mfa; +pub mod payto; +pub mod pw; + +pub const CONFIG_SOURCE: ConfigSource = + ConfigSource::new("libeufin", "libeufin-bank", "libeufin-bank"); + +#[derive(clap::Subcommand, Debug)] +pub enum Cmd { + /// Initialize libeufin-bank database + Dbinit { + /// Reset database (DANGEROUS: All existing data is lost) + #[arg(short, long)] + reset: bool, + }, + /// Change account password + Passwd { + /// Account username + username: CompactString, + /// Account password used for authentication + password: Option<String>, + }, + /// Create authentication token for a user + CreateToken { + /// Account username + #[arg(short, long, visible_alias("user"))] + username: CompactString, + + /// Scope for the token + #[arg(short, long)] + scope: TokenScope, + + /// Custom token validity duration + #[arg(short, long, value_name("forever|MICROS"))] + duration: Option<RelativeTime>, + + /// Optional token description + #[arg(long)] + description: Option<CompactString>, + + /// Make the token refreshable into a new token + #[arg(long)] + refreshable: bool, + + /// Current token to reuse if still valid + #[arg(long)] + current_token: Option<Base32<32>>, + }, + /// Run libeufin-bank HTTP server + Serve, + /// Create an account, returning the payto://-URI associated with it + CreateAccount { + /// Optional JSON payload. If provided, CLI options are ignored. + #[arg()] + json: Option<String>, + + /// Account unique username + #[arg(short, long, visible_alias("user"), required_unless_present("json"))] + username: Option<CompactString>, + + /// Account password used for authentication + #[arg(short, long)] + password: Option<CompactString>, + + /// Legal name of the account owner + #[arg(long, required_unless_present("json"))] + name: Option<CompactString>, + + /// Make this account visible to anyone + #[arg(long)] + public: bool, + + /// Make this account a taler exchange + #[arg(long)] + exchange: bool, + + /// E-Mail address used for TAN transmission + #[arg(long)] + email: Option<CompactString>, + + /// Phone number used for TAN transmission + #[arg(long)] + phone: Option<CompactString>, + + /// Payto URI of a fiat account who receive cashout amount + #[arg(long, alias("cashout_payto_uri"))] + cashout_payto_uri: Option<IbanPayto>, + + /// Payto URI of this account + #[arg(long)] + payto_uri: Option<BankPayto>, + + /// Max debit allowed for this account + #[arg(long, alias("debit_threshold"))] + debit_threshold: Option<Amount>, + + /// Enables 2FA and set the TAN channel used for challenges + #[arg(long)] + #[arg(long, alias("tan_channel"))] + tan_channel: Vec<TanChannel>, + }, + /// Edit an existing account + EditAccount { + /// Account unique username + username: CompactString, + + /// Legal name of the account owner + #[arg(long)] + name: Option<CompactString>, + + /// Make this account visible to anyone + #[arg(long)] + public: Option<bool>, + + /// Make this account a taler exchange + #[arg(long)] + exchange: Option<bool>, + + /// E-Mail address used for TAN transmission + #[arg(long)] + email: Option<Maybe<CompactString>>, + + /// Phone number used for TAN transmission + #[arg(long)] + phone: Option<Maybe<CompactString>>, + + /// Payto URI of a fiat account who receive cashout amount + #[arg(long, alias("cashout_payto_uri"))] + cashout_payto_uri: Option<Maybe<IbanPayto>>, + + /// Max debit allowed for this account + #[arg(long, alias("debit_threshold"))] + debit_threshold: Option<Amount>, + + /// Enables 2FA and set the TAN channel used for challenges + #[arg(long, alias("tan_channel"))] + tan_channel: Option<Vec<TanChannel>>, + }, + /// Run garbage collection: abort expired operations and clean expired data + Gc, + /// Benchmark password hashing algorithm and configuration + BenchPwh, + #[command(subcommand)] + Config(ConfigCmd), +} + +#[derive(clap::Parser, Debug)] +#[command(long_version = long_version(), about, long_about = None)] +pub struct Args { + #[clap(flatten)] + pub common: CommonArgs, + + #[command(subcommand)] + pub cmd: Cmd, +} + +// Allowed values for cashout TAN channels. +#[derive( + sqlx::Type, + Debug, + Clone, + Copy, + PartialEq, + Eq, + PartialOrd, + Ord, + EnumMeta, + Serialize, + Deserialize, + clap::ValueEnum, +)] +#[sqlx(type_name = "tan_enum")] +#[enum_meta(Str)] +#[allow(non_camel_case_types)] +pub enum TanChannel { + sms, + email, +} + +pub async fn run(cfg: Config, cmd: Cmd) -> anyhow::Result<()> { + match cmd { + Cmd::Dbinit { reset } => { + let cfg = BankCfg::parse(cfg)?; + let db = dbinit(&cfg, reset).await?; + match create_admin_account(&db, &cfg, None).await? { + CreationResult::Success(_) => { + info!("Admin's account created") + } + CreationResult::UsernameReuse => {} + CreationResult::PayToReuse + | CreationResult::UnknownConversionClass + | CreationResult::BonusBalanceInsufficient => unreachable!(), + } + } + Cmd::Passwd { username, password } => { + let cfg = BankCfg::parse(cfg)?; + let db = pool(&cfg).await?; + + let pw = match password { + Some(p) => p, + None => Password::new() + .with_prompt("Password") + .with_confirmation("Repeat for confirmation", "Values do not match, try again") + .interact()?, + }; + checkpw(&pw, cfg.pwd_check_quality)?; + match reconfig_password(&db, &cfg.pw_crypto, &username, &pw, None, true).await? { + PatchAuthResult::Success => { + info!("Password change for '{username}' account succeeded") + } + PatchAuthResult::UnknownAccount => { + bail!("Password change for '{username}' account failed: unknown account") + } + PatchAuthResult::OldPasswordMismatch | PatchAuthResult::TanRequired => { + unreachable!() + } + } + } + Cmd::CreateToken { + username, + scope, + duration, + description, + refreshable, + current_token, + } => { + let cfg = BankCfg::parse(cfg)?; + let db = pool(&cfg).await?; + let now = Timestamp::now(); + let new = if let Some(current) = current_token + && let Some(token) = access(&db, current.as_ref(), &now).await? + && token.expiration < TalerTimestamp::Timestamp(now) + && scope.logical().is_valid_scope(token.scope, refreshable) + { + current + } else { + let expiration = + match duration.unwrap_or(RelativeTime::Duration(Duration::from_hours(24))) { + RelativeTime::Forever => TalerTimestamp::Never, + RelativeTime::Duration(duration) => TalerTimestamp::Timestamp( + now.checked_add(duration) + .map_err(|e| anyhow!("Bad token duration: {e}"))?, + ), + }; + let token = Base32::<32>::secure_rand(); + match db::token::create( + &db, + &username, + token.as_ref(), + &now, + &expiration, + &scope, + refreshable, + description.as_deref(), + true, + ) + .await? + { + TokenCreationResult::Success(_) => token, + TokenCreationResult::TanRequired => unreachable!(), + } + }; + println!("{TOKEN_PREFIX}{new}"); + } + Cmd::Serve => { + let cfg = BankCfg::parse(cfg)?; + let db = pool(&cfg).await?; + if cfg.fiat.is_some() { + info!("Ensure exchange account exists"); + let Some(info) = bank_info(&db, &cfg.ctx, "exchange").await? else { + bail!( + "Exchange account missing: an exchange account named 'exchange' is required for conversion to be enabled" + ) + }; + if !info.is_exchange { + bail!( + "Account is not an exchange: an exchange account named 'exchange' is required for conversion to be enabled" + ) + } + info!("Ensure conversion is enabled"); + match std::fs::read_to_string(format!( + "{}/libeufin-conversion-setup.sql", + cfg.db_cfg.sql_dir + )) { + Ok(sql) => { + sqlx::raw_sql(&sql).execute(&db).await?; + } + Err(e) => { + bail!( + "Could not read libeufin-conversion-setup.sql at '{}': {}", + cfg.db_cfg.sql_dir, + e.kind() + ) + } + }; + } else { + info!("Ensure conversion is disabled"); + match std::fs::read_to_string(format!( + "{}/libeufin-conversion-drop.sql", + cfg.db_cfg.sql_dir + )) { + Ok(sql) => { + sqlx::raw_sql(&sql).execute(&db).await?; + } + Err(e) => { + bail!( + "Could not read libeufin-conversion-setup.sql at '{}': {}", + cfg.db_cfg.sql_dir, + e.kind() + ) + } + }; + } + let state = Arc::new(BankState::start(db, cfg).await); + bank_api(state.clone()) + .serve(&state.cfg.serve, None) + .await?; + } + Cmd::CreateAccount { + json, + username, + password, + name, + public: is_public, + exchange, + email, + phone, + cashout_payto_uri, + payto_uri, + debit_threshold, + tan_channel, + } => { + let cfg = BankCfg::parse(cfg)?; + let db = pool(&cfg).await?; + let req = if let Some(json_str) = json { + serde_json::from_str::<RegisterAccountRequest>(&json_str) + .map_err(|e| anyhow!("Failed to parse JSON: {e}"))? + } else { + RegisterAccountRequest { + username: username.unwrap(), + password: match password { + Some(p) => p, + None => Password::new() + .with_prompt("Password") + .with_confirmation( + "Repeat for confirmation", + "Values do not match, try again", + ) + .interact()? + .into(), + }, + name: name.unwrap(), + is_public, + is_taler_exchange: exchange, + contact_data: ChallengeContactData { + email: email.into(), + phone: phone.into(), + }, + cashout_payto_uri, + payto_uri, + debit_threshold, + tan_channel: None, + tan_channels: Some(tan_channel), + conversion_rate_class_id: None, + } + }; + + match create_account(&db, &cfg, &req, true).await? { + CreationResult::Success(full_payto) => { + info!("Account '{}' created", req.username); + println!("{full_payto}") + } + CreationResult::UsernameReuse => { + bail!("Account username reuse '{}'", req.username) + } + CreationResult::PayToReuse => bail!("Bank internalPayToUri reuse"), + CreationResult::UnknownConversionClass => unreachable!(), + CreationResult::BonusBalanceInsufficient => { + bail!("Insufficient admin funds to grant bonus") + } + } + } + Cmd::EditAccount { + username, + name, + public, + exchange, + email, + phone, + cashout_payto_uri, + debit_threshold, + tan_channel, + } => { + let cfg = BankCfg::parse(cfg)?; + let db = pool(&cfg).await?; + let req = AccountReconfiguration { + name, + is_taler_exchange: exchange, + is_public: public, + contact_data: ChallengeContactData { + email: email.into(), + phone: phone.into(), + }, + cashout_payto_uri: cashout_payto_uri.into(), + debit_threshold, + tan_channel: Maybe::Missing, + tan_channels: tan_channel, + conversion_rate_class_id: Maybe::Missing, + }; + match reconfig( + &db, + &cfg.regional_currency, + &username, + &req, + true, + true, + true, + true, + ) + .await? + { + PatchResult::UnknownAccount => { + bail!("Password change for '{username}' account failed: unknown account") + } + PatchResult::NonAdminName + | PatchResult::NonAdminCashout + | PatchResult::NonAdminDebtLimit + | PatchResult::NonAdminConversionRateClass + | PatchResult::UnknownConversionClass + | PatchResult::Challenges(_) => unreachable!(), + PatchResult::MissingTanInfo(e) => bail!("{e}"), + PatchResult::Success => info!("Account '{username}' edited"), + } + } + Cmd::Gc => { + let cfg = BankCfg::parse(cfg)?; + let db = pool(&cfg).await?; + let now = Zoned::now(); + collect( + &db, + &(now.clone() + cfg.gc_abort_after).timestamp(), + &(now.clone() + cfg.gc_clean_after).timestamp(), + &(now + cfg.gc_delete_after).timestamp(), + ) + .await?; + } + Cmd::BenchPwh => { + let cfg = BankCfg::parse(cfg)?; + let pwc = cfg.pw_crypto; + + match pwc { + PwCrypto::Bcrypt { cost } => { + println!("Benching bcrypt with cost={cost} for 10s"); + } + PwCrypto::Sha256 => unreachable!(), + } + + let start = Instant::now(); + let stop = start + Duration::from_secs(10); + let mut count = 0; + + loop { + let now = Instant::now(); + if now < stop { + let password: String = rand::rng() + .sample_iter(&Alphanumeric) + .take(20) + .map(char::from) + .collect(); + + pwc.hashpw(&password); + count += 1; + } else { + let elapsed = start.elapsed().as_secs_f64(); + let per_sec = count as f64 / elapsed; + let iter_time_ms = (elapsed * 1000.0) / count as f64; + + println!("hash password in {iter_time_ms:.0}ms {per_sec:.2} H/s"); + break; + } + } + } + Cmd::Config(cmd) => cmd.run(&cfg)?, + } + Ok(()) +} diff --git a/crates/libeufin-bank/src/main.rs b/libeufin-bank/src/main.rs diff --git a/libeufin-bank/src/main/kotlin/tech/libeufin/bank/Config.kt b/libeufin-bank/src/main/kotlin/tech/libeufin/bank/Config.kt @@ -1,222 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2023, 2024, 2025, 2026 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.bank - -import kotlinx.serialization.Serializable -import io.github.smiley4.schemakenerator.core.annotations.Description -import tech.libeufin.bank.db.Database -import tech.libeufin.common.* -import tech.libeufin.common.crypto.PwCrypto -import tech.libeufin.common.db.DatabaseConfig -import java.nio.file.Path -import java.time.Duration - -/** Configuration for libeufin-bank */ -data class BankConfig( - private val cfg: TalerConfig, - val name: String, - val baseUrl: BaseURL, - val regionalCurrency: String, - val regionalCurrencySpec: CurrencySpecification, - val wireTransferFees: TalerAmount, - val minAmount: TalerAmount, - val maxAmount: TalerAmount, - val allowRegistration: Boolean, - val allowAccountDeletion: Boolean, - val allowEditName: Boolean, - val allowEditCashout: Boolean, - val defaultDebtLimit: TalerAmount, - val registrationBonus: TalerAmount, - val suggestedWithdrawalExchange: String?, - val allowConversion: Boolean, - val fiatCurrency: String?, - val fiatCurrencySpec: CurrencySpecification?, - val spaPath: Path?, - val fallbackSpaPath: Path, - val tanChannels: Map<TanChannel, Pair<Path, Map<String, String>>>, - val payto: BankPaytoCtx, - val wireMethod: WireMethod, - val pwCrypto: PwCrypto, - val gcAbortAfter: Duration, - val gcCleanAfter: Duration, - val gcDeleteAfter: Duration, - val pwdCheckQuality: Boolean, - val basicAuthCompat: Boolean -) { - val dbCfg: DatabaseConfig by lazy { - val sect = cfg.section("libeufin-bankdb-postgres") - DatabaseConfig( - dbConnStr = sect.stringsub("config").require(), - sqlDir = sect.path("sql_dir").require() - ) - } - val serverCfg: ServerConfig by lazy { - cfg.loadServerConfig("libeufin-bank") - } -} - -@Description("Currency conversion rate configuration") -@Serializable -data class ConversionRate ( - @Description("Ratio for converting fiat to regional currency") - val cashin_ratio: DecimalNumber, - @Description("Fee charged on cash-in conversions") - val cashin_fee: TalerAmount, - @Description("Smallest possible cash-in amount") - val cashin_tiny_amount: TalerAmount, - @Description("Rounding mode for cash-in conversions") - val cashin_rounding_mode: RoundingMode, - @Description("Minimum amount for cash-in conversions") - val cashin_min_amount: TalerAmount, - @Description("Ratio for converting regional currency to fiat") - val cashout_ratio: DecimalNumber, - @Description("Fee charged on cash-out conversions") - val cashout_fee: TalerAmount, - @Description("Smallest possible cash-out amount") - val cashout_tiny_amount: TalerAmount, - @Description("Rounding mode for cash-out conversions") - val cashout_rounding_mode: RoundingMode, - @Description("Minimum amount for cash-out conversions") - val cashout_min_amount: TalerAmount, -) { - fun check(cfg: BankConfig) { - for (regionalAmount in sequenceOf(cashin_fee, cashin_tiny_amount, cashout_min_amount)) { - cfg.checkRegionalCurrency(regionalAmount) - } - for (fiatAmount in sequenceOf(cashout_fee, cashout_tiny_amount, cashin_min_amount)) { - cfg.checkFiatCurrency(fiatAmount) - } - - if (cashout_tiny_amount.isZero()) { - throw badRequest("cashout_tiny_amount must be > 0") - } else if (cashin_tiny_amount.isZero()) { - throw badRequest("cashin_tiny_amount must be > 0") - } else if (cashout_tiny_amount.isSubCent()) { - throw badRequest("Sub-cent amounts no supported by cashout, cashout_tiny_amount must be >= 0.01") - } - } -} - -/** Load bank config at [configPath] */ -fun bankConfig(configPath: Path?): BankConfig = BANK_CONFIG_SOURCE.fromFile(configPath).loadBankConfig() - -/** Run [lambda] with access to a database conn pool */ -suspend fun BankConfig.withDb(lambda: suspend (Database, BankConfig) -> Unit) { - Database(dbCfg, regionalCurrency, fiatCurrency, payto).use { lambda(it, this) } -} - -private fun TalerConfig.loadBankConfig(): BankConfig = section("libeufin-bank").run { - val regionalCurrency = string("currency").require() - var fiatCurrency: String? = null - var fiatCurrencySpec: CurrencySpecification? = null - val allowConversion = boolean("allow_conversion").default(false) - if (allowConversion) { - fiatCurrency = string("fiat_currency").require() - fiatCurrencySpec = currencySpecificationFor(fiatCurrency) - } - val tanChannels = buildMap { - for (channel in TanChannel.entries) { - path("tan_$channel").orNull()?.let { - put(channel, Pair(it, jsonMap("tan_${channel}_env").orNull() ?: mapOf())) - } - } - } - - val baseUrl = baseURL("base_url").require() - val hostname = baseUrl.url.host - - val method = map("wire_type", "payment target type", mapOf( - "iban" to WireMethod.IBAN, - "x-taler-bank" to WireMethod.X_TALER_BANK, - )).default(WireMethod.IBAN, logger, " defaulting to 'iban' but will fail in a future update") - - if (method == WireMethod.X_TALER_BANK) { - val xhostname = string("x_taler_bank_payto_hostname").orNull() - if (xhostname != null && xhostname != hostname) { - logger.warn("deprecated x_taler_bank_payto_hostname '${xhostname}' does not match base_url hostname '$hostname'") - } - } - val payto = BankPaytoCtx( - bic = string("iban_payto_bic").orNull(), - hostname = hostname - ) - - val pwCrypto = map("pwd_hash_algorithm", "password hash algorithm", mapOf( - "bcrypt" to json<PwCrypto.Bcrypt>("pwd_hash_config", "bcrypt JSON config").require() - )).require() - - val ZERO = TalerAmount.zero(regionalCurrency) - val MAX = TalerAmount.max(regionalCurrency) - BankConfig( - cfg = this@loadBankConfig, - name = string("name").default("Taler Bank"), - regionalCurrency = regionalCurrency, - regionalCurrencySpec = currencySpecificationFor(regionalCurrency), - allowRegistration = boolean("allow_registration").default(false), - allowAccountDeletion = boolean("allow_account_deletion").default(false), - allowEditName = boolean("allow_edit_name").default(false), - allowEditCashout = boolean("allow_edit_cashout_payto_uri").default(false), - allowConversion = allowConversion, - defaultDebtLimit = amount("default_debt_limit", regionalCurrency).default(ZERO), - registrationBonus = amount("registration_bonus", regionalCurrency).default(ZERO), - wireTransferFees = amount("wire_transfer_fees", regionalCurrency).default(ZERO), - minAmount = amount("min_wire_transfer_amount", regionalCurrency).default(ZERO), - maxAmount = amount("max_wire_transfer_amount", regionalCurrency).default(MAX), - suggestedWithdrawalExchange = string("suggested_withdrawal_exchange").orNull(), - spaPath = path("spa").orNull(), - fallbackSpaPath = this@loadBankConfig.section("paths").path("datadir").require().resolve("spa"), - baseUrl = baseUrl, - fiatCurrency = fiatCurrency, - fiatCurrencySpec = fiatCurrencySpec, - tanChannels = tanChannels, - payto = payto, - wireMethod = method, - pwCrypto = pwCrypto, - gcAbortAfter = duration("gc_abort_after").require(), - gcCleanAfter = duration("gc_clean_after").require(), - gcDeleteAfter = duration("gc_delete_after").require(), - pwdCheckQuality = boolean("pwd_check").require(), - basicAuthCompat = boolean("pwd_auth_compat").require() - ) -} - -private fun TalerConfig.currencySpecificationFor(currency: String): CurrencySpecification - = sections.find { - val section = section(it) - it.startsWith("CURRENCY-") && section.boolean("enabled").require() && section.string("code").require() == currency - }?.let { section(it).loadCurrencySpecification() } ?: run { - logger.warn("Missing currency specification for $currency, using sane defaults") - CurrencySpecification( - name = currency, - num_fractional_input_digits = 2, - num_fractional_normal_digits = 2, - num_fractional_trailing_zero_digits = 2, - alt_unit_names = mapOf("0" to currency) - ) - } - -private fun TalerConfigSection.loadCurrencySpecification(): CurrencySpecification { - return CurrencySpecification( - name = string("name").require(), - num_fractional_input_digits = number("fractional_input_digits").require(), - num_fractional_normal_digits = number("fractional_normal_digits").require(), - num_fractional_trailing_zero_digits = number("fractional_trailing_zero_digits").require(), - alt_unit_names = jsonMap("alt_unit_names").require() - ) -} diff --git a/libeufin-bank/src/main/kotlin/tech/libeufin/bank/Constants.kt b/libeufin-bank/src/main/kotlin/tech/libeufin/bank/Constants.kt @@ -1,44 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2024-2025 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.bank - -import tech.libeufin.common.ConfigSource -import java.time.Duration - -// Config -val BANK_CONFIG_SOURCE = ConfigSource("libeufin", "libeufin-bank", "libeufin-bank") - -// TAN -const val TAN_RETRY_COUNTER: Int = 3 -val TAN_VALIDITY_PERIOD: Duration = Duration.ofMinutes(30) -val TAN_RETRANSMISSION_PERIOD: Duration = Duration.ofMinutes(3) - -// Token -val TOKEN_DEFAULT_DURATION: Duration = Duration.ofHours(3) - -// Account -val RESERVED_ACCOUNTS = setOf("admin", "bank") -const val IBAN_ALLOCATION_RETRY_COUNTER: Int = 5 -const val MAX_TOKEN_CREATION_ATTEMPTS: Int = 5 -const val MAX_ACTIVE_CHALLENGES: Int = 5 - -// API version -const val COREBANK_API_VERSION: String = "12:0:0" -const val CONVERSION_API_VERSION: String = "2:0:1" -const val INTEGRATION_API_VERSION: String = "5:0:5" diff --git a/libeufin-bank/src/main/kotlin/tech/libeufin/bank/Error.kt b/libeufin-bank/src/main/kotlin/tech/libeufin/bank/Error.kt @@ -1,73 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2023-2025 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.bank - -import tech.libeufin.common.* - -/* ----- Currency checks ----- */ - -fun BankConfig.checkRegionalCurrency(amount: TalerAmount) { - if (amount.currency != regionalCurrency) throw badRequest( - "Wrong currency: expected regional currency $regionalCurrency got ${amount.currency}", - TalerErrorCode.GENERIC_CURRENCY_MISMATCH - ) -} - -fun BankConfig.checkFiatCurrency(amount: TalerAmount) { - if (amount.currency != fiatCurrency) throw badRequest( - "Wrong currency: expected fiat currency $fiatCurrency got ${amount.currency}", - TalerErrorCode.GENERIC_CURRENCY_MISMATCH - ) -} - -fun BankConfig.checkCurrency(input: ConversionRateClassInput) { - for (regionalAmount in sequenceOf(input.cashin_fee, input.cashout_min_amount).filterNotNull()) { - this.checkRegionalCurrency(regionalAmount) - } - for (fiatAmount in sequenceOf(input.cashout_fee, input.cashin_min_amount).filterNotNull()) { - this.checkFiatCurrency(fiatAmount) - } -} - -/* ----- Common errors ----- */ - -fun unknownAccount(id: String) = notFound( - "Account '$id' not found", - TalerErrorCode.BANK_UNKNOWN_ACCOUNT -) - -fun unknownCreditorAccount(id: String) = conflict( - "Creditor account '$id' not found", - TalerErrorCode.BANK_UNKNOWN_CREDITOR -) - -fun unsupportedTanChannel(channel: TanChannel) = conflict( - "Unsupported tan channel $channel", - TalerErrorCode.BANK_TAN_CHANNEL_NOT_SUPPORTED -) - -fun notExchange(username: String): ApiException = conflict( - "Account '$username' is not an exchange account.", - TalerErrorCode.BANK_ACCOUNT_IS_NOT_EXCHANGE -) - -fun unknownConversionClass(id: Long?): ApiException = conflict( - "Unknown conversion rate class $id", - TalerErrorCode.BANK_CONVERSION_RATE_CLASS_UNKNOWN -) -\ No newline at end of file diff --git a/libeufin-bank/src/main/kotlin/tech/libeufin/bank/Main.kt b/libeufin-bank/src/main/kotlin/tech/libeufin/bank/Main.kt @@ -1,86 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2023, 2024, 2025, 2026 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.bank - -import io.ktor.server.application.* -import io.ktor.server.http.content.* -import io.ktor.server.response.* -import io.ktor.server.routing.* -import org.slf4j.Logger -import org.slf4j.LoggerFactory -import tech.libeufin.bank.api.* -import tech.libeufin.bank.cli.LibeufinBank -import tech.libeufin.bank.db.Database -import tech.libeufin.common.api.OpenApiInfo -import tech.libeufin.common.api.talerApi -import tech.libeufin.common.VERSION -import com.github.ajalt.clikt.core.main -import kotlin.io.path.isDirectory - -val logger: Logger = LoggerFactory.getLogger("libeufin-bank") - -/** Set up web server handlers for the Taler corebank API */ -fun Application.corebankWebApp(db: Database, cfg: BankConfig, serveSpec: Boolean = false) = talerApi( - LoggerFactory.getLogger("libeufin-bank-api"), - OpenApiInfo( - title = "LibEuFin Bank API", - version = VERSION, - description = "Taler corebank, wire gateway, wire transfer gateway, integration, conversion, revenue, and observability APIs for LibEuFin Bank", - securityConfig = { - securityScheme("bearerAuth") { - type = io.github.smiley4.ktoropenapi.config.AuthType.HTTP - scheme = io.github.smiley4.ktoropenapi.config.AuthScheme.BEARER - bearerFormat = "token" - description = "Bearer tokens minted by libeufin-bank /accounts/{USERNAME}/token" - } - securityScheme("basicAuth") { - type = io.github.smiley4.ktoropenapi.config.AuthType.HTTP - scheme = io.github.smiley4.ktoropenapi.config.AuthScheme.BASIC - description = "HTTP Basic authentication, supported only on compatibility-enabled endpoints" - } - } - ), - serveSpec = serveSpec -) { - coreBankApi(db, cfg) - conversionApi(db, cfg) - bankIntegrationApi(db, cfg) - wireGatewayApi(db, cfg) - preparedTransferApi(db, cfg) - revenueApi(db, cfg) - observabilityApi(db, cfg) - val spaPath = cfg.spaPath?.takeIf { it.isDirectory() } ?: run { - cfg.spaPath?.let { - logger.warn("SPA not found at '{}', using fallback", it) - } - check(cfg.fallbackSpaPath.isDirectory()) { - "SPA fallback directory not found at '${cfg.fallbackSpaPath}'" - } - cfg.fallbackSpaPath - } - get("/") { - call.respondRedirect("/webui/") - } - staticFiles("/webui/", spaPath.toFile()) -} - -fun main(args: Array<String>) { - LibeufinBank().main(args) -} diff --git a/libeufin-bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt b/libeufin-bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt @@ -1,1045 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2024, 2025, 2026 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.bank - -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.KSerializer -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder -import io.github.smiley4.schemakenerator.core.annotations.Description -import io.github.smiley4.schemakenerator.core.annotations.Name -import tech.libeufin.common.* -import java.time.Instant - -/** - * Allowed lengths for fractional digits in amounts. - */ -enum class FracDigits { - TWO, EIGHT -} - -// Allowed values for bank transactions directions. -@Description("Direction of a bank transaction") -enum class TransactionDirection { - credit, - debit -} - -@Description("Status of a cashout operation") -enum class CashoutStatus { - pending, - aborted, - confirmed -} - -@Description("Status of a withdrawal operation") -enum class WithdrawalStatus { - pending, - aborted, - selected, - confirmed -} - -@Description("Status of a bank account") -enum class AccountStatus { - active, - locked, - deleted -} - -@Description("Rounding mode for currency conversion") -enum class RoundingMode { - zero, - up, - nearest -} - -enum class Timeframe { - hour, - day, - month, - year -} - -enum class Operation { - account_reconfig, - account_delete, - account_auth_reconfig, - bank_transaction, - cashout, - withdrawal, - create_token -} - -@Description("Wire transfer method type") -enum class WireMethod { - IBAN, - X_TALER_BANK -} - -@Serializable(with = Option.Serializer::class) -sealed class Option<out T> { - data object None : Option<Nothing>() - data class Some<T>(val value: T) : Option<T>() - - fun get(): T? { - return when (this) { - None -> null - is Some -> this.value - } - } - - inline fun some(lambda: (T) -> Unit) { - if (this is Some) { - lambda(value) - } - } - - fun isSome(): Boolean = this is Some - - @OptIn(ExperimentalSerializationApi::class) - internal class Serializer<T> ( - private val valueSerializer: KSerializer<T> - ) : KSerializer<Option<T>> { - override val descriptor: SerialDescriptor = valueSerializer.descriptor - - override fun serialize(encoder: Encoder, value: Option<T>) { - when (value) { - None -> encoder.encodeNull() - is Some -> valueSerializer.serialize(encoder, value.value) - } - } - - override fun deserialize(decoder: Decoder): Option<T> { - return Some(valueSerializer.deserialize(decoder)) - } - } - - companion object { - fun <T> invert(optional: Option<T>?): Option<T?> { - return when (optional) { - null -> Option.None - is Option.None -> Option.Some(null) - is Option.Some -> Option.Some(optional.value) - } - } - } -} - -@Serializable -@Description("Response containing TAN challenges") -data class ChallengeResponse( - @Description("List of pending challenges") - val challenges: List<Challenge>, - @Description("Whether all challenges must be solved") - val combi_and: Boolean -) - -@Serializable -@Description("Single TAN challenge details") -data class Challenge( - @Description("Unique challenge identifier") - val challenge_id: String, - @Description("Channel used for TAN delivery") - val tan_channel: TanChannel, - @Description("Masked contact information for TAN") - val tan_info: String -) - -@Serializable -@Description("Response after requesting a TAN challenge") -data class ChallengeRequestResponse( - @Description("Expiration time for solving the challenge") - val solve_expiration: TalerTimestamp, - @Description("Earliest time to request retransmission") - val earliest_retransmission: TalerTimestamp -) - -/** - * HTTP response type of successful token refresh. - * access_token is the Crockford encoding of the 32 byte - * access token, whereas 'expiration' is the point in time - * when this token expires. - */ -@Serializable -@Description("Successful token creation or refresh response") -data class TokenSuccessResponse( - @Description("Crockford-encoded access token") - val access_token: String, - @Description("Token expiration timestamp") - val expiration: TalerTimestamp -) - - -/* Contains contact data to send TAN challges to the -* users, to let them complete cashout operations. */ -@Serializable -@Description("Contact data for TAN challenge delivery") -data class ChallengeContactData( - @Description("Email address for TAN delivery") - val email: Option<String?> = Option.None, - @Description("Phone number for TAN delivery") - val phone: Option<String?> = Option.None -) { - init { - if (email.get()?.let { !EMAIL_PATTERN.matches(it) } == true) - throw badRequest("email contact data '$email' is malformed") - - if (phone.get()?.let { !PHONE_PATTERN.matches(it) } == true) - throw badRequest("phone contact data '$phone' is malformed") - } - companion object { - private val EMAIL_PATTERN = Regex("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,4}") - private val PHONE_PATTERN = Regex("\\+?[0-9]+") - } -} - -// Type expected at POST /accounts -@Serializable -@Description("Request to register a new bank account") -data class RegisterAccountRequest( - @Description("Unique account username") - val username: String, - @Description("Account password") - val password: String, - @Description("Legal name of account holder") - val name: String, - @Description("Whether account is publicly visible") - val is_public: Boolean = false, - @Description("Whether account is a Taler exchange") - val is_taler_exchange: Boolean = false, - @Description("Contact data for TAN challenges") - val contact_data: ChallengeContactData? = null, - @Description("Cashout payto URI for fiat withdrawals") - val cashout_payto_uri: IbanPayto? = null, - @Description("Payto URI for the account") - val payto_uri: Payto? = null, - @Description("Maximum allowed debit balance") - val debit_threshold: TalerAmount? = null, - @Description("Preferred TAN channel (deprecated)") - val tan_channel: TanChannel? = null, - @Description("Set of enabled TAN channels") - val tan_channels: Set<TanChannel>? = null, - @Description("Conversion rate class identifier") - val conversion_rate_class_id: Long? = null -) { - init { - if (!USERNAME_REGEX.matches(username)) - throw badRequest("username '$username' is malformed, must match [a-zA-Z0-9\\-\\._~]{1,126}") - else if (tan_channel != null && tan_channels != null) - throw badRequest("you must only use either tan_channel or tan_channels") - } - - @Description("Computed set of enabled TAN channels") - val channels: Set<TanChannel> get() { - if (tan_channels != null) { - return tan_channels - } else if (tan_channel != null) { - return setOf(tan_channel) - } else { - return emptySet() - } - } - - companion object { - private val USERNAME_REGEX = Regex("^[a-zA-Z0-9-._~]{1,126}$") - } -} - -@Serializable -@Description("Response after successful account registration") -data class RegisterAccountResponse( - @Description("Internal payto URI of the new account") - val internal_payto_uri: String -) - -/** - * Request of PATCH /accounts/{USERNAME} - */ -@Serializable -@Description("Request to reconfigure an existing account") -data class AccountReconfiguration( - @Description("Updated contact data for TAN challenges") - val contact_data: ChallengeContactData? = null, - @Description("Updated cashout payto URI") - val cashout_payto_uri: Option<IbanPayto?> = Option.None, - @Description("Updated legal name of account holder") - val name: String? = null, - @Description("Whether account is publicly visible") - val is_public: Boolean? = null, - @Description("Updated maximum allowed debit balance") - val debit_threshold: TalerAmount? = null, - @Description("Updated preferred TAN channel (deprecated)") - val tan_channel: Option<TanChannel?> = Option.None, - @Description("Updated set of enabled TAN channels") - val tan_channels: Option<Set<TanChannel>> = Option.None, - @Description("Whether account is a Taler exchange") - val is_taler_exchange: Boolean? = null, - @Description("Updated conversion rate class identifier") - val conversion_rate_class_id: Option<Long?> = Option.None -) { - init { - if (tan_channel.isSome() && tan_channels.isSome()) - throw badRequest("you must only use either tan_channel or tan_channels") - } - - @Description("Computed set of enabled TAN channels") - val channels: Option<Set<TanChannel>> get() { - if (tan_channels.isSome()) { - return tan_channels - } else if (tan_channel is Option.Some) { - if (tan_channel.value == null) { - return Option.Some(emptySet()) - } else { - return Option.Some(setOf(tan_channel.value)) - } - } else { - return Option.None - } - } -} - -/** - * Type expected at POST /accounts/{USERNAME}/token - * It complies with Taler's design document #49 - */ -@Serializable -@Description("Request to create an authentication token") -data class TokenRequest( - @Description("Permission scope for the token") - val scope: TokenScope, - @Description("Token validity duration") - val duration: RelativeTime? = null, - @Description("Human-readable token description") - val description: String? = null, - @Description("Whether the token can be refreshed") - val refreshable: Boolean = false -) - -@Serializable -@Description("Monitor statistics response") -sealed interface MonitorResponse { - val talerInCount: Long - val talerInVolume: TalerAmount - val talerOutCount: Long - val talerOutVolume: TalerAmount -} - -@Serializable -@SerialName("no-conversions") -@Name("MonitorNoConversion", qualifiedName = "no-conversions") -@Description("Monitor stats without currency conversion") -data class MonitorNoConversion( - @Description("Number of incoming Taler transactions") - override val talerInCount: Long, - @Description("Total volume of incoming Taler transactions") - override val talerInVolume: TalerAmount, - @Description("Number of outgoing Taler transactions") - override val talerOutCount: Long, - @Description("Total volume of outgoing Taler transactions") - override val talerOutVolume: TalerAmount -) : MonitorResponse - -@Serializable -@SerialName("with-conversions") -@Name("MonitorWithConversion", qualifiedName = "with-conversions") -@Description("Monitor stats with currency conversion") -data class MonitorWithConversion( - @Description("Number of cash-in operations") - val cashinCount: Long, - @Description("Total regional currency cash-in volume") - val cashinRegionalVolume: TalerAmount, - @Description("Total fiat currency cash-in volume") - val cashinFiatVolume: TalerAmount, - @Description("Number of cashout operations") - val cashoutCount: Long, - @Description("Total regional currency cashout volume") - val cashoutRegionalVolume: TalerAmount, - @Description("Total fiat currency cashout volume") - val cashoutFiatVolume: TalerAmount, - @Description("Number of incoming Taler transactions") - override val talerInCount: Long, - @Description("Total volume of incoming Taler transactions") - override val talerInVolume: TalerAmount, - @Description("Number of outgoing Taler transactions") - override val talerOutCount: Long, - @Description("Total volume of outgoing Taler transactions") - override val talerOutVolume: TalerAmount -) : MonitorResponse - -typealias Tans = List<Pair<TanChannel, String>> - -/** - * Convenience type to get bank account information - * from/to the database. - */ -data class BankInfo( - val username: String, - val payto: String, - val bankAccountId: Long, - val isTalerExchange: Boolean, - override val phone: String?, - override val email: String?, - override val channels: Set<TanChannel> -): TanInfo - -interface TanInfo { - val phone: String? - val email: String? - val channels: Set<TanChannel> - - val mfa: Tans get() = channels.map { channel -> - val info = when (channel) { - TanChannel.sms -> phone - TanChannel.email -> email - } - Pair(channel, requireNotNull(info)) - } -} - -// Allowed values for cashout TAN channels. -@Description("Channel for TAN delivery") -enum class TanChannel { - sms, - email -} - -// Scopes for authentication tokens. -@Description("Scope for authentication tokens") -enum class TokenScope { - readonly, - readwrite, - revenue, - wiregateway, - observability; - - - fun logical(): TokenLogicalScope = when (this) { - readonly -> TokenLogicalScope.readonly - readwrite -> TokenLogicalScope.readwrite - revenue -> TokenLogicalScope.revenue - wiregateway -> TokenLogicalScope.readwrite_wiregateway - observability -> TokenLogicalScope.observability - } -} - -enum class TokenLogicalScope { - readonly, - readwrite, - revenue, - refreshable, - readonly_wiregateway, - readwrite_wiregateway, - observability -} - -data class BearerToken( - val scope: TokenScope, - val isRefreshable: Boolean, - val creationTime: Instant, - val expirationTime: Instant -) - -@Serializable -@Description("Information about an authentication token") -data class TokenInfo( - @Description("Token creation timestamp") - val creation_time: TalerTimestamp, - @Description("Token expiration timestamp") - val expiration: TalerTimestamp, - @Description("Permission scope of the token") - val scope: TokenScope, - @Description("Whether the token can be refreshed") - val refreshable: Boolean, - @Description("Human-readable token description") - val description: String? = null, - @Description("Timestamp of last token usage") - val last_access: TalerTimestamp, - @Description("Row identifier of the token") - val row_id: Long, - @Description("Unique token identifier") - val token_id: Long -) - - -@Serializable -@Description("List of authentication tokens") -data class TokenInfos ( - @Description("Array of token information entries") - val tokens: List<TokenInfo> -) - -@Serializable -@Description("Bank configuration response") -data class Config( - @Description("Regional currency code") - val currency: String, - @Description("Currency specification for display") - val currency_specification: CurrencySpecification, - @Description("Base URL of the bank") - val base_url: BaseURL?, - @Description("Human-readable bank name") - val bank_name: String, - @Description("Whether currency conversion is enabled") - val allow_conversion: Boolean, - @Description("Whether account registration is open") - val allow_registrations: Boolean, - @Description("Whether account deletion is allowed") - val allow_deletions: Boolean, - @Description("Whether users can edit their legal name") - val allow_edit_name: Boolean, - @Description("Whether users can edit their cashout payto URI") - val allow_edit_cashout_payto_uri: Boolean, - @Description("Default debit threshold for new accounts") - val default_debit_threshold: TalerAmount, - @Description("Supported TAN channels for two-factor authentication") - val supported_tan_channels: Set<TanChannel>, - @Description("Wire transfer method (IBAN or X_TALER_BANK)") - val wire_type: WireMethod, - @Description("Fee charged per wire transfer") - val wire_transfer_fees: TalerAmount, - @Description("Minimum wire transfer amount") - val min_wire_transfer_amount: TalerAmount, - @Description("Maximum wire transfer amount") - val max_wire_transfer_amount: TalerAmount -) { - @Description("API name identifier") - val name: String = "taler-corebank" - @Description("API version string") - val version: String = COREBANK_API_VERSION -} - -@Serializable -@Description("Currency conversion configuration") -data class ConversionConfig( - @Description("Regional currency code") - val regional_currency: String, - @Description("Regional currency display specification") - val regional_currency_specification: CurrencySpecification, - @Description("Fiat currency code") - val fiat_currency: String, - @Description("Fiat currency display specification") - val fiat_currency_specification: CurrencySpecification, - @Description("Applicable conversion rate") - val conversion_rate: ConversionRate -) { - @Description("API name identifier") - val name: String = "taler-conversion-info" - @Description("API version string") - val version: String = CONVERSION_API_VERSION -} - -@Serializable -@Description("Taler integration API configuration response") -data class TalerIntegrationConfigResponse( - @Description("Currency used by this bank") - val currency: String, - @Description("Currency display specification") - val currency_specification: CurrencySpecification -) { - @Description("API name identifier") - val name: String = "taler-bank-integration" - @Description("API version string") - val version: String = INTEGRATION_API_VERSION -} - -@Description("Indicates whether a transaction is a credit or debit") -enum class CreditDebitInfo { - credit, debit -} - -@Serializable -@Description("Account balance information") -data class Balance( - @Description("Balance amount") - val amount: TalerAmount, - @Description("Whether balance is credit or debit") - val credit_debit_indicator: CreditDebitInfo, -) - -/** - * GET /accounts response. - */ -@Serializable -@Description("Minimal account data for account listings") -data class AccountMinimalData( - @Description("Account username") - val username: String, - @Description("Legal name of account holder") - val name: String, - @Description("Payto URI of the account") - val payto_uri: String, - @Description("Current account balance") - val balance: Balance, - @Description("Maximum allowed debit balance") - val debit_threshold: TalerAmount, - @Description("Whether account is publicly visible") - val is_public: Boolean, - @Description("Whether account is a Taler exchange") - val is_taler_exchange: Boolean, - @Description("Whether account is locked") - val is_locked: Boolean, - @Description("Row identifier of the account") - val row_id: Long, - @Description("Current account status") - val status: AccountStatus, - @Description("Conversion rate class identifier") - val conversion_rate_class_id: Long? = null, - @Description("Applicable conversion rate") - val conversion_rate: ConversionRate? = null -) - -/** - * Response type of GET /accounts. - */ -@Serializable -@Description("Paginated list of bank accounts") -data class ListBankAccountsResponse( - @Description("Array of account summary entries") - val accounts: List<AccountMinimalData> -) - -/** - * GET /accounts/$USERNAME response. - */ -@Serializable -@Description("Detailed account data for a single account") -data class AccountData( - @Description("Legal name of account holder") - val name: String, - @Description("Payto URI of the account") - val payto_uri: String, - @Description("Current account balance") - val balance: Balance, - @Description("Maximum allowed debit balance") - val debit_threshold: TalerAmount, - @Description("Contact data for TAN challenges") - val contact_data: ChallengeContactData? = null, - @Description("Cashout payto URI for fiat withdrawals") - val cashout_payto_uri: String? = null, - @Description("Preferred TAN channel (deprecated)") - val tan_channel: TanChannel? = null, - @Description("Set of enabled TAN channels") - val tan_channels: Set<TanChannel> = emptySet(), - @Description("Whether account is publicly visible") - val is_public: Boolean, - @Description("Whether account is a Taler exchange") - val is_taler_exchange: Boolean, - @Description("Whether account is locked") - val is_locked: Boolean, - @Description("Current account status") - val status: AccountStatus, - @Description("Conversion rate class identifier") - val conversion_rate_class_id: Long? = null, - @Description("Applicable conversion rate") - val conversion_rate: ConversionRate? = null -) - -@Serializable -@Description("Request to create a bank transaction") -data class TransactionCreateRequest( - @Description("Recipient payto URI") - val payto_uri: Payto, - @Description("Transaction amount") - val amount: TalerAmount?, - @Description("Idempotency key for the request") - val request_uid: ShortHashCode? -) - -@Serializable -@Description("Response after creating a bank transaction") -data class TransactionCreateResponse( - @Description("Row identifier of the new transaction") - val row_id: Long -) - -/* History element, either from GET /transactions/T_ID - or from GET /transactions */ -@Serializable -@Description("Details of a single bank transaction") -data class BankAccountTransactionInfo( - @Description("Payto URI of the creditor") - val creditor_payto_uri: String, - @Description("Payto URI of the debtor") - val debtor_payto_uri: String, - @Description("Transaction amount") - val amount: TalerAmount, - @Description("Transaction direction (credit or debit)") - val direction: TransactionDirection, - @Description("Wire transfer subject line") - val subject: String, - @Description("Row identifier of the transaction") - val row_id: Long, // is T_ID - @Description("Transaction timestamp") - val date: TalerTimestamp -) - -// Response type for histories, namely GET /transactions -@Serializable -@Description("Paginated list of bank transactions") -data class BankAccountTransactionsResponse( - @Description("Array of transaction entries") - val transactions: List<BankAccountTransactionInfo> -) - -// Taler withdrawal request. -@Serializable -@Description("Request to create a Taler withdrawal") -data class BankAccountCreateWithdrawalRequest( - @Description("Exact withdrawal amount") - val amount: TalerAmount? = null, - @Description("Suggested withdrawal amount for wallet") - val suggested_amount: TalerAmount? = null, - @Description("Whether wallet should choose the amount") - val no_amount_to_wallet: Boolean = false -) - -// Taler withdrawal response. -@Serializable -@Description("Response after creating a Taler withdrawal") -data class BankAccountCreateWithdrawalResponse( - @Description("Unique withdrawal operation identifier") - val withdrawal_id: String, - @Description("Taler URI for the wallet to process") - val taler_withdraw_uri: String -) - -@Serializable -@Description("Public information about a withdrawal operation") -data class WithdrawalPublicInfo ( - @Description("Current withdrawal status") - val status: WithdrawalStatus, - @Description("Withdrawal amount if fixed") - val amount: TalerAmount? = null, - @Description("Suggested withdrawal amount") - val suggested_amount: TalerAmount? = null, - @Description("Whether wallet should choose the amount") - val no_amount_to_wallet: Boolean, - @Description("Username of the withdrawing account") - val username: String, - @Description("Selected reserve public key") - val selected_reserve_pub: EddsaPublicKey? = null, - @Description("Selected exchange payto account") - val selected_exchange_account: String? = null, -) - -@Serializable -@Description("Currency display specification") -data class CurrencySpecification( - @Description("Human-readable currency name") - val name: String, - @Description("Fractional digits for input") - val num_fractional_input_digits: Int, - @Description("Fractional digits for normal display") - val num_fractional_normal_digits: Int, - @Description("Trailing zero digits to display") - val num_fractional_trailing_zero_digits: Int, - @Description("Alternative unit names by power of ten") - val alt_unit_names: Map<String, String> -) - - -@Serializable -@Description("Detailed withdrawal operation status") -data class BankWithdrawalOperationStatus( - @Description("Current withdrawal status") - val status: WithdrawalStatus, - @Description("Withdrawal amount if fixed") - val amount: TalerAmount? = null, - @Description("Suggested withdrawal amount") - val suggested_amount: TalerAmount? = null, - @Description("Minimum allowed withdrawal amount") - val min_amount: TalerAmount? = null, - @Description("Maximum allowed withdrawal amount") - val max_amount: TalerAmount? = null, - @Description("Card processing fees") - val card_fees: TalerAmount? = null, - @Description("Sender wire account details") - val sender_wire: String? = null, - @Description("Suggested exchange base URL") - val suggested_exchange: String? = null, - @Description("Required exchange base URL") - val required_exchange: String? = null, - @Description("URL to confirm the wire transfer") - val confirm_transfer_url: String? = null, - @Description("Supported wire transfer types") - val wire_types: List<String>, - @Description("Selected reserve public key") - val selected_reserve_pub: EddsaPublicKey? = null, - @Description("Selected exchange payto account") - val selected_exchange_account: String? = null, - @Description("Whether wallet should choose the amount") - val no_amount_to_wallet: Boolean = false, - @Description("Currency of the withdrawal") - val currency: String? = null, - // TODO deprecated remove in the next breaking release - @Description("Whether withdrawal was aborted (deprecated)") - val aborted: Boolean, - @Description("Whether exchange was selected (deprecated)") - val selection_done: Boolean, - @Description("Whether wire transfer completed (deprecated)") - val transfer_done: Boolean, -) - -/** - * Selection request on a Taler withdrawal. - */ -@Serializable -@Description("Wallet request to select exchange for withdrawal") -data class BankWithdrawalOperationPostRequest( - @Description("Reserve public key from the wallet") - val reserve_pub: EddsaPublicKey, - @Description("Selected exchange payto URI") - val selected_exchange: Payto, - @Description("Withdrawal amount chosen by wallet") - val amount: TalerAmount? = null -) - -/** - * Response to the wallet after it selects the exchange - * and the reserve pub. - */ -@Serializable -@Description("Response after wallet selects exchange") -data class BankWithdrawalOperationPostResponse( - @Description("Current withdrawal status") - val status: WithdrawalStatus, - @Description("URL to confirm the wire transfer") - val confirm_transfer_url: String? = null, - // TODO deprecated remove in the next breaking release - @Description("Whether wire transfer completed (deprecated)") - val transfer_done: Boolean, -) - -@Serializable -@Description("Request to initiate a cashout operation") -data class CashoutRequest( - @Description("Idempotency key for the request") - val request_uid: ShortHashCode, - @Description("Wire transfer subject line") - val subject: String?, - @Description("Amount debited in regional currency") - val amount_debit: TalerAmount, - @Description("Amount credited in fiat currency") - val amount_credit: TalerAmount -) - -@Serializable -@Description("Response after creating a cashout operation") -data class CashoutResponse( - @Description("Unique cashout operation identifier") - val cashout_id: Long, -) - -@Serializable -@Description("List of cashout operations for an account") -data class Cashouts( - @Description("Array of cashout summary entries") - val cashouts: List<CashoutInfo>, -) - -@Serializable -@Description("Summary of a single cashout operation") -data class CashoutInfo( - @Description("Unique cashout operation identifier") - val cashout_id: Long, - @Description("Current cashout status") - val status: CashoutStatus, -) - - -@Serializable -@Description("Global list of cashout operations") -data class GlobalCashouts( - @Description("Array of global cashout entries") - val cashouts: List<GlobalCashoutInfo>, -) - -@Serializable -@Description("Global cashout entry with username") -data class GlobalCashoutInfo( - @Description("Unique cashout operation identifier") - val cashout_id: Long, - @Description("Account username of the cashout owner") - val username: String, - @Description("Current cashout status") - val status: CashoutStatus, -) - -@Serializable -@Description("Detailed status of a cashout operation") -data class CashoutStatusResponse( - @Description("Amount debited in regional currency") - val amount_debit: TalerAmount, - @Description("Amount credited in fiat currency") - val amount_credit: TalerAmount, - @Description("Wire transfer subject line") - val subject: String, - @Description("Cashout creation timestamp") - val creation_time: TalerTimestamp, - @Description("Cashout confirmation timestamp") - val confirmation_time: TalerTimestamp? = null // TODO update doc -) - -@Serializable -@Description("Request to solve a TAN challenge") -data class ChallengeSolve( - @Description("TAN code entered by the user") - val tan: String -) - -@Serializable -@Description("Currency conversion result") -data class ConversionResponse( - @Description("Amount debited from source currency") - val amount_debit: TalerAmount, - @Description("Amount credited in target currency") - val amount_credit: TalerAmount, -) - -/** - * Response to GET /public-accounts - */ -@Serializable -@Description("List of publicly visible accounts") -data class PublicAccountsResponse( - @Description("Array of public account entries") - val public_accounts: List<PublicAccount> -) - -/** - * Single element of GET /public-accounts list. - */ -@Serializable -@Description("Public account summary information") -data class PublicAccount( - @Description("Account username") - val username: String, - @Description("Payto URI of the account") - val payto_uri: String, - @Description("Current account balance") - val balance: Balance, - @Description("Whether account is a Taler exchange") - val is_taler_exchange: Boolean, - @Description("Row identifier of the account") - val row_id: Long -) - -/** - * Request of PATCH /accounts/{USERNAME}/auth - */ -@Serializable -@Description("Request to change account password") -data class AccountPasswordChange( - @Description("New password for the account") - val new_password: String, - @Description("Current password for verification") - val old_password: String? = null -) - -// Request POST /accounts/{USERNAME}/withdrawals/{WITHDRAWAL_ID}/confirm -@Serializable -@Description("Request to confirm a withdrawal operation") -data class BankAccountConfirmWithdrawalRequest( - @Description("Final withdrawal amount") - val amount: TalerAmount? = null -) - -/** -* Request POST /conversion-rate-classes -* Request PATCH /conversion-rate-classes/{CLASS_ID} -*/ -@Serializable -@Description("Input for creating or updating a conversion rate class") -data class ConversionRateClassInput( - @Description("Human-readable class name") - val name: String, - @Description("Description of the rate class") - val description: String? = null, - @Description("Cash-in conversion ratio") - val cashin_ratio: DecimalNumber? = null, - @Description("Fee applied on cash-in operations") - val cashin_fee: TalerAmount? = null, - @Description("Rounding mode for cash-in amounts") - val cashin_rounding_mode: RoundingMode? = null, - @Description("Minimum amount for cash-in operations") - val cashin_min_amount: TalerAmount? = null, - @Description("Cashout conversion ratio") - val cashout_ratio: DecimalNumber? = null, - @Description("Fee applied on cashout operations") - val cashout_fee: TalerAmount? = null, - @Description("Rounding mode for cashout amounts") - val cashout_rounding_mode: RoundingMode? = null, - @Description("Minimum amount for cashout operations") - val cashout_min_amount: TalerAmount? = null -) - -/** Response POST /conversion-rate-classes */ -@Serializable -@Description("Response after creating a conversion rate class") -data class ConversionRateClassResponse( - @Description("Identifier of the created rate class") - val conversion_rate_class_id: Long, -) - -/** -* Response GET /conversion-rate-classes/{CLASS_ID} -*/ -@Serializable -@Description("Detailed conversion rate class information") -data class ConversionRateClass( - @Description("Unique rate class identifier") - val conversion_rate_class_id: Long, - @Description("Human-readable class name") - val name: String, - @Description("Description of the rate class") - val description: String? = null, - @Description("Number of users in this class") - val num_users: Int, - @Description("Cash-in conversion ratio") - val cashin_ratio: DecimalNumber? = null, - @Description("Fee applied on cash-in operations") - val cashin_fee: TalerAmount? = null, - @Description("Rounding mode for cash-in amounts") - val cashin_rounding_mode: RoundingMode? = null, - @Description("Minimum amount for cash-in operations") - val cashin_min_amount: TalerAmount? = null, - @Description("Cashout conversion ratio") - val cashout_ratio: DecimalNumber? = null, - @Description("Fee applied on cashout operations") - val cashout_fee: TalerAmount? = null, - @Description("Rounding mode for cashout amounts") - val cashout_rounding_mode: RoundingMode? = null, - @Description("Minimum amount for cashout operations") - val cashout_min_amount: TalerAmount? = null -) - -/** -* Response GET /conversion-rate-classes -*/ -@Serializable -@Description("List of all conversion rate classes") -data class ConversionRateClasses( - @Description("Array of conversion rate class entries") - val classes: List<ConversionRateClass> -) diff --git a/libeufin-bank/src/main/kotlin/tech/libeufin/bank/api/BankIntegrationApi.kt b/libeufin-bank/src/main/kotlin/tech/libeufin/bank/api/BankIntegrationApi.kt @@ -1,187 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2024, 2025, 2026 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/> - */ - -/* This file contains the Taler Integration API endpoints, -* that are typically requested by wallets. */ -package tech.libeufin.bank.api - -import io.ktor.http.* -import io.ktor.server.application.* -import io.ktor.server.request.* -import io.ktor.server.response.* -import io.ktor.server.routing.* -import io.github.smiley4.ktoropenapi.* -import tech.libeufin.bank.* -import tech.libeufin.bank.db.AbortResult -import tech.libeufin.bank.db.Database -import tech.libeufin.bank.db.WithdrawalDAO.WithdrawalSelectionResult -import tech.libeufin.common.* - -fun Routing.bankIntegrationApi(db: Database, ctx: BankConfig) { - get("/taler-integration/config", { - operationId = "getIntegrationConfig" - description = "Get bank integration configuration" - tags = listOf("Bank Integration") - response { - code(HttpStatusCode.OK) { description = "Integration configuration"; body<TalerIntegrationConfigResponse>() } - } - }) { - call.respond(TalerIntegrationConfigResponse( - currency = ctx.regionalCurrency, - currency_specification = ctx.regionalCurrencySpec - )) - } - - // Note: wopid acts as an authentication token. - get("/taler-integration/withdrawal-operation/{wopid}", { - operationId = "getIntegrationWithdrawalStatus" - description = "Get withdrawal operation status" - tags = listOf("Bank Integration") - request { - pathParameter<String>("wopid") { description = "Withdrawal operation ID" } - queryParameter<Long>("timeout_ms") { - description = "Timeout for long polling in milliseconds (default: 0, no long polling)" - required = false - } - queryParameter<String>("old_state") { - description = "Previous state for long polling (pending, aborted, selected, confirmed). Default: pending" - required = false - } - } - response { - code(HttpStatusCode.OK) { description = "Withdrawal operation status"; body<BankWithdrawalOperationStatus>() } - code(HttpStatusCode.NotFound) { description = "Withdrawal operation not found" } - } - }) { - val uuid = call.uuidPath("wopid") - val params = StatusParams.extract(call.request.queryParameters) - val op = db.withdrawal.pollStatus( - uuid, - params, - ctx.wireMethod, - ctx.maxAmount - )?.copy( - card_fees = ctx.wireTransferFees, - min_amount = ctx.minAmount - ) ?: throw notFound( - "Withdrawal operation '$uuid' not found", - TalerErrorCode.BANK_TRANSACTION_NOT_FOUND - ) - call.respond(op.copy( - suggested_exchange = ctx.suggestedWithdrawalExchange, - confirm_transfer_url = if (op.status == WithdrawalStatus.pending || op.status == WithdrawalStatus.selected) ctx.withdrawConfirmUrl(uuid) else null - )) - } - post("/taler-integration/withdrawal-operation/{wopid}", { - operationId = "selectWithdrawalExchange" - description = "Select exchange and reserve for withdrawal" - tags = listOf("Bank Integration") - request { - pathParameter<String>("wopid") { description = "Withdrawal operation ID" } - body<BankWithdrawalOperationPostRequest>() - } - response { - code(HttpStatusCode.OK) { description = "Exchange selected"; body<BankWithdrawalOperationPostResponse>() } - code(HttpStatusCode.NotFound) { description = "Withdrawal operation not found" } - code(HttpStatusCode.Conflict) { description = "Operation conflict" } - } - }) { - val uuid = call.uuidPath("wopid") - val req = call.receive<BankWithdrawalOperationPostRequest>() - req.amount?.run(ctx::checkRegionalCurrency) - val res = db.withdrawal.setDetails( - uuid, - req.selected_exchange, - req.reserve_pub, - req.amount, - ctx.wireTransferFees, - ctx.minAmount, - ctx.maxAmount - ) - - when (res) { - WithdrawalSelectionResult.UnknownOperation -> throw notFound( - "Withdrawal operation '$uuid' not found", - TalerErrorCode.BANK_TRANSACTION_NOT_FOUND - ) - WithdrawalSelectionResult.AlreadySelected -> throw conflict( - "Cannot select different exchange and reserve pub. under the same withdrawal operation", - TalerErrorCode.BANK_WITHDRAWAL_OPERATION_RESERVE_SELECTION_CONFLICT - ) - WithdrawalSelectionResult.ReservePubReuse -> throw conflict( - "Reserve pub already used", - TalerErrorCode.BANK_DUPLICATE_RESERVE_PUB_SUBJECT - ) - WithdrawalSelectionResult.UnknownAccount -> throw conflict( - "Account ${req.selected_exchange} not found", - TalerErrorCode.BANK_UNKNOWN_ACCOUNT - ) - WithdrawalSelectionResult.AccountIsNotExchange -> throw notExchange(req.selected_exchange.toString()) - WithdrawalSelectionResult.AmountDiffers -> throw conflict( - "Given amount is different from the current", - TalerErrorCode.BANK_AMOUNT_DIFFERS - ) - WithdrawalSelectionResult.BalanceInsufficient -> throw conflict( - "Insufficient funds", - TalerErrorCode.BANK_UNALLOWED_DEBIT - ) - WithdrawalSelectionResult.BadAmount -> throw conflict( - "Amount either to high or too low", - TalerErrorCode.BANK_UNALLOWED_DEBIT - ) - WithdrawalSelectionResult.AlreadyAborted -> throw conflict( - "Cannot update an aborted withdrawal", - TalerErrorCode.BANK_UPDATE_ABORT_CONFLICT - ) - is WithdrawalSelectionResult.Success -> { - call.respond(BankWithdrawalOperationPostResponse( - transfer_done = res.status == WithdrawalStatus.confirmed, - status = res.status, - confirm_transfer_url = if (res.status == WithdrawalStatus.pending || res.status == WithdrawalStatus.selected) ctx.withdrawConfirmUrl(uuid) else null - )) - } - } - } - post("/taler-integration/withdrawal-operation/{wopid}/abort", { - operationId = "abortIntegrationWithdrawal" - description = "Abort a withdrawal operation" - tags = listOf("Bank Integration") - request { - pathParameter<String>("wopid") { description = "Withdrawal operation ID" } - } - response { - code(HttpStatusCode.NoContent) { description = "Withdrawal aborted successfully" } - code(HttpStatusCode.NotFound) { description = "Withdrawal operation not found" } - code(HttpStatusCode.Conflict) { description = "Cannot abort confirmed withdrawal" } - } - }) { - val uuid = call.uuidPath("wopid") - when (db.withdrawal.abort(uuid)) { - AbortResult.UnknownOperation -> throw notFound( - "Withdrawal operation '$uuid' not found", - TalerErrorCode.BANK_TRANSACTION_NOT_FOUND - ) - AbortResult.AlreadyConfirmed -> throw conflict( - "Cannot abort confirmed withdrawal", - TalerErrorCode.BANK_ABORT_CONFIRM_CONFLICT - ) - AbortResult.Success -> call.respond(HttpStatusCode.NoContent) - } - } -} -\ No newline at end of file diff --git a/libeufin-bank/src/main/kotlin/tech/libeufin/bank/api/ConversionApi.kt b/libeufin-bank/src/main/kotlin/tech/libeufin/bank/api/ConversionApi.kt @@ -1,408 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2023-2025 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.bank.api - -import io.ktor.http.* -import io.ktor.server.application.* -import io.ktor.server.request.* -import io.ktor.server.response.* -import io.ktor.server.routing.* -import io.github.smiley4.ktoropenapi.* -import tech.libeufin.bank.* -import tech.libeufin.bank.auth.* -import tech.libeufin.bank.db.ConversionDAO -import tech.libeufin.bank.db.ConversionDAO.ConversionResult -import tech.libeufin.bank.db.Database -import tech.libeufin.common.* - -private suspend fun ApplicationCall.config(db: Database, cfg: BankConfig) { - respond( - ConversionConfig( - regional_currency = cfg.regionalCurrency, - regional_currency_specification = cfg.regionalCurrencySpec, - fiat_currency = cfg.fiatCurrency!!, - fiat_currency_specification = cfg.fiatCurrencySpec!!, - conversion_rate = db.conversion.getDefaultRate() - ) - ) -} -private suspend fun ApplicationCall.setGlobal(db: Database, cfg: BankConfig) { - val req = receive<ConversionRate>() - req.check(cfg) - db.conversion.updateConfig(req) - respond(HttpStatusCode.NoContent) -} -fun Routing.conversionApi(db: Database, cfg: BankConfig) = conditional(cfg.allowConversion) { - get("/conversion-info/config", { - operationId = "getConversionConfig" - description = "Get conversion configuration" - tags = listOf("Conversion") - response { - code(HttpStatusCode.OK) { description = "Conversion configuration"; body<ConversionConfig>() } - } - }) { call.config(db, cfg) } - get("/conversion-rate-classes/{CLASS_ID}/conversion-info/config", { - operationId = "getConversionConfigForClass" - description = "Get conversion configuration for a rate class" - tags = listOf("Conversion") - request { - pathParameter<Long>("CLASS_ID") { description = "Conversion rate class ID" } - } - response { - code(HttpStatusCode.OK) { description = "Conversion configuration for rate class"; body<ConversionConfig>() } - } - }) { call.config(db, cfg) } - get("/accounts/{USERNAME}/conversion-info/config", { - operationId = "getConversionConfigForAccount" - description = "Get conversion configuration for an account" - tags = listOf("Conversion") - request { - pathParameter<String>("USERNAME") { description = "Account username" } - } - response { - code(HttpStatusCode.OK) { description = "Conversion configuration for account"; body<ConversionConfig>() } - } - }) { call.config(db, cfg) } - authAdmin(db, cfg.pwCrypto, TokenLogicalScope.readwrite, cfg.basicAuthCompat) { - post("/conversion-info/conversion-rate", { - operationId = "setGlobalConversionRate" - description = "Set global conversion rate" - tags = listOf("Conversion") - protected = true - securitySchemeNames("bearerAuth", "basicAuth") - request { - body<ConversionRate>() - } - response { - code(HttpStatusCode.NoContent) { description = "Conversion rate updated" } - } - }) { call.setGlobal(db, cfg) } - post("/accounts/{USERNAME}/conversion-info/conversion-rate", { - operationId = "setAccountConversionRate" - description = "Set conversion rate for an account" - tags = listOf("Conversion") - protected = true - securitySchemeNames("bearerAuth", "basicAuth") - request { - pathParameter<String>("USERNAME") { description = "Account username" } - body<ConversionRate>() - } - response { - code(HttpStatusCode.NoContent) { description = "Conversion rate updated" } - } - }) { call.setGlobal(db, cfg) } - post("/conversion-rate-classes/{CLASS_ID}/conversion-info/conversion-rate", { - operationId = "setClassConversionRate" - description = "Set conversion rate for a rate class" - tags = listOf("Conversion") - protected = true - securitySchemeNames("bearerAuth", "basicAuth") - request { - pathParameter<Long>("CLASS_ID") { description = "Conversion rate class ID" } - body<ConversionRate>() - } - response { - code(HttpStatusCode.NoContent) { description = "Conversion rate updated" } - } - }) { call.setGlobal(db, cfg) } - } - suspend fun ApplicationCall.convert( - input: TalerAmount, - conversion: suspend ConversionDAO.(TalerAmount) -> ConversionResult, - output: (TalerAmount) -> ConversionResponse - ) { - when (val res = db.conversion.(conversion)(input)) { - is ConversionResult.Success -> respond(output(res.converted)) - ConversionResult.ToSmall -> throw conflict( - "$input is too small to be converted", - TalerErrorCode.BANK_BAD_CONVERSION - ) - ConversionResult.IsExchange -> throw conflict( - "exchange accounts cannot cashout", - TalerErrorCode.BANK_ACCOUNT_IS_EXCHANGE - ) - ConversionResult.NotExchange -> throw conflict( - "only exchange accounts can cashin", - TalerErrorCode.BANK_ACCOUNT_IS_NOT_EXCHANGE - ) - } - } - get("/conversion-info/rate", { - operationId = "getDefaultConversionRate" - description = "Get default conversion rate" - tags = listOf("Conversion") - response { - code(HttpStatusCode.OK) { description = "Default conversion rate"; body<ConversionRate>() } - } - }) { - call.respond(db.conversion.getDefaultRate()) - } - get("/conversion-info/cashout-rate", { - operationId = "getDefaultCashoutRate" - description = "Calculate cashout conversion rate" - tags = listOf("Conversion") - request { - queryParameter<String>("amount_debit") { - description = "Amount to convert from regional currency (mutually exclusive with amount_credit)" - required = false - } - queryParameter<String>("amount_credit") { - description = "Amount to convert to fiat currency (mutually exclusive with amount_debit)" - required = false - } - } - response { - code(HttpStatusCode.OK) { description = "Cashout conversion result"; body<ConversionResponse>() } - } - }) { - val params = RateParams.extract(call.request.queryParameters) - - params.debit?.let { cfg.checkRegionalCurrency(it) } - params.credit?.let { cfg.checkFiatCurrency(it) } - - if (params.debit != null) { - call.convert(params.debit, ConversionDAO::defaultToCashout) { - ConversionResponse(params.debit, it) - } - } else { - call.convert(params.credit!!, ConversionDAO::defaultFromCashout) { - ConversionResponse(it, params.credit) - } - } - } - get("/conversion-info/cashin-rate", { - operationId = "getDefaultCashinRate" - description = "Calculate cashin conversion rate" - tags = listOf("Conversion") - request { - queryParameter<String>("amount_debit") { - description = "Amount to convert from fiat currency (mutually exclusive with amount_credit)" - required = false - } - queryParameter<String>("amount_credit") { - description = "Amount to convert to regional currency (mutually exclusive with amount_debit)" - required = false - } - } - response { - code(HttpStatusCode.OK) { description = "Cashin conversion result"; body<ConversionResponse>() } - } - }) { - val params = RateParams.extract(call.request.queryParameters) - - params.debit?.let { cfg.checkFiatCurrency(it) } - params.credit?.let { cfg.checkRegionalCurrency(it) } - - if (params.debit != null) { - call.convert(params.debit, ConversionDAO::defaultToCashin) { - ConversionResponse(params.debit, it) - } - } else { - call.convert(params.credit!!, ConversionDAO::defaultFromCashin) { - ConversionResponse(it, params.credit) - } - } - } - authAdmin(db, cfg.pwCrypto, TokenLogicalScope.readonly, cfg.basicAuthCompat) { - get("/conversion-rate-classes/{CLASS_ID}/conversion-info/rate", { - operationId = "getClassConversionRate" - description = "Get conversion rate for a rate class" - tags = listOf("Conversion") - protected = true - securitySchemeNames("bearerAuth", "basicAuth") - request { - pathParameter<Long>("CLASS_ID") { description = "Conversion rate class ID" } - } - response { - code(HttpStatusCode.OK) { description = "Class conversion rate"; body<ConversionRate>() } - } - }) { - val id = call.longPath("CLASS_ID") - val rate = db.conversion.getClassRate(id) - call.respond(rate) - } - get("/conversion-rate-classes/{CLASS_ID}/conversion-info/cashout-rate", { - operationId = "getClassCashoutRate" - description = "Calculate cashout conversion rate for a rate class" - tags = listOf("Conversion") - protected = true - securitySchemeNames("bearerAuth", "basicAuth") - request { - pathParameter<Long>("CLASS_ID") { description = "Conversion rate class ID" } - queryParameter<String>("amount_debit") { - description = "Amount to convert from regional currency (mutually exclusive with amount_credit)" - required = false - } - queryParameter<String>("amount_credit") { - description = "Amount to convert to fiat currency (mutually exclusive with amount_debit)" - required = false - } - } - response { - code(HttpStatusCode.OK) { description = "Class cashout conversion result"; body<ConversionResponse>() } - } - }) { - val id = call.longPath("CLASS_ID") - val params = RateParams.extract(call.request.queryParameters) - - params.debit?.let { cfg.checkRegionalCurrency(it) } - params.credit?.let { cfg.checkFiatCurrency(it) } - - if (params.debit != null) { - call.convert(params.debit, { classToCashout(id, it) }) { - ConversionResponse(params.debit, it) - } - } else { - call.convert(params.credit!!, { classFromCashout(id, it) }) { - ConversionResponse(it, params.credit) - } - } - } - get("/conversion-rate-classes/{CLASS_ID}/conversion-info/cashin-rate", { - operationId = "getClassCashinRate" - description = "Calculate cashin conversion rate for a rate class" - tags = listOf("Conversion") - protected = true - securitySchemeNames("bearerAuth", "basicAuth") - request { - pathParameter<Long>("CLASS_ID") { description = "Conversion rate class ID" } - queryParameter<String>("amount_debit") { - description = "Amount to convert from fiat currency (mutually exclusive with amount_credit)" - required = false - } - queryParameter<String>("amount_credit") { - description = "Amount to convert to regional currency (mutually exclusive with amount_debit)" - required = false - } - } - response { - code(HttpStatusCode.OK) { description = "Class cashin conversion result"; body<ConversionResponse>() } - } - }) { - val id = call.longPath("CLASS_ID") - val params = RateParams.extract(call.request.queryParameters) - - params.debit?.let { cfg.checkFiatCurrency(it) } - params.credit?.let { cfg.checkRegionalCurrency(it) } - - if (params.debit != null) { - call.convert(params.debit, { classToCashin(id, it) }) { - ConversionResponse(params.debit, it) - } - } else { - call.convert(params.credit!!, { classFromCashin(id, it) }) { - ConversionResponse(it, params.credit) - } - } - } - } - get("/accounts/{USERNAME}/conversion-info/cashin-rate", { - operationId = "getAccountCashinRate" - description = "Calculate cashin conversion rate for an account" - tags = listOf("Conversion") - request { - pathParameter<String>("USERNAME") { description = "Account username" } - queryParameter<String>("amount_debit") { - description = "Amount to convert from fiat currency (mutually exclusive with amount_credit)" - required = false - } - queryParameter<String>("amount_credit") { - description = "Amount to convert to regional currency (mutually exclusive with amount_debit)" - required = false - } - } - response { - code(HttpStatusCode.OK) { description = "Account cashin conversion result"; body<ConversionResponse>() } - } - }) { - val params = RateParams.extract(call.request.queryParameters) - - params.debit?.let { cfg.checkFiatCurrency(it) } - params.credit?.let { cfg.checkRegionalCurrency(it) } - - if (params.debit != null) { - call.convert(params.debit, { userToCashin(call.pathUsername, it) }) { - ConversionResponse(params.debit, it) - } - } else { - call.convert(params.credit!!, { userFromCashin(call.pathUsername, it) }) { - ConversionResponse(it, params.credit) - } - } - } - optAuth(db, cfg.pwCrypto, TokenLogicalScope.readonly, cfg.basicAuthCompat, allowAdmin = true) { - get("/accounts/{USERNAME}/conversion-info/rate", { - operationId = "getAccountConversionRate" - description = "Get conversion rate for an account" - tags = listOf("Conversion") - protected = true - securitySchemeNames("bearerAuth", "basicAuth") - request { - pathParameter<String>("USERNAME") { description = "Account username" } - } - response { - code(HttpStatusCode.OK) { description = "Account conversion rate"; body<ConversionRate>() } - } - }) { - val (isExchange, rate) = db.conversion.getUserRate(call.pathUsername) - if (!isExchange && !call.isAuthenticated) { - throw forbidden("Non exchange account rates are private") - } - call.respond(rate) - } - } - auth(db, cfg.pwCrypto, TokenLogicalScope.readonly, cfg.basicAuthCompat, allowAdmin = true) { - get("/accounts/{USERNAME}/conversion-info/cashout-rate", { - operationId = "getAccountCashoutRate" - description = "Calculate cashout conversion rate for an account" - tags = listOf("Conversion") - protected = true - securitySchemeNames("bearerAuth", "basicAuth") - request { - pathParameter<String>("USERNAME") { description = "Account username" } - queryParameter<String>("amount_debit") { - description = "Amount to convert from regional currency (mutually exclusive with amount_credit)" - required = false - } - queryParameter<String>("amount_credit") { - description = "Amount to convert to fiat currency (mutually exclusive with amount_debit)" - required = false - } - } - response { - code(HttpStatusCode.OK) { description = "Account cashout conversion result"; body<ConversionResponse>() } - } - }) { - val params = RateParams.extract(call.request.queryParameters) - - params.debit?.let { cfg.checkRegionalCurrency(it) } - params.credit?.let { cfg.checkFiatCurrency(it) } - - if (params.debit != null) { - call.convert(params.debit, { userToCashout(call.pathUsername, it) }) { - ConversionResponse(params.debit, it) - } - } else { - call.convert(params.credit!!, { userFromCashout(call.pathUsername, it) }) { - ConversionResponse(it, params.credit) - } - } - } - } -} -\ No newline at end of file diff --git a/libeufin-bank/src/main/kotlin/tech/libeufin/bank/api/CoreBankApi.kt b/libeufin-bank/src/main/kotlin/tech/libeufin/bank/api/CoreBankApi.kt @@ -1,1451 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2023, 2024, 2025, 2026 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.bank.api - -import io.ktor.http.* -import io.ktor.server.application.* -import io.ktor.server.request.* -import io.ktor.server.response.* -import io.ktor.server.routing.* -import io.github.smiley4.ktoropenapi.* -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.future.await -import kotlinx.coroutines.withContext -import org.slf4j.Logger -import org.slf4j.LoggerFactory -import tech.libeufin.bank.* -import tech.libeufin.bank.auth.* -import tech.libeufin.bank.db.* -import tech.libeufin.bank.db.AccountDAO.* -import tech.libeufin.bank.db.CashoutDAO.CashoutCreationResult -import tech.libeufin.bank.db.TanDAO.* -import tech.libeufin.bank.db.TokenDAO.TokenCreationResult -import tech.libeufin.bank.db.TransactionDAO.BankTransactionResult -import tech.libeufin.bank.db.WithdrawalDAO.* -import tech.libeufin.bank.db.ConversionDAO.* -import tech.libeufin.common.* -import tech.libeufin.common.crypto.* -import java.time.Duration -import java.time.Instant -import java.time.temporal.ChronoUnit -import java.util.* - -private val logger: Logger = LoggerFactory.getLogger("libeufin-bank-api") - -fun Routing.coreBankApi(db: Database, cfg: BankConfig) { - get("/config", { - operationId = "getConfig" - description = "Get bank configuration" - tags = listOf("Core Bank") - response { - code(HttpStatusCode.OK) { description = "Bank configuration"; body<Config>() } - } - }) { - call.respond( - Config( - bank_name = cfg.name, - base_url = cfg.baseUrl, - currency = cfg.regionalCurrency, - currency_specification = cfg.regionalCurrencySpec, - allow_conversion = cfg.allowConversion, - allow_registrations = cfg.allowRegistration, - allow_deletions = cfg.allowAccountDeletion, - default_debit_threshold = cfg.defaultDebtLimit, - supported_tan_channels = cfg.tanChannels.keys, - allow_edit_name = cfg.allowEditName, - allow_edit_cashout_payto_uri = cfg.allowEditCashout, - wire_type = cfg.wireMethod, - wire_transfer_fees = cfg.wireTransferFees, - min_wire_transfer_amount = cfg.minAmount, - max_wire_transfer_amount = cfg.maxAmount - ) - ) - } - authAdmin(db, cfg.pwCrypto, TokenLogicalScope.readonly, cfg.basicAuthCompat) { - get("/monitor", { - operationId = "getMonitor" - description = "Get bank monitoring data (admin only)" - tags = listOf("Core Bank") - protected = true - securitySchemeNames("bearerAuth", "basicAuth") - request { - queryParameter<String>("timeframe") { - description = "Timeframe for monitoring data (hour, day, month, year). Default: hour" - required = false - } - queryParameter<Int>("which") { - description = "Deprecated. Which period to query within the timeframe" - required = false - deprecated = true - } - queryParameter<Long>("date_s") { - description = "Timestamp in seconds to specify the period" - required = false - } - } - response { - code(HttpStatusCode.OK) { description = "Monitoring data"; body<MonitorResponse>() } - } - }) { - val params = MonitorParams.extract(call.request.queryParameters) - call.respond(db.monitor(params)) - } - } - coreBankTokenApi(db, cfg) - coreBankAccountsApi(db, cfg) - coreBankTransactionsApi(db, cfg) - coreBankWithdrawalApi(db, cfg) - coreBankCashoutApi(db, cfg) - coreBankTanApi(db, cfg) - coreBankConversionApi(db, cfg) -} - -private fun Routing.coreBankTokenApi(db: Database, cfg: BankConfig) { - val TOKEN_DEFAULT_DURATION: Duration = Duration.ofDays(1L) - val TOKEN_REFRESH_OVERLAP: Duration = Duration.ofMinutes(5L) - auth(db, cfg.pwCrypto, TokenLogicalScope.refreshable, cfg.basicAuthCompat, allowPw = true) { - post("/accounts/{USERNAME}/token", { - operationId = "createToken" - description = "Create or refresh an authentication token" - tags = listOf("Core Bank - Tokens") - protected = true - securitySchemeNames("bearerAuth", "basicAuth") - request { - pathParameter<String>("USERNAME") { description = "Account username" } - body<TokenRequest>() - } - response { - code(HttpStatusCode.OK) { description = "Token created"; body<TokenSuccessResponse>() } - } - }) { - val (req, challenge) = call.receiveChallenge<TokenRequest>(db, Operation.create_token) - - val existingToken = call.authToken - if (existingToken != null) { - // This block checks permissions ONLY IF the call was authenticated with a token - val refreshingToken = db.token.access(existingToken, Instant.now()) ?: throw internalServerError( - "Token used to auth not found in the database!" - ) - if (!validScope(req.scope.logical(), refreshingToken.scope)) - throw forbidden( - "Impossible to refresh a token with a larger scope", - TalerErrorCode.GENERIC_TOKEN_PERMISSION_INSUFFICIENT - ) - } - val token = Base32Crockford32B.secureRand() - val tokenDuration: Duration = req.duration?.duration ?: TOKEN_DEFAULT_DURATION - - val creationTime = Instant.now() - val expirationTimestamp = - if (tokenDuration == ChronoUnit.FOREVER.duration) { - Instant.MAX - } else { - try { - creationTime.plus(tokenDuration) - } catch (e: Exception) { - throw badRequest("Bad token duration: ${e.message}") - } - } - val creationResult = if (existingToken == null) { - db.token.create( - username = call.pathUsername, - content = token.raw, - creationTime = creationTime, - expirationTime = expirationTimestamp, - scope = req.scope, - isRefreshable = req.refreshable, - description = req.description, - is2fa = challenge != null - ) - } else { - val refreshed = db.token.refresh( - username = call.pathUsername, - source = existingToken, - content = token.raw, - creationTime = creationTime, - expirationTime = expirationTimestamp, - sourceExpirationCap = creationTime.plus(TOKEN_REFRESH_OVERLAP), - scope = req.scope, - isRefreshable = req.refreshable, - description = req.description - ) - if (!refreshed) throw internalServerError( - "Token used to authenticate refresh disappeared" - ) - TokenCreationResult.Success - } - when (creationResult) { - TokenCreationResult.TanRequired -> call.respondMfa(db, Operation.create_token) - TokenCreationResult.Success -> call.respond( - TokenSuccessResponse( - access_token = "$TOKEN_PREFIX$token", - expiration = TalerTimestamp(expirationTimestamp) - ) - ) - } - } - } - auth(db, cfg.pwCrypto, TokenLogicalScope.readwrite, cfg.basicAuthCompat, allowAdmin = true) { - delete("/accounts/{USERNAME}/tokens/{TOKEN_ID}", { - operationId = "deleteTokenById" - description = "Delete a specific authentication token by ID" - tags = listOf("Core Bank - Tokens") - protected = true - securitySchemeNames("bearerAuth", "basicAuth") - request { - pathParameter<String>("USERNAME") { description = "Account username" } - pathParameter<Long>("TOKEN_ID") { description = "Token identifier" } - } - response { - code(HttpStatusCode.NoContent) { description = "Token deleted" } - code(HttpStatusCode.NotFound) { description = "Token not found" } - } - }) { - val id = call.longPath("TOKEN_ID") - if (db.token.deleteById(id, call.pathUsername)) { - call.respond(HttpStatusCode.NoContent) - } else { - throw notFound( - "Token '$id' not found", - TalerErrorCode.BANK_TRANSACTION_NOT_FOUND - ) - } - } - } - auth(db, cfg.pwCrypto, TokenLogicalScope.readonly, cfg.basicAuthCompat) { - delete("/accounts/{USERNAME}/token", { - operationId = "deleteToken" - description = "Delete the current authentication token" - tags = listOf("Core Bank - Tokens") - protected = true - securitySchemeNames("bearerAuth", "basicAuth") - request { - pathParameter<String>("USERNAME") { description = "Account username" } - } - response { - code(HttpStatusCode.NoContent) { description = "Token deleted" } - } - }) { - val token = call.authToken ?: throw badRequest("Basic auth not supported here.") - db.token.delete(token) - call.respond(HttpStatusCode.NoContent) - } - } - auth(db, cfg.pwCrypto, TokenLogicalScope.readonly, cfg.basicAuthCompat, allowAdmin = true) { - get("/accounts/{USERNAME}/tokens", { - operationId = "listTokens" - description = "List authentication tokens for an account" - tags = listOf("Core Bank - Tokens") - protected = true - securitySchemeNames("bearerAuth", "basicAuth") - request { - pathParameter<String>("USERNAME") { description = "Account username" } - queryParameter<Int>("limit") { - description = "Maximum number of results to return (default: -20, negative means descending)" - required = false - } - queryParameter<Long>("offset") { - description = "Row ID offset for pagination" - required = false - } - } - response { - code(HttpStatusCode.OK) { description = "List of tokens"; body<TokenInfos>() } - code(HttpStatusCode.NoContent) { description = "No tokens found" } - } - }) { - val params = PageParams.extract(call.request.queryParameters) - val tokens = db.token.page(params, call.pathUsername, Instant.now()) - if (tokens.isEmpty()) { - call.respond(HttpStatusCode.NoContent) - } else { - call.respond(TokenInfos(tokens)) - } - } - } -} - -suspend fun createAccount( - db: Database, - cfg: BankConfig, - req: RegisterAccountRequest, - isAdmin: Boolean -): AccountCreationResult { - // Prohibit reserved usernames: - if (RESERVED_ACCOUNTS.contains(req.username)) - throw conflict( - "Username '${req.username}' is reserved", - TalerErrorCode.BANK_RESERVED_USERNAME_CONFLICT - ) - - // Check tan channels has the corresponding info - val channels = req.channels - - if (!isAdmin) { - if (req.debit_threshold != null) - throw conflict( - "only admin account can choose the debit limit", - TalerErrorCode.BANK_NON_ADMIN_PATCH_DEBT_LIMIT - ) - - if (req.conversion_rate_class_id != null) - throw conflict( - "only admin account can choose the conversion rate class", - TalerErrorCode.BANK_NON_ADMIN_SET_CONVERSION_RATE_CLASS - ) - - if (channels.isNotEmpty()) - throw conflict( - "only admin account can enable 2fa on creation", - TalerErrorCode.BANK_NON_ADMIN_SET_TAN_CHANNEL - ) - } - - for (channel in channels) { - if (cfg.tanChannels[channel] == null) { - throw unsupportedTanChannel(channel) - } - val info = when (channel) { - TanChannel.sms -> req.contact_data?.phone?.get() - TanChannel.email -> req.contact_data?.email?.get() - } - if (info == null) { - throw conflict( - "missing info for tan channel $channel", - TalerErrorCode.BANK_MISSING_TAN_INFO - ) - } - } - - if (req.username == "exchange" && !req.is_taler_exchange) - throw conflict( - "'exchange' account must be a taler exchange account", - TalerErrorCode.END - ) - - val password = req.password.checkPw(cfg.pwdCheckQuality) - - suspend fun doDb(internalPayto: Payto) = db.account.create( - username = req.username, - name = req.name, - email = req.contact_data?.email?.get(), - phone = req.contact_data?.phone?.get(), - cashoutPayto = req.cashout_payto_uri, - password = password.pw, - internalPayto = internalPayto, - isPublic = req.is_public, - isTalerExchange = req.is_taler_exchange, - maxDebt = req.debit_threshold ?: cfg.defaultDebtLimit, - bonus = if (!req.is_taler_exchange) cfg.registrationBonus - else TalerAmount(0, 0, cfg.regionalCurrency), - tanChannels = req.channels, - checkPaytoIdempotent = req.payto_uri != null, - pwCrypto = cfg.pwCrypto, - conversionRateClassId = req.conversion_rate_class_id - ) - - when (cfg.wireMethod) { - WireMethod.IBAN -> { - req.payto_uri?.expectIban() - var retry = if (req.payto_uri == null) IBAN_ALLOCATION_RETRY_COUNTER else 0 - - while (true) { - val internalPayto = req.payto_uri ?: IbanPayto.rand() as Payto - val res = doDb(internalPayto) - // Retry with new IBAN - if (res == AccountCreationResult.PayToReuse && retry > 0) { - retry-- - continue - } - return res - } - } - WireMethod.X_TALER_BANK -> { - if (req.payto_uri != null) { - val payto = req.payto_uri.expectXTalerBank() - if (payto.username != req.username) - throw badRequest("Expected a payto uri for '${req.username}' got one for '${payto.username}'") - } - - val internalPayto = XTalerBankPayto.forUsername(req.username) - return doDb(internalPayto) - } - } -} - -suspend fun patchAccount( - db: Database, - cfg: BankConfig, - req: AccountReconfiguration, - username: String, - isAdmin: Boolean, - is2fa: Boolean -): AccountPatchResult { - req.debit_threshold?.run { cfg.checkRegionalCurrency(this) } - - if (username == "admin" && req.is_public == true) - throw conflict( - "'admin' account cannot be public", - TalerErrorCode.END - ) - - if (username == "exchange" && req.is_taler_exchange == false) - throw conflict( - "'exchange' account must be a taler exchange account", - TalerErrorCode.END - ) - - val channels = req.channels.get() - - if (channels != null) { - for (channel in channels) { - if (cfg.tanChannels[channel] == null) { - throw unsupportedTanChannel(channel) - } - } - } - - return db.account.reconfig( - username = username, - req = req, - isAdmin = isAdmin, - is2fa = is2fa, - allowEditName = cfg.allowEditName, - allowEditCashout = cfg.allowEditCashout - ) -} - -private fun Routing.coreBankAccountsApi(db: Database, cfg: BankConfig) { - authAdmin(db, cfg.pwCrypto, TokenLogicalScope.readwrite, cfg.basicAuthCompat, !cfg.allowRegistration) { - post("/accounts", { - operationId = "createAccount" - description = "Create a new bank account" - tags = listOf("Core Bank - Accounts") - protected = true - securitySchemeNames("bearerAuth", "basicAuth") - request { - body<RegisterAccountRequest>() - } - response { - code(HttpStatusCode.OK) { description = "Account created"; body<RegisterAccountResponse>() } - code(HttpStatusCode.Conflict) { description = "Account creation conflict" } - } - }) { - val req = call.receive<RegisterAccountRequest>() - when (val result = createAccount(db, cfg, req, call.isAdmin)) { - AccountCreationResult.BonusBalanceInsufficient -> throw conflict( - "Insufficient admin funds to grant bonus", - TalerErrorCode.BANK_UNALLOWED_DEBIT - ) - AccountCreationResult.UsernameReuse -> throw conflict( - "Account username reuse '${req.username}'", - TalerErrorCode.BANK_REGISTER_USERNAME_REUSE - ) - AccountCreationResult.PayToReuse -> throw conflict( - "Bank internalPayToUri reuse", - TalerErrorCode.BANK_REGISTER_PAYTO_URI_REUSE - ) - AccountCreationResult.UnknownConversionClass -> - throw unknownConversionClass(req.conversion_rate_class_id) - is AccountCreationResult.Success -> call.respond(RegisterAccountResponse(result.payto)) - } - } - } - auth( - db, - cfg.pwCrypto, - TokenLogicalScope.readwrite, - cfg.basicAuthCompat, - allowAdmin = true, - requireAdmin = !cfg.allowAccountDeletion - ) { - delete("/accounts/{USERNAME}", { - operationId = "deleteAccount" - description = "Delete a bank account" - tags = listOf("Core Bank - Accounts") - protected = true - securitySchemeNames("bearerAuth", "basicAuth") - request { - pathParameter<String>("USERNAME") { description = "Account username" } - } - response { - code(HttpStatusCode.NoContent) { description = "Account deleted" } - code(HttpStatusCode.NotFound) { description = "Account not found" } - code(HttpStatusCode.Conflict) { description = "Account balance not zero or reserved" } - } - }) { - val (_, challenge) = call.receiveChallenge(db, Operation.account_delete, Unit) - - // Not deleting reserved names. - if (RESERVED_ACCOUNTS.contains(call.pathUsername)) - throw conflict( - "Cannot delete reserved accounts", - TalerErrorCode.BANK_RESERVED_USERNAME_CONFLICT - ) - if (call.pathUsername == "exchange" && cfg.allowConversion) - throw conflict( - "Cannot delete 'exchange' accounts when conversion is enabled", - TalerErrorCode.BANK_RESERVED_USERNAME_CONFLICT - ) - - when (db.account.delete(call.pathUsername, call.isAdmin || challenge != null)) { - AccountDeletionResult.UnknownAccount -> throw unknownAccount(call.pathUsername) - AccountDeletionResult.BalanceNotZero -> throw conflict( - "Account balance is not zero.", - TalerErrorCode.BANK_ACCOUNT_BALANCE_NOT_ZERO - ) - AccountDeletionResult.TanRequired -> call.respondMfa(db, Operation.account_delete) - AccountDeletionResult.Success -> call.respond(HttpStatusCode.NoContent) - } - } - } - auth(db, cfg.pwCrypto, TokenLogicalScope.readwrite, cfg.basicAuthCompat, allowAdmin = true) { - patch("/accounts/{USERNAME}", { - operationId = "reconfigureAccount" - description = "Reconfigure a bank account" - tags = listOf("Core Bank - Accounts") - protected = true - securitySchemeNames("bearerAuth", "basicAuth") - request { - pathParameter<String>("USERNAME") { description = "Account username" } - body<AccountReconfiguration>() - } - response { - code(HttpStatusCode.NoContent) { description = "Account reconfigured" } - code(HttpStatusCode.NotFound) { description = "Account not found" } - code(HttpStatusCode.Conflict) { description = "Reconfiguration conflict" } - } - }) { - val (req, pendingValidation) = call.receiveChallenge<AccountReconfiguration>(db, Operation.account_reconfig) - - if (pendingValidation != null && pendingValidation.isNotEmpty()) { - return@patch call.respondValidation(db, Operation.account_reconfig, pendingValidation) - } - - val res = patchAccount(db, cfg, req, call.pathUsername, call.isAdmin, pendingValidation != null) - when (res) { - AccountPatchResult.Success -> call.respond(HttpStatusCode.NoContent) - is AccountPatchResult.Challenges -> { - if (res.validations.isNotEmpty()) { - call.respondValidation(db, Operation.account_reconfig, res.validations) - } else { - call.respondMfa(db, Operation.account_reconfig) - } - } - AccountPatchResult.UnknownAccount -> throw unknownAccount(call.pathUsername) - AccountPatchResult.NonAdminName -> throw conflict( - "non-admin user cannot change their legal name", - TalerErrorCode.BANK_NON_ADMIN_PATCH_LEGAL_NAME - ) - AccountPatchResult.NonAdminCashout -> throw conflict( - "non-admin user cannot change their cashout account", - TalerErrorCode.BANK_NON_ADMIN_PATCH_CASHOUT - ) - AccountPatchResult.NonAdminDebtLimit -> throw conflict( - "non-admin user cannot change their debt limit", - TalerErrorCode.BANK_NON_ADMIN_PATCH_DEBT_LIMIT - ) - AccountPatchResult.NonAdminConversionRateClass -> throw conflict( - "non-admin user cannot change their conversion rate class", - TalerErrorCode.BANK_NON_ADMIN_SET_CONVERSION_RATE_CLASS - ) - AccountPatchResult.UnknownConversionClass -> - throw unknownConversionClass(req.conversion_rate_class_id.get()) - AccountPatchResult.MissingTanInfo -> throw conflict( - "missing info for tan channel ${req.tan_channel.get()}", - TalerErrorCode.BANK_MISSING_TAN_INFO - ) - } - } - patch("/accounts/{USERNAME}/auth", { - operationId = "changePassword" - description = "Change account password" - tags = listOf("Core Bank - Accounts") - protected = true - securitySchemeNames("bearerAuth", "basicAuth") - request { - pathParameter<String>("USERNAME") { description = "Account username" } - body<AccountPasswordChange>() - } - response { - code(HttpStatusCode.NoContent) { description = "Password changed" } - code(HttpStatusCode.NotFound) { description = "Account not found" } - code(HttpStatusCode.Conflict) { description = "Old password mismatch" } - } - }) { - val (req, challenge) = call.receiveChallenge<AccountPasswordChange>(db, Operation.account_auth_reconfig) - - if (!call.isAdmin && req.old_password == null) { - throw conflict( - "non-admin user cannot change password without providing old password", - TalerErrorCode.BANK_NON_ADMIN_PATCH_MISSING_OLD_PASSWORD - ) - } - val newPassword = req.new_password.checkPw(cfg.pwdCheckQuality) - - when (db.account.reconfigPassword(call.pathUsername, newPassword, req.old_password, call.isAdmin || challenge != null, cfg.pwCrypto)) { - AccountPatchAuthResult.Success -> call.respond(HttpStatusCode.NoContent) - AccountPatchAuthResult.TanRequired -> call.respondMfa(db, Operation.account_auth_reconfig) - AccountPatchAuthResult.UnknownAccount -> throw unknownAccount(call.pathUsername) - AccountPatchAuthResult.OldPasswordMismatch -> throw conflict( - "old password does not match", - TalerErrorCode.BANK_PATCH_BAD_OLD_PASSWORD - ) - } - } - } - get("/public-accounts", { - operationId = "listPublicAccounts" - description = "List public bank accounts" - tags = listOf("Core Bank - Accounts") - request { - queryParameter<Int>("limit") { - description = "Maximum number of results to return (default: -20, negative means descending)" - required = false - } - queryParameter<Long>("offset") { - description = "Row ID offset for pagination" - required = false - } - queryParameter<String>("filter_name") { - description = "Filter accounts by username substring" - required = false - } - queryParameter<Long>("conversion_rate_class_id") { - description = "Filter accounts by conversion rate class ID" - required = false - } - } - response { - code(HttpStatusCode.OK) { description = "List of public accounts"; body<PublicAccountsResponse>() } - code(HttpStatusCode.NoContent) { description = "No public accounts found" } - } - }) { - val params = AccountParams.extract(call.request.queryParameters) - val publicAccounts = db.account.pagePublic(params) - if (publicAccounts.isEmpty()) { - call.respond(HttpStatusCode.NoContent) - } else { - call.respond(PublicAccountsResponse(publicAccounts)) - } - } - authAdmin(db, cfg.pwCrypto, TokenLogicalScope.readonly, cfg.basicAuthCompat) { - get("/accounts", { - operationId = "listAccounts" - description = "List all bank accounts (admin only)" - tags = listOf("Core Bank - Accounts") - protected = true - securitySchemeNames("bearerAuth", "basicAuth") - request { - queryParameter<Int>("limit") { - description = "Maximum number of results to return (default: -20, negative means descending)" - required = false - } - queryParameter<Long>("offset") { - description = "Row ID offset for pagination" - required = false - } - queryParameter<String>("filter_name") { - description = "Filter accounts by username substring" - required = false - } - queryParameter<Long>("conversion_rate_class_id") { - description = "Filter accounts by conversion rate class ID" - required = false - } - } - response { - code(HttpStatusCode.OK) { description = "List of accounts"; body<ListBankAccountsResponse>() } - code(HttpStatusCode.NoContent) { description = "No accounts found" } - } - }) { - val params = AccountParams.extract(call.request.queryParameters) - val accounts = db.account.pageAdmin(params) - if (accounts.isEmpty()) { - call.respond(HttpStatusCode.NoContent) - } else { - call.respond(ListBankAccountsResponse(accounts)) - } - } - } - auth(db, cfg.pwCrypto, TokenLogicalScope.readonly, cfg.basicAuthCompat, allowAdmin = true) { - get("/accounts/{USERNAME}", { - operationId = "getAccount" - description = "Get account details" - tags = listOf("Core Bank - Accounts") - protected = true - securitySchemeNames("bearerAuth", "basicAuth") - request { - pathParameter<String>("USERNAME") { description = "Account username" } - } - response { - code(HttpStatusCode.OK) { description = "Account details"; body<AccountData>() } - code(HttpStatusCode.NotFound) { description = "Account not found" } - } - }) { - val account = db.account.get(call.pathUsername) ?: throw unknownAccount(call.pathUsername) - call.respond(account) - } - } -} - -private fun Routing.coreBankTransactionsApi(db: Database, cfg: BankConfig) { - optAuth(db, cfg.pwCrypto, TokenLogicalScope.readonly, cfg.basicAuthCompat, allowAdmin = true) { - get("/accounts/{USERNAME}/transactions", { - operationId = "getTransactions" - description = "Get transaction history for an account" - tags = listOf("Core Bank - Transactions") - protected = false - securitySchemeNames("bearerAuth", "basicAuth") - request { - pathParameter<String>("USERNAME") { description = "Account username" } - queryParameter<Int>("limit") { - description = "Maximum number of results to return (default: -20, negative means descending)" - required = false - } - queryParameter<Long>("offset") { - description = "Row ID offset for pagination" - required = false - } - queryParameter<Long>("timeout_ms") { - description = "Timeout for long polling in milliseconds (default: 0, no long polling)" - required = false - } - } - response { - code(HttpStatusCode.OK) { description = "Transaction history"; body<BankAccountTransactionsResponse>() } - code(HttpStatusCode.NoContent) { description = "No transactions found" } - } - }) { - val params = HistoryParams.extract(call.request.queryParameters) - call.response.header(HttpHeaders.CacheControl, "no-store") - val bankAccount = if (call.isAuthenticated) { - call.bankInfo(db) - } else { - db.account.publicBankInfo(call.pathUsername) - ?: throw unknownAccount(call.pathUsername) - } - - val history: List<BankAccountTransactionInfo> = - db.transaction.pollHistory(params, bankAccount.bankAccountId) - if (history.isEmpty()) { - call.respond(HttpStatusCode.NoContent) - } else { - call.respond(BankAccountTransactionsResponse(history)) - } - } - } - auth(db, cfg.pwCrypto, TokenLogicalScope.readonly, cfg.basicAuthCompat, allowAdmin = true) { - get("/accounts/{USERNAME}/transactions/{T_ID}", { - operationId = "getTransaction" - description = "Get a specific transaction by ID" - tags = listOf("Core Bank - Transactions") - protected = true - securitySchemeNames("bearerAuth", "basicAuth") - request { - pathParameter<String>("USERNAME") { description = "Account username" } - pathParameter<Long>("T_ID") { description = "Transaction identifier" } - } - response { - code(HttpStatusCode.OK) { description = "Transaction details"; body<BankAccountTransactionInfo>() } - code(HttpStatusCode.NotFound) { description = "Transaction not found" } - } - }) { - val tId = call.longPath("T_ID") - val tx = db.transaction.get(tId, call.pathUsername) ?: throw notFound( - "Bank transaction '$tId' not found", - TalerErrorCode.BANK_TRANSACTION_NOT_FOUND - ) - call.respond(tx) - } - } - auth(db, cfg.pwCrypto, TokenLogicalScope.readwrite, cfg.basicAuthCompat) { - post("/accounts/{USERNAME}/transactions", { - operationId = "createTransaction" - description = "Create a new bank transaction" - tags = listOf("Core Bank - Transactions") - protected = true - securitySchemeNames("bearerAuth", "basicAuth") - request { - pathParameter<String>("USERNAME") { description = "Account username" } - body<TransactionCreateRequest>() - } - response { - code(HttpStatusCode.OK) { description = "Transaction created"; body<TransactionCreateResponse>() } - code(HttpStatusCode.Conflict) { description = "Transaction conflict" } - } - }) { - val (req, challenge) = call.receiveChallenge<TransactionCreateRequest>(db, Operation.bank_transaction) - - val subject = req.payto_uri.message ?: throw badRequest("Wire transfer lacks subject") - val amount = req.payto_uri.amount ?: req.amount ?: throw badRequest("Wire transfer lacks amount") - - cfg.checkRegionalCurrency(amount) - - val res = db.transaction.create( - creditAccountPayto = req.payto_uri, - debitAccountUsername = call.pathUsername, - subject = subject, - amount = amount, - timestamp = Instant.now(), - requestUid = req.request_uid, - is2fa = challenge != null, - wireTransferFees = cfg.wireTransferFees, - minAmount = cfg.minAmount, - maxAmount = cfg.maxAmount - ) - when (res) { - BankTransactionResult.UnknownDebtor -> throw unknownAccount(call.pathUsername) - BankTransactionResult.TanRequired -> { - call.respondMfa(db, Operation.bank_transaction) - } - BankTransactionResult.BothPartySame -> throw conflict( - "Wire transfer attempted with credit and debit party being the same bank account", - TalerErrorCode.BANK_SAME_ACCOUNT - ) - BankTransactionResult.UnknownCreditor -> throw unknownCreditorAccount(req.payto_uri.canonical) - BankTransactionResult.AdminCreditor -> throw conflict( - "Cannot transfer money to admin account", - TalerErrorCode.BANK_ADMIN_CREDITOR - ) - BankTransactionResult.BalanceInsufficient -> throw conflict( - "Insufficient funds", - TalerErrorCode.BANK_UNALLOWED_DEBIT - ) - BankTransactionResult.BadAmount -> throw conflict( - "Amount either to high or too low", - TalerErrorCode.BANK_UNALLOWED_DEBIT - ) - BankTransactionResult.RequestUidReuse -> throw conflict( - "request_uid used already", - TalerErrorCode.BANK_TRANSFER_REQUEST_UID_REUSED - ) - is BankTransactionResult.Success -> call.respond(TransactionCreateResponse(res.id)) - } - } - } -} - -private fun Routing.coreBankWithdrawalApi(db: Database, cfg: BankConfig) { - auth(db, cfg.pwCrypto, TokenLogicalScope.readwrite, cfg.basicAuthCompat) { - post("/accounts/{USERNAME}/withdrawals", { - operationId = "createWithdrawal" - description = "Create a new withdrawal operation" - tags = listOf("Core Bank - Withdrawals") - protected = true - securitySchemeNames("bearerAuth", "basicAuth") - request { - pathParameter<String>("USERNAME") { description = "Account username" } - body<BankAccountCreateWithdrawalRequest>() - } - response { - code(HttpStatusCode.OK) { description = "Withdrawal created"; body<BankAccountCreateWithdrawalResponse>() } - code(HttpStatusCode.Conflict) { description = "Withdrawal conflict" } - } - }) { - val req = call.receive<BankAccountCreateWithdrawalRequest>() - req.amount?.run(cfg::checkRegionalCurrency) - req.suggested_amount?.run(cfg::checkRegionalCurrency) - val opId = UUID.randomUUID() - when (db.withdrawal.create( - call.pathUsername, - opId, - req.amount, - req.suggested_amount, - req.no_amount_to_wallet, - Instant.now(), - cfg.wireTransferFees, - cfg.minAmount, - cfg.maxAmount - )) { - WithdrawalCreationResult.UnknownAccount -> throw unknownAccount(call.pathUsername) - WithdrawalCreationResult.AccountIsExchange -> throw conflict( - "Exchange account cannot perform withdrawal operation", - TalerErrorCode.BANK_ACCOUNT_IS_EXCHANGE - ) - WithdrawalCreationResult.BalanceInsufficient -> throw conflict( - "Insufficient funds to withdraw with Taler", - TalerErrorCode.BANK_UNALLOWED_DEBIT - ) - WithdrawalCreationResult.BadAmount -> throw conflict( - "Amount either to high or too low", - TalerErrorCode.BANK_UNALLOWED_DEBIT - ) - WithdrawalCreationResult.Success -> { - call.respond( - BankAccountCreateWithdrawalResponse( - withdrawal_id = opId.toString(), - taler_withdraw_uri = cfg.talerWithdrawUri(opId) - ) - ) - } - } - } - post("/accounts/{USERNAME}/withdrawals/{withdrawal_id}/confirm", { - operationId = "confirmWithdrawal" - description = "Confirm a withdrawal operation" - tags = listOf("Core Bank - Withdrawals") - protected = true - securitySchemeNames("bearerAuth", "basicAuth") - request { - pathParameter<String>("USERNAME") { description = "Account username" } - pathParameter<String>("withdrawal_id") { description = "Withdrawal operation ID" } - body<BankAccountConfirmWithdrawalRequest>() - } - response { - code(HttpStatusCode.NoContent) { description = "Withdrawal confirmed" } - code(HttpStatusCode.NotFound) { description = "Withdrawal operation not found" } - code(HttpStatusCode.Conflict) { description = "Confirmation conflict" } - } - }) { - val id = call.uuidPath("withdrawal_id") - val (req, challenge) = call.receiveChallenge<BankAccountConfirmWithdrawalRequest>(db, Operation.withdrawal, BankAccountConfirmWithdrawalRequest()) - req.amount?.run(cfg::checkRegionalCurrency) - when (db.withdrawal.confirm( - call.pathUsername, - id, - Instant.now(), - req.amount, - challenge != null, - cfg.wireTransferFees, - cfg.minAmount, - cfg.maxAmount - )) { - WithdrawalConfirmationResult.UnknownOperation -> throw notFound( - "Withdrawal operation $id not found", - TalerErrorCode.BANK_TRANSACTION_NOT_FOUND - ) - WithdrawalConfirmationResult.AlreadyAborted -> throw conflict( - "Cannot confirm an aborted withdrawal", - TalerErrorCode.BANK_CONFIRM_ABORT_CONFLICT - ) - WithdrawalConfirmationResult.ReservePubReuse -> throw conflict( - "Reserve pub already used", - TalerErrorCode.BANK_DUPLICATE_RESERVE_PUB_SUBJECT - ) - WithdrawalConfirmationResult.NotSelected -> throw conflict( - "Cannot confirm an unselected withdrawal", - TalerErrorCode.BANK_CONFIRM_INCOMPLETE - ) - WithdrawalConfirmationResult.BalanceInsufficient -> throw conflict( - "Insufficient funds", - TalerErrorCode.BANK_UNALLOWED_DEBIT - ) - WithdrawalConfirmationResult.MissingAmount -> throw conflict( - "An amount is required", - TalerErrorCode.BANK_AMOUNT_REQUIRED - ) - WithdrawalConfirmationResult.AmountDiffers -> throw conflict( - "Given amount is different from the current", - TalerErrorCode.BANK_AMOUNT_DIFFERS - ) - WithdrawalConfirmationResult.BadAmount -> throw conflict( - "Amount either to high or too low", - TalerErrorCode.BANK_UNALLOWED_DEBIT - ) - WithdrawalConfirmationResult.TanRequired -> { - call.respondMfa(db, Operation.withdrawal) - } - WithdrawalConfirmationResult.Success -> call.respond(HttpStatusCode.NoContent) - } - } - post("/accounts/{USERNAME}/withdrawals/{withdrawal_id}/abort", { - operationId = "abortWithdrawal" - description = "Abort a withdrawal operation" - tags = listOf("Core Bank - Withdrawals") - protected = true - securitySchemeNames("bearerAuth", "basicAuth") - request { - pathParameter<String>("USERNAME") { description = "Account username" } - pathParameter<String>("withdrawal_id") { description = "Withdrawal operation ID" } - } - response { - code(HttpStatusCode.NoContent) { description = "Withdrawal aborted" } - code(HttpStatusCode.NotFound) { description = "Withdrawal operation not found" } - code(HttpStatusCode.Conflict) { description = "Cannot abort confirmed withdrawal" } - } - }) { - val opId = call.uuidPath("withdrawal_id") - when (db.withdrawal.abort(opId)) { - AbortResult.UnknownOperation -> throw notFound( - "Withdrawal operation $opId not found", - TalerErrorCode.BANK_TRANSACTION_NOT_FOUND - ) - AbortResult.AlreadyConfirmed -> throw conflict( - "Cannot abort confirmed withdrawal", - TalerErrorCode.BANK_ABORT_CONFIRM_CONFLICT - ) - AbortResult.Success -> call.respond(HttpStatusCode.NoContent) - } - } - } - get("/withdrawals/{withdrawal_id}", { - operationId = "getWithdrawalStatus" - description = "Get withdrawal operation status" - tags = listOf("Core Bank - Withdrawals") - request { - pathParameter<String>("withdrawal_id") { description = "Withdrawal operation ID" } - queryParameter<Long>("timeout_ms") { - description = "Timeout for long polling in milliseconds (default: 0, no long polling)" - required = false - } - queryParameter<String>("old_state") { - description = "Previous state for long polling (pending, aborted, selected, confirmed). Default: pending" - required = false - } - } - response { - code(HttpStatusCode.OK) { description = "Withdrawal status"; body<WithdrawalPublicInfo>() } - code(HttpStatusCode.NotFound) { description = "Withdrawal operation not found" } - } - }) { - val uuid = call.uuidPath("withdrawal_id") - val params = StatusParams.extract(call.request.queryParameters) - val op = db.withdrawal.pollInfo(uuid, params) ?: throw notFound( - "Withdrawal operation '$uuid' not found", - TalerErrorCode.BANK_TRANSACTION_NOT_FOUND - ) - call.respond(op) - } -} - -private fun Routing.coreBankCashoutApi(db: Database, cfg: BankConfig) = conditional(cfg.allowConversion) { - auth(db, cfg.pwCrypto, TokenLogicalScope.readwrite, cfg.basicAuthCompat) { - post("/accounts/{USERNAME}/cashouts", { - operationId = "createCashout" - description = "Create a new cashout operation" - tags = listOf("Core Bank - Cashouts") - protected = true - securitySchemeNames("bearerAuth", "basicAuth") - request { - pathParameter<String>("USERNAME") { description = "Account username" } - body<CashoutRequest>() - } - response { - code(HttpStatusCode.OK) { description = "Cashout created"; body<CashoutResponse>() } - code(HttpStatusCode.Conflict) { description = "Cashout conflict" } - } - }) { - val (req, challenge) = call.receiveChallenge<CashoutRequest>(db, Operation.cashout) - - cfg.checkRegionalCurrency(req.amount_debit) - cfg.checkFiatCurrency(req.amount_credit) - - val res = db.cashout.create( - username = call.pathUsername, - requestUid = req.request_uid, - amountDebit = req.amount_debit, - amountCredit = req.amount_credit, - subject = req.subject ?: "", // TODO default subject - timestamp = Instant.now(), - is2fa = challenge != null - ) - when (res) { - CashoutCreationResult.AccountNotFound -> throw unknownAccount(call.pathUsername) - CashoutCreationResult.BadConversion -> throw conflict( - "Wrong currency conversion", - TalerErrorCode.BANK_BAD_CONVERSION - ) - CashoutCreationResult.UnderMin -> throw conflict( - "Amount of currency conversion it less than the minimum allowed", - TalerErrorCode.BANK_CONVERSION_AMOUNT_TO_SMALL - ) - CashoutCreationResult.AccountIsExchange -> throw conflict( - "Exchange account cannot perform cashout operation", - TalerErrorCode.BANK_ACCOUNT_IS_EXCHANGE - ) - CashoutCreationResult.BalanceInsufficient -> throw conflict( - "Insufficient funds to withdraw with Taler", - TalerErrorCode.BANK_UNALLOWED_DEBIT - ) - CashoutCreationResult.RequestUidReuse -> throw conflict( - "request_uid used already", - TalerErrorCode.BANK_TRANSFER_REQUEST_UID_REUSED - ) - CashoutCreationResult.NoCashoutPayto -> throw conflict( - "Missing cashout payto uri", - TalerErrorCode.BANK_CONFIRM_INCOMPLETE - ) - CashoutCreationResult.TanRequired -> { - call.respondMfa(db, Operation.cashout) - } - is CashoutCreationResult.Success -> call.respond(CashoutResponse(res.id)) - } - } - } - auth(db, cfg.pwCrypto, TokenLogicalScope.readonly, cfg.basicAuthCompat, allowAdmin = true) { - get("/accounts/{USERNAME}/cashouts/{CASHOUT_ID}", { - operationId = "getCashout" - description = "Get a specific cashout operation" - tags = listOf("Core Bank - Cashouts") - protected = true - securitySchemeNames("bearerAuth", "basicAuth") - request { - pathParameter<String>("USERNAME") { description = "Account username" } - pathParameter<Long>("CASHOUT_ID") { description = "Cashout operation identifier" } - } - response { - code(HttpStatusCode.OK) { description = "Cashout details"; body<CashoutStatusResponse>() } - code(HttpStatusCode.NotFound) { description = "Cashout operation not found" } - } - }) { - val id = call.longPath("CASHOUT_ID") - val cashout = db.cashout.get(id, call.pathUsername) ?: throw notFound( - "Cashout operation $id not found", - TalerErrorCode.BANK_TRANSACTION_NOT_FOUND - ) - call.respond(cashout) - } - get("/accounts/{USERNAME}/cashouts", { - operationId = "listAccountCashouts" - description = "List cashout operations for an account" - tags = listOf("Core Bank - Cashouts") - protected = true - securitySchemeNames("bearerAuth", "basicAuth") - request { - pathParameter<String>("USERNAME") { description = "Account username" } - queryParameter<Int>("limit") { - description = "Maximum number of results to return (default: -20, negative means descending)" - required = false - } - queryParameter<Long>("offset") { - description = "Row ID offset for pagination" - required = false - } - } - response { - code(HttpStatusCode.OK) { description = "List of cashouts for account"; body<Cashouts>() } - code(HttpStatusCode.NoContent) { description = "No cashout operations found" } - } - }) { - val params = PageParams.extract(call.request.queryParameters) - val cashouts = db.cashout.pageForUser(params, call.pathUsername) - if (cashouts.isEmpty()) { - call.respond(HttpStatusCode.NoContent) - } else { - call.respond(Cashouts(cashouts)) - } - } - } - authAdmin(db, cfg.pwCrypto, TokenLogicalScope.readonly, cfg.basicAuthCompat) { - get("/cashouts", { - operationId = "listAllCashouts" - description = "List all cashout operations (admin only)" - tags = listOf("Core Bank - Cashouts") - protected = true - securitySchemeNames("bearerAuth", "basicAuth") - request { - queryParameter<Int>("limit") { - description = "Maximum number of results to return (default: -20, negative means descending)" - required = false - } - queryParameter<Long>("offset") { - description = "Row ID offset for pagination" - required = false - } - } - response { - code(HttpStatusCode.OK) { description = "List of all cashouts"; body<GlobalCashouts>() } - code(HttpStatusCode.NoContent) { description = "No cashout operations found" } - } - }) { - val params = PageParams.extract(call.request.queryParameters) - val cashouts = db.cashout.pageAll(params) - if (cashouts.isEmpty()) { - call.respond(HttpStatusCode.NoContent) - } else { - call.respond(GlobalCashouts(cashouts)) - } - } - } -} - -private fun Routing.coreBankTanApi(db: Database, cfg: BankConfig) { - post("/accounts/{USERNAME}/challenge/{CHALLENGE_ID}", { - operationId = "sendTanChallenge" - description = "Send a TAN challenge" - tags = listOf("Core Bank - TAN") - request { - pathParameter<String>("USERNAME") { description = "Account username" } - pathParameter<String>("CHALLENGE_ID") { description = "Challenge identifier" } - } - response { - code(HttpStatusCode.OK) { description = "Challenge response"; body<ChallengeRequestResponse>() } - code(HttpStatusCode.NotFound) { description = "Challenge not found or expired" } - code(HttpStatusCode.Gone) { description = "Challenge already solved" } - } - }) { - val uuid = call.uuidPath("CHALLENGE_ID") - val res = db.tan.send( - uuid = uuid, - timestamp = Instant.now(), - maxActive = MAX_ACTIVE_CHALLENGES - ) - when (res) { - TanSendResult.NotFound -> throw notFound( - "Challenge $uuid not found", - TalerErrorCode.BANK_TRANSACTION_NOT_FOUND - ) - TanSendResult.Expired -> throw notFound( - "Challenge $uuid expired", - TalerErrorCode.BANK_TAN_CHALLENGE_EXPIRED - ) - TanSendResult.TooMany -> throw tooManyRequests( - "Too many active challenges", - TalerErrorCode.BANK_TAN_RATE_LIMITED - ) - TanSendResult.Solved -> call.respond(HttpStatusCode.Gone) - is TanSendResult.Send -> { - val (tanScript, tanEnv) = cfg.tanChannels[res.tanChannel] - ?: throw unsupportedTanChannel(res.tanChannel) - val msg = "T-${res.tanCode} is your ${cfg.name} verification code" - val exitValue = withContext(Dispatchers.IO) { - val builder = ProcessBuilder(tanScript.toString(), res.tanInfo) - builder.redirectErrorStream(true) - for ((name, value) in tanEnv) { - builder.environment()[name] = value - } - val process = builder.start() - try { - process.outputWriter().use { it.write(msg) } - process.onExit().await() - } catch (e: Exception) { - process.destroy() - } - val exitValue = process.exitValue() - if (exitValue != 0) { - val out = runCatching { - process.inputStream.use { - res.tanCode.reader().readText() - } - }.getOrDefault("") - if (out.isNotEmpty()) { - logger.error("TAN ${res.tanChannel} - ${tanScript}: $out") - } - } - exitValue - } - if (exitValue != 0) { - throw apiError( - HttpStatusCode.BadGateway, - "Tan channel script failure with exit value $exitValue", - TalerErrorCode.BANK_TAN_CHANNEL_SCRIPT_FAILED - ) - } - val retransmission = Instant.now() + TAN_RETRANSMISSION_PERIOD - db.tan.markSent(uuid, retransmission) - call.respond(ChallengeRequestResponse( - solve_expiration = res.expiration, - earliest_retransmission = TalerTimestamp(retransmission) - )) - } - is TanSendResult.Success -> { - call.respond(ChallengeRequestResponse( - solve_expiration = res.expiration, - earliest_retransmission = res.retransmission - )) - } - } - } - post("/accounts/{USERNAME}/challenge/{CHALLENGE_ID}/confirm", { - operationId = "confirmTanChallenge" - description = "Confirm a TAN challenge" - tags = listOf("Core Bank - TAN") - request { - pathParameter<String>("USERNAME") { description = "Account username" } - pathParameter<String>("CHALLENGE_ID") { description = "Challenge identifier" } - body<ChallengeSolve>() - } - response { - code(HttpStatusCode.NoContent) { description = "Challenge confirmed successfully" } - code(HttpStatusCode.NotFound) { description = "Challenge not found or expired" } - code(HttpStatusCode.Conflict) { description = "Incorrect TAN code" } - } - }) { - val uuid = call.uuidPath("CHALLENGE_ID") - val req = call.receive<ChallengeSolve>() - val code = req.tan.removePrefix("T-") - val res = db.tan.solve( - uuid = uuid, - code = code, - timestamp = Instant.now() - ) - when (res) { - TanSolveResult.NotFound -> throw notFound( - "Challenge $uuid not found", - TalerErrorCode.BANK_CHALLENGE_NOT_FOUND - ) - TanSolveResult.BadCode -> throw conflict( - "Incorrect TAN code", - TalerErrorCode.BANK_TAN_CHALLENGE_FAILED - ) - TanSolveResult.NoRetry -> throw tooManyRequests( - "Too many failed confirmation attempt", - TalerErrorCode.BANK_TAN_RATE_LIMITED - ) - TanSolveResult.Expired -> throw notFound( - "Challenge $uuid expired", - TalerErrorCode.BANK_TAN_CHALLENGE_EXPIRED - ) - is TanSolveResult.Success -> call.respond(HttpStatusCode.NoContent) - } - } -} - -private fun Routing.coreBankConversionApi(db: Database, cfg: BankConfig) = conditional(cfg.allowConversion) { - authAdmin(db, cfg.pwCrypto, TokenLogicalScope.readwrite, cfg.basicAuthCompat) { - post("/conversion-rate-classes", { - operationId = "createConversionRateClass" - description = "Create a conversion rate class" - tags = listOf("Core Bank - Conversion Rate Classes") - protected = true - securitySchemeNames("bearerAuth", "basicAuth") - request { - body<ConversionRateClassInput>() - } - response { - code(HttpStatusCode.OK) { description = "Rate class created"; body<ConversionRateClassResponse>() } - code(HttpStatusCode.Conflict) { description = "Name already in use" } - } - }) { - val req = call.receive<ConversionRateClassInput>() - cfg.checkCurrency(req) - when (val res = db.conversion.createClass(req)) { - is ClassCreateResult.Success -> call.respond(ConversionRateClassResponse(res.id)) - ClassCreateResult.NameReuse -> throw conflict( - "Conversion rate class name '${req.name}' already use", - TalerErrorCode.BANK_NAME_REUSE - ) - } - - } - patch("/conversion-rate-classes/{CLASS_ID}", { - operationId = "updateConversionRateClass" - description = "Update a conversion rate class" - tags = listOf("Core Bank - Conversion Rate Classes") - protected = true - securitySchemeNames("bearerAuth", "basicAuth") - request { - pathParameter<Long>("CLASS_ID") { description = "Conversion rate class ID" } - body<ConversionRateClassInput>() - } - response { - code(HttpStatusCode.NoContent) { description = "Rate class updated" } - code(HttpStatusCode.NotFound) { description = "Rate class not found" } - code(HttpStatusCode.Conflict) { description = "Name already in use" } - } - }) { - val id = call.longPath("CLASS_ID") - val req = call.receive<ConversionRateClassInput>() - cfg.checkCurrency(req) - when (val res = db.conversion.patchClass(id, req)) { - ClassPatchResult.Success -> call.respond(HttpStatusCode.NoContent) - ClassPatchResult.Unknown -> throw notFound( - "Conversion rate class '$id' not found", - TalerErrorCode.BANK_TRANSACTION_NOT_FOUND - ) - ClassPatchResult.NameReuse -> throw conflict( - "Conversion rate class name '${req.name}' already use", - TalerErrorCode.BANK_NAME_REUSE - ) - } - } - delete("/conversion-rate-classes/{CLASS_ID}", { - operationId = "deleteConversionRateClass" - description = "Delete a conversion rate class" - tags = listOf("Core Bank - Conversion Rate Classes") - protected = true - securitySchemeNames("bearerAuth", "basicAuth") - request { - pathParameter<Long>("CLASS_ID") { description = "Conversion rate class ID" } - } - response { - code(HttpStatusCode.NoContent) { description = "Rate class deleted" } - code(HttpStatusCode.NotFound) { description = "Rate class not found" } - } - }) { - val id = call.longPath("CLASS_ID") - if (db.conversion.deleteClass(id)) { - call.respond(HttpStatusCode.NoContent) - } else { - throw notFound( - "Conversion rate class '$id' not found", - TalerErrorCode.BANK_TRANSACTION_NOT_FOUND - ) - } - } - } - authAdmin(db, cfg.pwCrypto, TokenLogicalScope.readonly, cfg.basicAuthCompat) { - get("/conversion-rate-classes/{CLASS_ID}", { - operationId = "getConversionRateClass" - description = "Get a conversion rate class" - tags = listOf("Core Bank - Conversion Rate Classes") - protected = true - securitySchemeNames("bearerAuth", "basicAuth") - request { - pathParameter<Long>("CLASS_ID") { description = "Conversion rate class ID" } - } - response { - code(HttpStatusCode.OK) { description = "Rate class details"; body<ConversionRateClass>() } - code(HttpStatusCode.NotFound) { description = "Rate class not found" } - } - }) { - val id = call.longPath("CLASS_ID") - val cashout = db.conversion.getClass(id) ?: throw notFound( - "Conversion rate class $id not found", - TalerErrorCode.BANK_TRANSACTION_NOT_FOUND - ) - call.respond(cashout) - } - get("/conversion-rate-classes", { - operationId = "listConversionRateClasses" - description = "List all conversion rate classes" - tags = listOf("Core Bank - Conversion Rate Classes") - protected = true - securitySchemeNames("bearerAuth", "basicAuth") - request { - queryParameter<Int>("limit") { - description = "Maximum number of results to return (default: -20, negative means descending)" - required = false - } - queryParameter<Long>("offset") { - description = "Row ID offset for pagination" - required = false - } - queryParameter<String>("filter_name") { - description = "Filter rate classes by name substring" - required = false - } - } - response { - code(HttpStatusCode.OK) { description = "List of rate classes"; body<ConversionRateClasses>() } - code(HttpStatusCode.NoContent) { description = "No rate classes found" } - } - }) { - val params = ClassParams.extract(call.request.queryParameters) - - val page = db.conversion.pageClass(params) - if (page.isEmpty()) { - call.respond(HttpStatusCode.NoContent) - } else { - call.respond(ConversionRateClasses(page)) - } - } - } -} diff --git a/libeufin-bank/src/main/kotlin/tech/libeufin/bank/api/ObservabilityApi.kt b/libeufin-bank/src/main/kotlin/tech/libeufin/bank/api/ObservabilityApi.kt @@ -1,85 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2025 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.bank.api - -import io.ktor.http.* -import io.ktor.server.application.* -import io.ktor.server.request.* -import io.ktor.server.response.* -import io.ktor.server.routing.* -import io.github.smiley4.ktoropenapi.* -import io.ktor.util.pipeline.* -import io.prometheus.metrics.core.metrics.* -import io.prometheus.metrics.model.registry.PrometheusRegistry -import io.prometheus.metrics.model.snapshots.Unit -import io.prometheus.metrics.instrumentation.jvm.JvmMetrics; -import io.prometheus.metrics.expositionformats.ExpositionFormats -import tech.libeufin.common.* -import tech.libeufin.common.db.* -import tech.libeufin.bank.* -import tech.libeufin.bank.db.* -import tech.libeufin.bank.auth.* -import java.time.Instant -import java.io.ByteArrayOutputStream - -object Metrics { - @Volatile - private var tanChannelCounter = Counter.builder() - .name("libeufin_bank_tan_channel") - .help("TAN script calls") - .labelNames("channel", "exit") - - init { - // Register JVM metrics - JvmMetrics.builder().register() - } - - // TODO add database table counter info ? -} - -fun Routing.observabilityApi(db: Database, cfg: BankConfig) { - get("/taler-observability/config", { - operationId = "getObservabilityConfig" - description = "Get observability configuration" - tags = listOf("Observability") - response { - code(HttpStatusCode.OK) { description = "Observability configuration"; body<TalerObservabilityConfig>() } - } - }) { - call.respond(TalerObservabilityConfig()) - } - authAdmin(db, cfg.pwCrypto, TokenLogicalScope.observability, cfg.basicAuthCompat) { - get("/taler-observability/metrics", { - operationId = "getMetrics" - description = "Get Prometheus metrics" - tags = listOf("Observability") - protected = true - securitySchemeNames("bearerAuth", "basicAuth") - response { - code(HttpStatusCode.OK) { description = "Prometheus metrics in text format" } - } - }) { - val snapshot = PrometheusRegistry.defaultRegistry.scrape() - val outputStream = ByteArrayOutputStream() - ExpositionFormats.init().getPrometheusTextFormatWriter().write(outputStream, snapshot) - call.respondText(outputStream.toString(Charsets.UTF_8), ContentType.parse("text/plain; version=0.0.4; charset=utf-8")) - } - } -} -\ No newline at end of file diff --git a/libeufin-bank/src/main/kotlin/tech/libeufin/bank/api/PreparedTransferApi.kt b/libeufin-bank/src/main/kotlin/tech/libeufin/bank/api/PreparedTransferApi.kt @@ -1,141 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2026 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.bank.api - -import io.ktor.http.* -import io.ktor.server.request.* -import io.ktor.server.response.* -import io.ktor.server.routing.* -import io.github.smiley4.ktoropenapi.* -import io.ktor.util.pipeline.* -import tech.libeufin.bank.* -import tech.libeufin.bank.auth.pathUsername -import tech.libeufin.bank.db.Database -import tech.libeufin.bank.db.TransferDAO.RegistrationResult -import tech.libeufin.common.* -import tech.libeufin.common.crypto.CryptoUtil -import java.time.Instant -import java.time.Duration - -fun Routing.preparedTransferApi(db: Database, cfg: BankConfig) { - get("/taler-prepared-transfer/config", { - operationId = "getPreparedTransferConfig" - description = "Get the configuration of the prepared transfer API" - tags = listOf("Prepared Transfer") - request { - pathParameter<String>("USERNAME") { description = "Account username" } - } - response { - code(HttpStatusCode.OK) { description = "Configuration of the prepared transfer API"; body<PreparedTransferConfig>() } - } - }) { - call.respond( - PreparedTransferConfig( - currency = cfg.regionalCurrency, - supported_formats = listOf(SubjectFormat.SIMPLE) - ) - ) - } - post("/taler-prepared-transfer/registration", { - operationId = "registerTransfer" - description = "Register a prepared transfer" - tags = listOf("Prepared Transfer") - request { - body<SubjectRequest>() - } - response { - code(HttpStatusCode.OK) { description = "Registration successful"; body<SubjectResult>() } - code(HttpStatusCode.Conflict) { description = "Reserve pub or subject derivation already used" } - } - }) { - val req = call.receive<SubjectRequest>() - cfg.checkRegionalCurrency(req.credit_amount) - - if (!req.verify()) - throw forbidden( - "invalid signature", - TalerErrorCode.BANK_BAD_SIGNATURE - ) - - when (val result = db.transfer.register( - req.credit_account, - req.type, - req.account_pub, - req.authorization_pub, - req.authorization_sig, - req.recurrent, - req.credit_amount, - Instant.now() - )) { - RegistrationResult.NotExchange -> throw notExchange(req.credit_account.canonical) - RegistrationResult.ReservePubReuse -> throw conflict( - "reserve_pub used already", - TalerErrorCode.BANK_DUPLICATE_RESERVE_PUB_SUBJECT - ) - RegistrationResult.UnknownCreditor -> throw unknownCreditorAccount(req.credit_account.canonical) - is RegistrationResult.Success -> { - val subjects = mutableListOf<TransferSubject>() - if (result.uuid != null) - subjects.add(TransferSubject.Uri(cfg.talerWithdrawUri(result.uuid), req.credit_amount)) - subjects.add(TransferSubject.Simple(fmtIncomingSubject(IncomingType.map, req.authorization_pub), req.credit_amount)) - call.respond( - SubjectResult( - subjects, - TalerTimestamp.never() - ) - ) - } - } - } - post("/taler-prepared-transfer/unregistration", { - operationId = "unregisterTransfer" - description = "Unregister a prepared subject" - tags = listOf("Prepared Transfer") - response { - code(HttpStatusCode.NoContent) { description = "Successfully unregistered" } - code(HttpStatusCode.NotFound) { description = "Prepared transfer not found" } - code(HttpStatusCode.Conflict) { description = "Invalid signature or timestamp too old" } - } - }) { - val req = call.receive<Unregistration>(); - - val now = Instant.now() - if (req.timestamp.instant.isBefore(now.minus(Duration.ofMinutes(15))) || req.timestamp.instant.isAfter(now)) - throw conflict( - "timestamp too old", - TalerErrorCode.BANK_OLD_TIMESTAMP - ) - - if (!req.verify()) - throw forbidden( - "invalid signature", - TalerErrorCode.BANK_BAD_SIGNATURE - ) - - if (db.transfer.unregister(req.authorization_pub, Instant.now())) { - call.respond(HttpStatusCode.NoContent) - } else { - throw notFound( - "Prepared transfer '${req.authorization_pub}' not found", - TalerErrorCode.BANK_TRANSACTION_NOT_FOUND - ) - } - } -} diff --git a/libeufin-bank/src/main/kotlin/tech/libeufin/bank/api/RevenueApi.kt b/libeufin-bank/src/main/kotlin/tech/libeufin/bank/api/RevenueApi.kt @@ -1,89 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2023-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.bank.api - -import io.ktor.http.* -import io.ktor.server.application.* -import io.ktor.server.response.* -import io.ktor.server.routing.* -import io.github.smiley4.ktoropenapi.* -import tech.libeufin.bank.BankConfig -import tech.libeufin.bank.TokenLogicalScope -import tech.libeufin.bank.auth.auth -import tech.libeufin.bank.bankInfo -import tech.libeufin.bank.db.Database -import tech.libeufin.common.HistoryParams -import tech.libeufin.common.RevenueConfig -import tech.libeufin.common.RevenueIncomingHistory - -fun Routing.revenueApi(db: Database, cfg: BankConfig) { - get("/accounts/{USERNAME}/taler-revenue/config", { - operationId = "getRevenueConfig" - description = "Get revenue API configuration" - tags = listOf("Revenue") - request { - pathParameter<String>("USERNAME") { description = "Account username" } - } - response { - code(HttpStatusCode.OK) { description = "Revenue API configuration"; body<RevenueConfig>() } - } - }) { - call.respond(RevenueConfig( - currency = cfg.regionalCurrency - )) - } - auth(db, cfg.pwCrypto, TokenLogicalScope.revenue, cfg.basicAuthCompat) { - get("/accounts/{USERNAME}/taler-revenue/history", { - operationId = "getRevenueHistory" - description = "Get revenue history for an account" - tags = listOf("Revenue") - protected = true - securitySchemeNames("bearerAuth", "basicAuth") - request { - pathParameter<String>("USERNAME") { description = "Account username" } - queryParameter<Int>("limit") { - description = "Maximum number of results to return (default: -20, negative means descending)" - required = false - } - queryParameter<Long>("offset") { - description = "Row ID offset for pagination" - required = false - } - queryParameter<Long>("timeout_ms") { - description = "Timeout for long polling in milliseconds (default: 0, no long polling)" - required = false - } - } - response { - code(HttpStatusCode.OK) { description = "Revenue history"; body<RevenueIncomingHistory>() } - code(HttpStatusCode.NoContent) { description = "No revenue history found" } - } - }) { - val params = HistoryParams.extract(call.request.queryParameters) - val bankAccount = call.bankInfo(db) - val items = db.transaction.revenueHistory(params, bankAccount.bankAccountId) - - if (items.isEmpty()) { - call.respond(HttpStatusCode.NoContent) - } else { - call.respond(RevenueIncomingHistory(items, bankAccount.payto)) - } - } - } -} -\ No newline at end of file diff --git a/libeufin-bank/src/main/kotlin/tech/libeufin/bank/api/WireGatewayApi.kt b/libeufin-bank/src/main/kotlin/tech/libeufin/bank/api/WireGatewayApi.kt @@ -1,420 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2023, 2024, 2025, 2026 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/> - */ - -// This file contains the Taler Wire Gateway API handlers. - -package tech.libeufin.bank.api - -import io.ktor.http.* -import io.ktor.server.application.* -import io.ktor.server.request.* -import io.ktor.server.response.* -import io.ktor.server.routing.* -import io.github.smiley4.ktoropenapi.* -import tech.libeufin.bank.* -import tech.libeufin.bank.auth.auth -import tech.libeufin.bank.auth.authAdmin -import tech.libeufin.bank.auth.pathUsername -import tech.libeufin.bank.db.Database -import tech.libeufin.bank.db.ExchangeDAO -import tech.libeufin.bank.db.ExchangeDAO.AddIncomingResult -import tech.libeufin.bank.db.ExchangeDAO.TransferResult -import tech.libeufin.common.* -import java.time.Instant - - -fun Routing.wireGatewayApi(db: Database, cfg: BankConfig) { - get("/accounts/{USERNAME}/taler-wire-gateway/config", { - operationId = "getWireGatewayConfig" - description = "Get wire gateway configuration" - tags = listOf("Wire Gateway") - request { - pathParameter<String>("USERNAME") { description = "Account username" } - } - response { - code(HttpStatusCode.OK) { description = "Wire gateway configuration"; body<WireGatewayConfig>() } - } - }) { - call.respond( - WireGatewayConfig( - currency = cfg.regionalCurrency, - support_account_check = false, - ) - ) - } - auth(db, cfg.pwCrypto, TokenLogicalScope.readwrite_wiregateway, cfg.basicAuthCompat) { - post("/accounts/{USERNAME}/taler-wire-gateway/transfer", { - operationId = "createWireGatewayTransfer" - description = "Initiate a wire transfer from an exchange account" - tags = listOf("Wire Gateway") - protected = true - securitySchemeNames("bearerAuth", "basicAuth") - request { - pathParameter<String>("USERNAME") { description = "Account username" } - body<TransferRequest>() - } - response { - code(HttpStatusCode.OK) { description = "Transfer initiated"; body<TransferResponse>() } - code(HttpStatusCode.Conflict) { description = "Transfer conflict" } - } - }) { - val req = call.receive<TransferRequest>() - cfg.checkRegionalCurrency(req.amount) - val res = db.exchange.transfer( - req = req, - username = call.pathUsername, - timestamp = Instant.now(), - conversion = cfg.allowConversion - ) - when (res) { - TransferResult.UnknownExchange -> throw unknownAccount(call.pathUsername) - TransferResult.NotAnExchange -> throw notExchange(call.pathUsername) - TransferResult.BothPartyAreExchange -> throw conflict( - "Wire transfer attempted with credit and debit party being both exchange account", - TalerErrorCode.BANK_ACCOUNT_IS_EXCHANGE - ) - - TransferResult.ReserveUidReuse -> throw conflict( - "request_uid used already", - TalerErrorCode.BANK_TRANSFER_REQUEST_UID_REUSED - ) - - TransferResult.WtidReuse -> throw conflict( - "wtid used already", - TalerErrorCode.BANK_TRANSFER_WTID_REUSED - ) - - TransferResult.BalanceInsufficient -> throw conflict( - "Insufficient balance for exchange", - TalerErrorCode.BANK_UNALLOWED_DEBIT - ) - - TransferResult.AdminCreditor -> throw conflict( - "Cannot transfer money to admin account when conversion is disabled", - TalerErrorCode.BANK_ADMIN_CREDITOR - ) - - is TransferResult.Success -> call.respond( - TransferResponse( - timestamp = res.timestamp, - row_id = res.id - ) - ) - } - } - } - auth(db, cfg.pwCrypto, TokenLogicalScope.readonly_wiregateway, cfg.basicAuthCompat) { - suspend fun <T> ApplicationCall.historyEndpoint( - reduce: (List<T>, String) -> Any, - dbLambda: suspend ExchangeDAO.(HistoryParams, Long) -> List<T> - ) { - val params = HistoryParams.extract(this.request.queryParameters) - val bankAccount = this.bankInfo(db) - - if (!bankAccount.isTalerExchange) - throw notExchange(pathUsername) - - val items = db.exchange.dbLambda(params, bankAccount.bankAccountId) - - if (items.isEmpty()) { - this.respond(HttpStatusCode.NoContent) - } else { - this.respond(reduce(items, bankAccount.payto)) - } - } - get("/accounts/{USERNAME}/taler-wire-gateway/history/incoming", { - operationId = "getIncomingHistory" - description = "Get incoming wire transfer history" - tags = listOf("Wire Gateway") - protected = true - securitySchemeNames("bearerAuth", "basicAuth") - request { - pathParameter<String>("USERNAME") { description = "Account username" } - queryParameter<Int>("limit") { - description = "Maximum number of results to return (default: -20, negative means descending)" - required = false - } - queryParameter<Long>("offset") { - description = "Row ID offset for pagination" - required = false - } - queryParameter<Long>("timeout_ms") { - description = "Timeout for long polling in milliseconds (default: 0, no long polling)" - required = false - } - } - response { - code(HttpStatusCode.OK) { description = "Incoming transfer history"; body<IncomingHistory>() } - code(HttpStatusCode.NoContent) { description = "No incoming transfers found" } - } - }) { - call.historyEndpoint(::IncomingHistory, ExchangeDAO::incomingHistory) - } - get("/accounts/{USERNAME}/taler-wire-gateway/history/outgoing", { - operationId = "getOutgoingHistory" - description = "Get outgoing wire transfer history" - tags = listOf("Wire Gateway") - protected = true - securitySchemeNames("bearerAuth", "basicAuth") - request { - pathParameter<String>("USERNAME") { description = "Account username" } - queryParameter<Int>("limit") { - description = "Maximum number of results to return (default: -20, negative means descending)" - required = false - } - queryParameter<Long>("offset") { - description = "Row ID offset for pagination" - required = false - } - queryParameter<Long>("timeout_ms") { - description = "Timeout for long polling in milliseconds (default: 0, no long polling)" - required = false - } - } - response { - code(HttpStatusCode.OK) { description = "Outgoing transfer history"; body<OutgoingHistory>() } - code(HttpStatusCode.NoContent) { description = "No outgoing transfers found" } - } - }) { - call.historyEndpoint(::OutgoingHistory, ExchangeDAO::outgoingHistory) - } - get("/accounts/{USERNAME}/taler-wire-gateway/transfers", { - operationId = "listWireGatewayTransfers" - description = "List wire gateway transfers" - tags = listOf("Wire Gateway") - protected = true - securitySchemeNames("bearerAuth", "basicAuth") - request { - pathParameter<String>("USERNAME") { description = "Account username" } - queryParameter<Int>("limit") { - description = "Maximum number of results to return (default: -20, negative means descending)" - required = false - } - queryParameter<Long>("offset") { - description = "Row ID offset for pagination" - required = false - } - queryParameter<String>("status") { - description = "Filter by transfer status (pending, transient_failure, permanent_failure, success)" - required = false - } - } - response { - code(HttpStatusCode.OK) { description = "List of transfers"; body<TransferList>() } - code(HttpStatusCode.NoContent) { description = "No transfers found" } - } - }) { - val params = TransferParams.extract(call.request.queryParameters) - val bankAccount = call.bankInfo(db) - if (!bankAccount.isTalerExchange) - throw notExchange(call.pathUsername) - - if (params.status != null && params.status != TransferStatusState.success && params.status != TransferStatusState.permanent_failure) { - call.respond(HttpStatusCode.NoContent) - } else { - val items = db.exchange.pageTransfer(params.page, bankAccount.bankAccountId, params.status) - - if (items.isEmpty()) { - call.respond(HttpStatusCode.NoContent) - } else { - call.respond(TransferList(items, bankAccount.payto)) - } - } - } - get("/accounts/{USERNAME}/taler-wire-gateway/transfers/{ROW_ID}", { - operationId = "getWireGatewayTransfer" - description = "Get a specific wire gateway transfer" - tags = listOf("Wire Gateway") - protected = true - securitySchemeNames("bearerAuth", "basicAuth") - request { - pathParameter<String>("USERNAME") { description = "Account username" } - pathParameter<Long>("ROW_ID") { description = "Transfer row ID" } - } - response { - code(HttpStatusCode.OK) { description = "Transfer details"; body<TransferStatus>() } - code(HttpStatusCode.NotFound) { description = "Transfer not found" } - } - }) { - val bankAccount = call.bankInfo(db) - if (!bankAccount.isTalerExchange) - throw notExchange(call.pathUsername) - - val txId = call.longPath("ROW_ID") - val transfer = db.exchange.getTransfer(bankAccount.bankAccountId, txId) ?: throw notFound( - "Transfer '$txId' not found", - TalerErrorCode.BANK_TRANSACTION_NOT_FOUND - ) - call.respond(transfer) - } - get("/accounts/{USERNAME}/taler-wire-gateway/account/check", { - operationId = "checkAccount" - description = "Check if an account exists" - tags = listOf("Wire Gateway") - protected = true - securitySchemeNames("bearerAuth", "basicAuth") - request { - pathParameter<String>("USERNAME") { description = "Account username" } - queryParameter<String>("account") { - description = "Payto URI of the account to check (required)" - required = true - } - } - response { - code(HttpStatusCode.OK) { description = "Account information"; body<AccountInfo>() } - code(HttpStatusCode.NotFound) { description = "Account not found" } - } - }) { - val bankAccount = call.bankInfo(db) - if (!bankAccount.isTalerExchange) - throw notExchange(call.pathUsername) - - val params = AccountCheckParams.extract(call.request.queryParameters) - val account = params.account.expectIban() - - val info = db.account.checkInfo(account) ?: throw unknownAccount(account.canonical) - call.respond(info) - } - } - authAdmin(db, cfg.pwCrypto, TokenLogicalScope.readwrite, cfg.basicAuthCompat) { - suspend fun ApplicationCall.addIncoming( - amount: TalerAmount, - debitAccount: Payto, - subject: String, - metadata: IncomingSubject - ) { - cfg.checkRegionalCurrency(amount) - val timestamp = Instant.now() - val res = db.exchange.addIncoming( - amount = amount, - debitAccount = debitAccount, - subject = subject, - username = pathUsername, - timestamp = timestamp, - metadata = metadata - ) - when (res) { - AddIncomingResult.UnknownExchange -> throw unknownAccount(pathUsername) - AddIncomingResult.NotAnExchange -> throw notExchange(pathUsername) - AddIncomingResult.UnknownDebtor -> throw conflict( - "Debtor account $debitAccount was not found", - TalerErrorCode.BANK_UNKNOWN_DEBTOR - ) - - AddIncomingResult.BothPartyAreExchange -> throw conflict( - "Wire transfer attempted with credit and debit party being both exchange account", - TalerErrorCode.BANK_ACCOUNT_IS_EXCHANGE - ) - - AddIncomingResult.ReservePubReuse -> throw conflict( - "reserve_pub used already", - TalerErrorCode.BANK_DUPLICATE_RESERVE_PUB_SUBJECT - ) - - AddIncomingResult.BalanceInsufficient -> throw conflict( - "Insufficient balance for debitor", - TalerErrorCode.BANK_UNALLOWED_DEBIT - ) - AddIncomingResult.MappingReuse -> throw conflict( - "authorization_pub used already", - TalerErrorCode.BANK_TRANSFER_MAPPING_REUSED - ) - AddIncomingResult.UnknownMapping -> throw conflict( - "authorization_pub unknown", - TalerErrorCode.BANK_TRANSFER_MAPPING_UNKNOWN - ) - is AddIncomingResult.Success -> this.respond( - AddIncomingResponse( - timestamp = TalerTimestamp(timestamp), - row_id = res.id - ) - ) - } - } - post("/accounts/{USERNAME}/taler-wire-gateway/admin/add-incoming", { - operationId = "addIncoming" - description = "Add an incoming wire transfer (admin)" - tags = listOf("Wire Gateway") - protected = true - securitySchemeNames("bearerAuth", "basicAuth") - request { - pathParameter<String>("USERNAME") { description = "Account username" } - body<AddIncomingRequest>() - } - response { - code(HttpStatusCode.OK) { description = "Incoming transfer added"; body<AddIncomingResponse>() } - code(HttpStatusCode.Conflict) { description = "Transfer conflict" } - } - }) { - val req = call.receive<AddIncomingRequest>() - call.addIncoming( - amount = req.amount, - debitAccount = req.debit_account, - subject = "Admin incoming ${req.reserve_pub}", - metadata = IncomingSubject.Reserve(req.reserve_pub) - ) - } - post("/accounts/{USERNAME}/taler-wire-gateway/admin/add-kycauth", { - operationId = "addKycauth" - description = "Add an incoming KYC auth wire transfer (admin)" - tags = listOf("Wire Gateway") - protected = true - securitySchemeNames("bearerAuth", "basicAuth") - request { - pathParameter<String>("USERNAME") { description = "Account username" } - body<AddKycauthRequest>() - } - response { - code(HttpStatusCode.OK) { description = "KYC auth transfer added"; body<AddIncomingResponse>() } - code(HttpStatusCode.Conflict) { description = "Transfer conflict" } - } - }) { - val req = call.receive<AddKycauthRequest>() - call.addIncoming( - amount = req.amount, - debitAccount = req.debit_account, - subject = "Admin incoming KYC:${req.account_pub}", - metadata = IncomingSubject.Kyc(req.account_pub) - ) - } - post("/accounts/{USERNAME}/taler-wire-gateway/admin/add-mapped", { - operationId = "addMapped" - description = "Add an incoming mapped wire transfer (admin)" - tags = listOf("Wire Gateway") - protected = true - securitySchemeNames("bearerAuth", "basicAuth") - request { - pathParameter<String>("USERNAME") { description = "Account username" } - body<AddMappedRequest>() - } - response { - code(HttpStatusCode.OK) { description = "Mapped transfer added"; body<AddIncomingResponse>() } - code(HttpStatusCode.Conflict) { description = "Transfer conflict" } - } - }) { - val req = call.receive<AddMappedRequest>() - call.addIncoming( - amount = req.amount, - debitAccount = req.debit_account, - subject = "Manual incoming MAP:${req.authorization_pub}", - metadata = IncomingSubject.Map(req.authorization_pub) - ) - } - } -} diff --git a/libeufin-bank/src/main/kotlin/tech/libeufin/bank/auth/auth.kt b/libeufin-bank/src/main/kotlin/tech/libeufin/bank/auth/auth.kt @@ -1,274 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2024-2025 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.bank.auth - -import org.slf4j.Logger -import org.slf4j.LoggerFactory -import io.ktor.http.* -import io.ktor.server.application.* -import io.ktor.server.response.* -import io.ktor.server.routing.* -import io.ktor.util.* -import io.ktor.util.pipeline.* -import tech.libeufin.bank.* -import tech.libeufin.bank.db.AccountDAO.CheckPasswordResult -import tech.libeufin.bank.db.Database -import tech.libeufin.common.* -import tech.libeufin.common.api.intercept -import tech.libeufin.common.crypto.PwCrypto -import java.time.Instant - -private val logger: Logger = LoggerFactory.getLogger("libeufin-bank-auth") - -/** Used to store if the current request is authenticated */ -private val AUTH = AttributeKey<Boolean>("auth") - -/** Used to store if the currently authenticated user is admin */ -private val AUTH_IS_ADMIN = AttributeKey<Boolean>("is_admin") - -/** Used to store used auth token */ -private val AUTH_TOKEN = AttributeKey<ByteArray>("auth_token") - -/** Used to store auth account info */ -val AUTH_INFO = AttributeKey<BankInfo>("auth_info") - - -const val TOKEN_PREFIX = "secret-token:" - -/** Get username of the request account */ -val ApplicationCall.pathUsername: String get() = parameters.expect("USERNAME") - -/** Check if current request is authenticated */ -val ApplicationCall.isAuthenticated: Boolean get() = attributes.getOrNull(AUTH) == true - -/** Check if current auth account is admin */ -val ApplicationCall.isAdmin: Boolean get() = attributes.getOrNull(AUTH_IS_ADMIN) == true - -/** Check auth token used for authentication */ -val ApplicationCall.authToken: ByteArray? get() = attributes.getOrNull(AUTH_TOKEN) - - -/** - * Create an admin authenticated route for [scope]. - * - * If [enforce], only admin can access this route. - * - * You can check is the currently authenticated user is admin using [isAdmin]. - **/ -fun Route.authAdmin( - db: Database, - pwCrypto: PwCrypto, - scope: TokenLogicalScope, - compatPw: Boolean, - enforce: Boolean = true, - callback: Route.() -> Unit -): Route = intercept("AuthAdmin", callback) { - if (enforce) { - val info = this.authenticateBankRequest(db, pwCrypto, scope, false, compatPw) - if (info.username != "admin") { - throw forbidden("Only administrator allowed") - } - } else { - try { - this.authenticateBankRequest(db, pwCrypto, scope, false, compatPw) - } catch (e: Exception) { - null - } - } -} - - -/** - * Create an authenticated route for [scope]. - * - * If [allowAdmin], admin is allowed to auth for any user. - * If [requireAdmin], only admin can access this route. - * - * You can check is the currently authenticated user is admin using [isAdmin]. - **/ -fun Route.auth( - db: Database, - pwCrypto: PwCrypto, - scope: TokenLogicalScope, - compatPw: Boolean, - allowAdmin: Boolean = false, - requireAdmin: Boolean = false, - allowPw: Boolean = false, - callback: Route.() -> Unit -): Route = intercept("Auth", callback) { - val info = this.authenticateBankRequest(db, pwCrypto, scope, allowPw, compatPw) - if (requireAdmin && info.username != "admin") { - throw forbidden("Only administrator allowed") - } else { - val hasRight = info.username == pathUsername || (allowAdmin && info.username == "admin") - if (!hasRight) { - throw forbidden("Customer ${info.username} have no right on $pathUsername account") - } - } -} - -/** - * Create an optionally authenticated route for [scope]. - * - * You can check is the currently authenticated user is admin using [isAdmin]. - **/ -fun Route.optAuth( - db: Database, - pwCrypto: PwCrypto, - scope: TokenLogicalScope, - compatPw: Boolean, - allowAdmin: Boolean = false, - callback: Route.() -> Unit -): Route = intercept("Auth", callback) { - val header = request.headers[HttpHeaders.Authorization] - if (header != null) { - val info = this.authenticateBankRequest(db, pwCrypto, scope, false, compatPw) - val hasRight = info.username == pathUsername || (allowAdmin && info.username == "admin") - if (!hasRight) { - throw forbidden("Customer ${info.username} have no right on $pathUsername account") - } - } -} - -fun missingAuth(): ApiException = unauthorized( - "Authorization header not found", - TalerErrorCode.GENERIC_PARAMETER_MISSING -) - -/** - * Authenticate an HTTP request for [requiredScope] according to the scheme that is mentioned - * in the Authorization header. - * The allowed schemes are either 'Basic' or 'Bearer'. - * - * Returns the authenticated customer username. - */ -private suspend fun ApplicationCall.authenticateBankRequest( - db: Database, - pwCrypto: PwCrypto, - requiredScope: TokenLogicalScope, - allowPw: Boolean, - compatPw: Boolean -): BankInfo { - val header = request.headers[HttpHeaders.Authorization] - - // Basic auth challenge - if (header == null) { - if (allowPw || compatPw) { - response.header(HttpHeaders.WWWAuthenticate, "Basic realm=\"LibEuFin Bank\", charset=\"UTF-8\"") - } - throw missingAuth() - } - - // Parse header - val (scheme, content) = header.splitOnce(" ") ?: throw badRequest( - "Authorization is invalid", - TalerErrorCode.GENERIC_HTTP_HEADERS_MALFORMED - ) - val info = when (scheme) { - "Basic" -> doBasicAuth(db, content, pwCrypto, allowPw, compatPw) - "Bearer" -> doTokenAuth(db, content, requiredScope) - else -> throw unauthorized("Authorization method '$scheme' wrong or not supported") - } - - this.attributes.put(AUTH, true) - this.attributes.put(AUTH_IS_ADMIN, info.username == "admin") - this.attributes.put(AUTH_INFO, info) - return info -} - -/** - * Performs the HTTP Basic Authentication. - * - * Returns the authenticated customer username - */ -private suspend fun doBasicAuth( - db: Database, - encoded: String, - pwCrypto: PwCrypto, - allowPw: Boolean, - compatPw: Boolean -): BankInfo { - val decoded = String(encoded.decodeBase64(), Charsets.UTF_8) - val (username, plainPassword) = decoded.splitOnce(":") ?: throw badRequest( - "Malformed Basic auth credentials found in the Authorization header", - TalerErrorCode.GENERIC_HTTP_HEADERS_MALFORMED - ) - if (!allowPw) { - logger.warn("User '$username' used deprecated password auth") - if (!compatPw) { - throw unauthorized("Authorization method 'Basic' not supported") - } - } - when (val res = db.account.checkPassword(username, plainPassword, pwCrypto)) { - CheckPasswordResult.UnknownAccount -> throw unauthorized("Unknown account") - CheckPasswordResult.PasswordMismatch -> throw unauthorized("Bad password") - CheckPasswordResult.Locked -> throw forbidden("Account is locked", TalerErrorCode.BANK_ACCOUNT_LOCKED) - is CheckPasswordResult.Success -> return res.info - } -} - -fun validScope(required: TokenLogicalScope, scope: TokenScope): Boolean = when (required) { - TokenLogicalScope.readonly -> scope in setOf(TokenScope.readonly, TokenScope.readwrite) - TokenLogicalScope.readwrite -> scope in setOf(TokenScope.readwrite) - TokenLogicalScope.revenue -> scope in setOf(TokenScope.readonly, TokenScope.readwrite, TokenScope.revenue) - TokenLogicalScope.readonly_wiregateway -> scope in setOf(TokenScope.wiregateway, TokenScope.readonly, TokenScope.readwrite) - TokenLogicalScope.readwrite_wiregateway -> scope in setOf(TokenScope.wiregateway, TokenScope.readwrite) - TokenLogicalScope.observability -> scope in setOf(TokenScope.readonly, TokenScope.readwrite, TokenScope.observability) - TokenLogicalScope.refreshable -> true -} - -/** - * Performs the secret-token HTTP Bearer Authentication. - * - * Returns the authenticated customer username - */ -private suspend fun ApplicationCall.doTokenAuth( - db: Database, - bearer: String, - requiredScope: TokenLogicalScope, -): BankInfo { - if (!bearer.startsWith(TOKEN_PREFIX)) throw badRequest( - "Bearer token malformed", - TalerErrorCode.GENERIC_HTTP_HEADERS_MALFORMED - ) - val decoded = try { - Base32Crockford.decode(bearer.slice(13..<bearer.length)) - } catch (e: Exception) { - throw badRequest( - e.message, TalerErrorCode.GENERIC_HTTP_HEADERS_MALFORMED - ) - } - val (token, info) = db.token.accessInfo(decoded, Instant.now()) ?: throw unauthorized("Unknown token", TalerErrorCode.GENERIC_TOKEN_UNKNOWN) - when { - token.expirationTime.isBefore(Instant.now()) - -> throw unauthorized("Expired auth token", TalerErrorCode.GENERIC_TOKEN_EXPIRED) - - !validScope(requiredScope, token.scope) - -> throw forbidden("Auth token has insufficient scope", TalerErrorCode.GENERIC_TOKEN_PERMISSION_INSUFFICIENT) - - !token.isRefreshable && requiredScope == TokenLogicalScope.refreshable - -> throw forbidden("Unrefreshable token", TalerErrorCode.GENERIC_TOKEN_PERMISSION_INSUFFICIENT) - } - - this.attributes.put(AUTH_TOKEN, decoded) - this.attributes.put(AUTH_INFO, info) - - return info -} -\ No newline at end of file diff --git a/libeufin-bank/src/main/kotlin/tech/libeufin/bank/auth/mfa.kt b/libeufin-bank/src/main/kotlin/tech/libeufin/bank/auth/mfa.kt @@ -1,184 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2023-2025 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.bank.auth - -import io.ktor.http.* -import io.ktor.server.application.* -import io.ktor.server.request.* -import io.ktor.server.response.* -import io.ktor.server.plugins.* -import kotlinx.serialization.json.Json -import tech.libeufin.bank.* -import tech.libeufin.bank.db.* -import tech.libeufin.bank.db.TanDAO.* -import tech.libeufin.common.* -import tech.libeufin.common.crypto.* -import tech.libeufin.common.api.* -import java.text.DecimalFormat -import java.time.Instant -import java.util.UUID - -private suspend fun ApplicationCall.respondChallenges( - db: Database, - op: Operation, - tans: Tans -): List<Challenge> { - val (hbody, salt) = CryptoUtil.mfaBodyHashCreate(this.rawBody) - val challenges = mutableListOf<Challenge>() - - for ((channel, info) in tans) { - val code = Tan.genCode() - val uuid = db.tan.new( - hbody = hbody, - salt = salt, - username = pathUsername, - op = op, - code = code, - timestamp = Instant.now(), - retryCounter = TAN_RETRY_COUNTER, - validityPeriod = TAN_VALIDITY_PERIOD, - tanChannel = channel, - tanInfo = info - ) - val privateInfo = if (op == Operation.create_token) { - "REDACTED" - } else { - info - } - challenges.add(Challenge( - challenge_id = uuid.toString(), - tan_channel = channel, - tan_info = privateInfo - )) - } - return challenges -} - -/** - * Generate a TAN challenge for an [op] request with [body] and - * respond to the HTTP request with a TAN challenge. - * - * If [channel] and [info] are present, they will be used - * to send the TAN code, otherwise defaults will be used. - */ -suspend fun ApplicationCall.respondMfa( - db: Database, - op: Operation -) { - val info = this.bankInfo(db) - var challenges = respondChallenges(db, op, info.mfa) - respond( - status = HttpStatusCode.Accepted, - message = ChallengeResponse( - challenges = challenges, - combi_and = false - ) - ) -} - -suspend fun ApplicationCall.respondValidation( - db: Database, - op: Operation, - tans: Tans -) { - val challenges = respondChallenges(db, op, tans) - respond( - status = HttpStatusCode.Accepted, - message = ChallengeResponse( - challenges = challenges, - combi_and = true - ) - ) -} - -/** - * Retrieve a confirmed challenge and its body for [op] from the database - * if the challenge header is defined, otherwise extract the HTTP body. - */ -suspend inline fun <reified B> ApplicationCall.receiveChallenge( - db: Database, - op: Operation, - default: B? = null -): Pair<B, Tans?> { - // Parse body - val contentLenght = request.headers[HttpHeaders.ContentLength]?.toIntOrNull() ?: 0 - val body: B = if (contentLenght == 0 && default != null) { - default - } else { - this.receive() - } - - // Check if challenges are used - val ids = request.headers[TALER_CHALLENGE_IDS] - if (ids == null) return Pair(body, null) - - // List validated challenges - val uuids = ids.split(',').map { UUID.fromString(it.trim()) } - val challenges = db.tan.challenge(uuids) - - val validated = challenges.mapNotNull { challenge -> - if (challenge.op != op) { - throw forbidden("Challenge is for a different operation body") - } else if (!CryptoUtil.mfaBodyHashCheck(this.rawBody, challenge.hbody, challenge.salt)) { - throw forbidden("Challenge is for a different request body") - } else if (challenge.confirmed) { - Pair(challenge.channel, challenge.info) - } else { - null - } - } - - if (validated.isEmpty()) return Pair(body, null) - - // CHeck if challenges are solved - val bankInfo = this.bankInfo(db) - - // Account reconfig require mfa & new TAN validation - if (op == Operation.account_reconfig) { - val req = body as AccountReconfiguration - - val requiredValidation = req.requiredValidation(bankInfo) - if (requiredValidation.all { validated.contains(it) }) { - return Pair(body, emptyList()) - } else if (bankInfo.mfa.any { validated.contains(it) }) { - return Pair(body, requiredValidation) - } else { - return Pair(body, null) - } - } else { - // Check mfa - if (bankInfo.mfa.any { validated.contains(it) }) { - return Pair(body, emptyList()) - } else { - return Pair(body, null) - } - } -} - -object Tan { - private val CODE_FORMAT = DecimalFormat("00000000") - - /** Generate a secure random TAN code */ - fun genCode(): String { - val rand = SECURE_RNG.get().nextInt(100000000) - val code = CODE_FORMAT.format(rand) - return code - } -} - diff --git a/libeufin-bank/src/main/kotlin/tech/libeufin/bank/cli/BenchPwh.kt b/libeufin-bank/src/main/kotlin/tech/libeufin/bank/cli/BenchPwh.kt @@ -1,61 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2024-2025 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.bank.cli - -import com.github.ajalt.clikt.core.CliktCommand -import com.github.ajalt.clikt.core.Context -import com.github.ajalt.clikt.parameters.groups.provideDelegate -import tech.libeufin.bank.bankConfig -import tech.libeufin.bank.logger -import tech.libeufin.common.TalerCmd -import tech.libeufin.common.crypto.PwCrypto - -class BenchPwh : TalerCmd("bench-pwh") { - override fun help(context: Context) = "Benchmark password hashing algorithm and configuration" - - override fun run() = cliCmd(logger) { - val pwCrypto = bankConfig(config).pwCrypto - - when (pwCrypto) { - is PwCrypto.Bcrypt -> println("Benching bcrypt with cost=${pwCrypto.cost} for 10s") - } - - val start = System.currentTimeMillis() - val stop = start + 10000 // 10s - var count = 0 - - while (true) { - val now = System.currentTimeMillis() - if (now < stop) { - val password = (0..10).map { - "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#".random() - }.joinToString("") - pwCrypto.hashpw(password) - count ++ - } else { - val elapsed = (now-start).toDouble() - val perSec = count.toDouble() / (elapsed / 1000.0) - val iterTime = elapsed / count.toDouble() - println("hash password in ${String.format("%.0f", iterTime)}ms ${String.format("%.2f", perSec)} H/s") - break - } - } - } -} -\ No newline at end of file diff --git a/libeufin-bank/src/main/kotlin/tech/libeufin/bank/cli/ChangePw.kt b/libeufin-bank/src/main/kotlin/tech/libeufin/bank/cli/ChangePw.kt @@ -1,75 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2024-2025 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.bank.cli - -import com.github.ajalt.clikt.core.CliktCommand -import com.github.ajalt.clikt.core.Context -import com.github.ajalt.clikt.parameters.arguments.* -import com.github.ajalt.clikt.parameters.groups.provideDelegate -import com.github.ajalt.clikt.parameters.options.* -import tech.libeufin.bank.bankConfig -import tech.libeufin.bank.db.AccountDAO.AccountPatchAuthResult -import tech.libeufin.bank.logger -import tech.libeufin.bank.withDb -import tech.libeufin.common.TalerCmd -import tech.libeufin.common.crypto.checkPw -import com.github.ajalt.mordant.terminal.* - -class ChangePw : TalerCmd("passwd") { - override fun help(context: Context) = "Change account password" - - private val username by argument("username", help = "Account username") - private val password by argument( - "password", - help = "Account password used for authentication" - ).defaultLazy("prompt") { - val terminal = Terminal() - ConfirmationPrompt.create( - "Password", - "Repeat for confirmation", - "Values do not match, try again", - { - object : Prompt<String>( - prompt = it, - terminal = terminal, - hideInput = true - ) { - override fun convert(input: String): ConversionResult<String> { - return ConversionResult.Valid(input) - } - } - } - ).ask()!! - } - override fun run() = cliCmd(logger) { - bankConfig(config).withDb { db, cfg -> - val password = password.checkPw(cfg.pwdCheckQuality) - val res = db.account.reconfigPassword(username, password, null, true, cfg.pwCrypto) - 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") - } - } - } -} -\ No newline at end of file diff --git a/libeufin-bank/src/main/kotlin/tech/libeufin/bank/cli/CreateAccount.kt b/libeufin-bank/src/main/kotlin/tech/libeufin/bank/cli/CreateAccount.kt @@ -1,116 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2024-2025 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.bank.cli - -import com.github.ajalt.clikt.core.CliktCommand -import com.github.ajalt.clikt.core.Context -import com.github.ajalt.clikt.parameters.arguments.argument -import com.github.ajalt.clikt.parameters.arguments.convert -import com.github.ajalt.clikt.parameters.arguments.optional -import com.github.ajalt.clikt.parameters.groups.OptionGroup -import com.github.ajalt.clikt.parameters.groups.cooccurring -import com.github.ajalt.clikt.parameters.groups.provideDelegate -import com.github.ajalt.clikt.parameters.options.* -import kotlinx.serialization.json.Json -import tech.libeufin.bank.* -import tech.libeufin.bank.api.* -import tech.libeufin.bank.db.AccountDAO.* -import tech.libeufin.common.* - -class CreateAccountOption: OptionGroup() { - val username: String by option( - "-u", "--user", "--username", - metavar = "<username>", - help = "Account unique username" - ).required() - val password: String by option( - "--password", "-p", - help = "Account password used for authentication" - ).prompt(requireConfirmation = true, hideInput = true) - val name: String by option( - help = "Legal name of the account owner" - ).required() - val is_public: Boolean by option( - "--public", - help = "Make this account visible to anyone" - ).flag() - val exchange: Boolean by option( - help = "Make this account a taler exchange" - ).flag() - val email: String? by option(help = "E-Mail address used for TAN transmission") - val phone: String? by option(help = "Phone number used for TAN transmission") - val cashout_payto_uri: IbanPayto? by option( - help = "Payto URI of a fiant account who receive cashout amount" - ).convert { Payto.parse(it).expectIban() } - val payto_uri: Payto? by option( - help = "Payto URI of this account" - ).convert { Payto.parse(it) } - val debit_threshold: TalerAmount? by option( - help = "Max debit allowed for this account" - ).convert { TalerAmount(it) } - val tan_channel: TanChannel? by option( - help = "Enables 2FA and set the TAN channel used for challenges" - ).convert { TanChannel.valueOf(it) } -} - -class CreateAccount : TalerCmd("create-account") { - override fun help(context: Context) = "Create an account, returning the payto://-URI associated with it" - - private val json by argument().convert { Json.decodeFromString<RegisterAccountRequest>(it) }.optional() - private val options by CreateAccountOption().cooccurring() - - override fun run() = cliCmd(logger) { - bankConfig(config).withDb { db, cfg -> - 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, - tan_channel = tan_channel - ) - } - req?.let { - when (val result = createAccount(db, cfg, req, true)) { - AccountCreationResult.BonusBalanceInsufficient -> - throw Exception("Insufficient admin funds to grant bonus") - AccountCreationResult.UsernameReuse -> - throw Exception("Account username reuse '${req.username}'") - AccountCreationResult.PayToReuse -> - throw Exception("Bank internalPayToUri reuse") - AccountCreationResult.UnknownConversionClass -> - throw Exception("Unknown conversion class ${req.conversion_rate_class_id}") - is AccountCreationResult.Success -> { - logger.info("Account '${req.username}' created") - println(result.payto) - } - } - } - } - } -} -\ No newline at end of file diff --git a/libeufin-bank/src/main/kotlin/tech/libeufin/bank/cli/CreateToken.kt b/libeufin-bank/src/main/kotlin/tech/libeufin/bank/cli/CreateToken.kt @@ -1,96 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2024-2025 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.bank.cli - -import com.github.ajalt.clikt.core.CliktCommand -import com.github.ajalt.clikt.core.Context -import com.github.ajalt.clikt.parameters.arguments.* -import com.github.ajalt.clikt.parameters.groups.provideDelegate -import com.github.ajalt.clikt.parameters.options.* -import com.github.ajalt.clikt.parameters.types.* -import tech.libeufin.bank.* -import tech.libeufin.bank.auth.* -import tech.libeufin.common.* -import java.time.* -import java.time.temporal.ChronoUnit -import java.util.concurrent.TimeUnit - -class CreateToken : TalerCmd("create-token") { - override fun help(context: Context) = "Create authentication token for a user" - - private val username by option( - "-u", "--user", "--username", - metavar = "<username>", - help = "Account username" - ).required() - private val scope by option("--scope", "-s", help = "Scope for the token").enum<TokenScope>().required() - private val duration by option("--duration", "-d", metavar = "<forever|micros>", help = "Custom token validity duration").convert { - if (it == "forever") { - ChronoUnit.FOREVER.duration - } else { - val dUs = it.toLongOrNull() ?: throw Exception("Expected forever or a number in micros") - when { - dUs < 0 -> throw Exception("Negative duration specified") - dUs > RelativeTime.MAX_SAFE_INTEGER -> throw Exception("Duration value exceed cap (2^53-1)") - else -> Duration.of(dUs, ChronoUnit.MICROS) - } - } - }.default(TOKEN_DEFAULT_DURATION) - private val description by option("--description", help = "Optional token description") - private val refreshable by option("--refreshable", help = "Make the token refreshable into a new token").flag() - private val currentToken by option("--current-token", help = "Current token to reuse if still valid").convert { - Base32Crockford.decode(it.removePrefix(TOKEN_PREFIX)) - } - - override fun run() = cliCmd(logger) { - bankConfig(config).withDb { db, cfg -> - val now = Instant.now() - - val token = currentToken?.let { db.token.access(it, now) } - - if (token != null && token.expirationTime.isBefore(now) && validScope(scope.logical(), token.scope)) { - println("$TOKEN_PREFIX$currentToken") - } else { - val expirationTimestamp = - if (duration == ChronoUnit.FOREVER.duration) { - Instant.MAX - } else { - try { - now.plus(duration) - } catch (e: Exception) { - throw Exception("Bad token duration: ${e.message}") - } - } - val token = Base32Crockford32B.secureRand() - db.token.create( - username = username, - content = token.raw, - creationTime = now, - expirationTime = expirationTimestamp, - scope = scope, - isRefreshable = refreshable, - description = description, - is2fa = true - ) - println("$TOKEN_PREFIX$token") - } - } - } -} -\ No newline at end of file diff --git a/libeufin-bank/src/main/kotlin/tech/libeufin/bank/cli/DbInit.kt b/libeufin-bank/src/main/kotlin/tech/libeufin/bank/cli/DbInit.kt @@ -1,63 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2024-2025 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.bank.cli - -import com.github.ajalt.clikt.core.CliktCommand -import com.github.ajalt.clikt.core.Context -import com.github.ajalt.clikt.parameters.groups.provideDelegate -import com.github.ajalt.clikt.parameters.options.flag -import com.github.ajalt.clikt.parameters.options.option -import tech.libeufin.bank.bankConfig -import tech.libeufin.bank.createAdminAccount -import tech.libeufin.bank.db.AccountDAO.AccountCreationResult -import tech.libeufin.bank.logger -import tech.libeufin.bank.withDb -import tech.libeufin.common.TalerCmd -import tech.libeufin.common.db.dbInit -import tech.libeufin.common.db.pgDataSource - -class DbInit : TalerCmd("dbinit") { - override fun help(context: Context) = "Initialize the libeufin-bank database" - - private val reset by option( - "--reset", "-r", - help = "Reset database (DANGEROUS: All existing data is lost)" - ).flag() - - override fun run() = cliCmd(logger) { - val cfg = bankConfig(config) - val dbCfg = cfg.dbCfg - pgDataSource(dbCfg.dbConnStr).dbInit(dbCfg, "libeufin-bank", reset) - cfg.withDb { db, cfg -> - // Create admin account if missing - val res = createAdminAccount(db, cfg) - when (res) { - AccountCreationResult.UsernameReuse -> {} - AccountCreationResult.BonusBalanceInsufficient, - AccountCreationResult.UnknownConversionClass -> - throw IllegalStateException("Those error can never happen $res") - AccountCreationResult.PayToReuse -> - throw Exception("Failed to create admin's account") - is AccountCreationResult.Success -> - logger.info("Admin's account created") - } - } - } -} -\ No newline at end of file diff --git a/libeufin-bank/src/main/kotlin/tech/libeufin/bank/cli/EditAccount.kt b/libeufin-bank/src/main/kotlin/tech/libeufin/bank/cli/EditAccount.kt @@ -1,102 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2024-2025 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.bank.cli - -import com.github.ajalt.clikt.core.CliktCommand -import com.github.ajalt.clikt.core.Context -import com.github.ajalt.clikt.parameters.arguments.argument -import com.github.ajalt.clikt.parameters.groups.provideDelegate -import com.github.ajalt.clikt.parameters.options.convert -import com.github.ajalt.clikt.parameters.options.option -import com.github.ajalt.clikt.parameters.types.boolean -import tech.libeufin.bank.* -import tech.libeufin.bank.api.patchAccount -import tech.libeufin.bank.db.AccountDAO.AccountPatchResult -import tech.libeufin.common.* - - -class EditAccount : TalerCmd("edit-account") { - override fun help(context: Context) = "Edit an existing account" - - private val username: String by argument( - "username", - help = "Account username" - ) - private val name: String? by option( - help = "Legal name of the account owner" - ) - private val is_public: Boolean? by option( - "--public", - help = "Make this account visible to anyone" - ).boolean() - private val exchange: Boolean? by option( - help = "Make this account a taler exchange" - ).boolean() - private val email: String? by option(help = "E-Mail address used for TAN transmission") - private val phone: String? by option(help = "Phone number used for TAN transmission") - private val cashout_payto_uri: IbanPayto? by option( - help = "Payto URI of a fiant account who receive cashout amount" - ).convert { Payto.parse(it).expectIban() } - private val debit_threshold: TalerAmount? by option( - help = "Max debit allowed for this account" - ).convert { TalerAmount(it) } - private val tan_channel: Option<TanChannel>? by option( - help = "Enables 2FA and set the TAN channel used for challenges" - ).convert { - if (it == "") { - Option.None - } else { - Option.Some(TanChannel.valueOf(it)) - } - } - - override fun run() = cliCmd(logger) { - bankConfig(config).withDb { db, cfg -> - 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, - tan_channel = Option.invert(tan_channel) - ) - when (val res = patchAccount(db, cfg, 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, - AccountPatchResult.NonAdminConversionRateClass, - AccountPatchResult.UnknownConversionClass, - is AccountPatchResult.Challenges -> - throw IllegalStateException("Those error can never happen $res") - } - } - } -} -\ No newline at end of file diff --git a/libeufin-bank/src/main/kotlin/tech/libeufin/bank/cli/Gc.kt b/libeufin-bank/src/main/kotlin/tech/libeufin/bank/cli/Gc.kt @@ -1,40 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2024-2025 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.bank.cli - -import com.github.ajalt.clikt.core.CliktCommand -import com.github.ajalt.clikt.core.Context -import com.github.ajalt.clikt.parameters.groups.provideDelegate -import tech.libeufin.bank.bankConfig -import tech.libeufin.bank.logger -import tech.libeufin.bank.withDb -import tech.libeufin.common.TalerCmd -import java.time.Instant - -class GC : TalerCmd("gc") { - override fun help(context: Context) = "Run garbage collection: abort expired operations and clean expired data" - - override fun run() = cliCmd(logger) { - bankConfig(config).withDb { db, cfg -> - logger.info("Run garbage collection") - db.gc.collect(Instant.now(), cfg.gcAbortAfter, cfg.gcCleanAfter, cfg.gcDeleteAfter) - } - } -} -\ No newline at end of file diff --git a/libeufin-bank/src/main/kotlin/tech/libeufin/bank/cli/LibeufinBank.kt b/libeufin-bank/src/main/kotlin/tech/libeufin/bank/cli/LibeufinBank.kt @@ -1,36 +0,0 @@ -/* - * 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.bank.cli - -import com.github.ajalt.clikt.core.CliktCommand -import com.github.ajalt.clikt.core.subcommands -import com.github.ajalt.clikt.parameters.options.versionOption -import tech.libeufin.bank.BANK_CONFIG_SOURCE -import tech.libeufin.common.CliConfigCmd -import tech.libeufin.common.VERSION - -class LibeufinBank : CliktCommand() { - init { - versionOption(VERSION) - subcommands(DbInit(), ChangePw(), CreateToken(), Serve(), CreateAccount(), EditAccount(), GC(), BenchPwh(), CliConfigCmd(BANK_CONFIG_SOURCE)) - } - - override fun run() = Unit -} -\ No newline at end of file diff --git a/libeufin-bank/src/main/kotlin/tech/libeufin/bank/cli/Serve.kt b/libeufin-bank/src/main/kotlin/tech/libeufin/bank/cli/Serve.kt @@ -1,69 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2024-2025 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.bank.cli - -import com.github.ajalt.clikt.core.CliktCommand -import com.github.ajalt.clikt.core.Context -import com.github.ajalt.clikt.parameters.groups.provideDelegate -import tech.libeufin.bank.bankConfig -import tech.libeufin.bank.corebankWebApp -import tech.libeufin.bank.logger -import tech.libeufin.bank.withDb -import tech.libeufin.common.TalerCmd -import tech.libeufin.common.TalerConfigError -import tech.libeufin.common.api.serve -import kotlin.io.path.Path -import kotlin.io.path.exists -import kotlin.io.path.readText - -class Serve: TalerCmd("serve") { - override fun help(context: Context) = "Run libeufin-bank HTTP server" - - override fun run() = cliCmd(logger) { - bankConfig(config).withDb { db, cfg -> - if (cfg.allowConversion) { - logger.info("Ensure exchange account exists") - val info = db.account.bankInfo("exchange") - if (info == null) { - throw TalerConfigError.generic("Exchange account missing: an exchange account named 'exchange' is required for conversion to be enabled") - } else if (!info.isTalerExchange) { - throw TalerConfigError.generic("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("${cfg.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("${cfg.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 ? - } - serve(cfg.serverCfg, logger) { - corebankWebApp(db, cfg) - } - } - } -} -\ No newline at end of file diff --git a/libeufin-bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt b/libeufin-bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt @@ -1,807 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2023, 2025, 2024, 2026 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.bank.db - -import tech.libeufin.bank.* -import tech.libeufin.common.* -import tech.libeufin.common.crypto.* -import tech.libeufin.common.db.* -import java.time.Instant -import java.sql.SQLException -import org.postgresql.util.PSQLState - -/** Data access logic for accounts */ -class AccountDAO(private val db: Database) { - /** Result status of account creation */ - sealed interface AccountCreationResult { - data class Success(val payto: String): AccountCreationResult - data object UsernameReuse: AccountCreationResult - data object PayToReuse: AccountCreationResult - data object UnknownConversionClass: AccountCreationResult - data object BonusBalanceInsufficient: AccountCreationResult - } - - /** Create new account */ - suspend fun create( - username: String, - password: String, - name: String, - email: String?, - phone: String?, - cashoutPayto: IbanPayto?, - internalPayto: Payto, - isPublic: Boolean, - isTalerExchange: Boolean, - maxDebt: TalerAmount, - bonus: TalerAmount, - tanChannels: Set<TanChannel>, - // Whether to check [internalPaytoUri] for idempotency - checkPaytoIdempotent: Boolean, - pwCrypto: PwCrypto, - conversionRateClassId: Long? - ): AccountCreationResult = db.serializableTransaction { conn -> - val timestamp = Instant.now() - val idempotent = conn.withStatement(""" - SELECT password_hash, name=? - AND email IS NOT DISTINCT FROM ? - AND phone IS NOT DISTINCT FROM ? - AND cashout_payto IS NOT DISTINCT FROM ? - AND tan_channels = sort_uniq(?::tan_enum[]) - AND (NOT ? OR internal_payto=?) - AND is_public=? - AND is_taler_exchange=? - AND max_debt=(?,?)::taler_amount - AND conversion_rate_class_id IS NOT DISTINCT FROM ? - ,internal_payto, name - FROM customers - JOIN bank_accounts - ON customer_id=owning_customer_id - WHERE username=? - """) { - bind(name) - bind(email) - bind(phone) - bind(cashoutPayto?.simple()) - bind(tanChannels.toTypedArray()) - bind(checkPaytoIdempotent) - bind(internalPayto.canonical) - bind(isPublic) - bind(isTalerExchange) - bind(maxDebt) - bind(conversionRateClassId) - bind(username) - oneOrNull { - Pair( - pwCrypto.checkpw(password, it.getString(1)).match && it.getBoolean(2), - it.getBankPayto("internal_payto", "name", db.ctx) - ) - } - } - - if (idempotent != null) { - if (idempotent.first) { - AccountCreationResult.Success(idempotent.second) - } else { - AccountCreationResult.UsernameReuse - } - } else { - if (internalPayto is IbanPayto) - conn.withStatement (""" - INSERT INTO iban_history( - iban - ,creation_time - ) VALUES (?, ?) - """) { - bind(internalPayto.iban.value) - bind(timestamp) - if (!executeUpdateViolation()) { - conn.rollback() - return@serializableTransaction AccountCreationResult.PayToReuse - } - } - - val customerId = conn.withStatement(""" - INSERT INTO customers ( - username - ,password_hash - ,name - ,email - ,phone - ,cashout_payto - ,tan_channels - ) VALUES (?, ?, ?, ?, ?, ?, sort_uniq(?::tan_enum[])) - RETURNING customer_id - """ - ) { - bind(username) - bind(pwCrypto.hashpw(password)) - bind(name) - bind(email) - bind(phone) - bind(cashoutPayto?.simple()) - bind(tanChannels.toTypedArray()) - one { it.getLong("customer_id") } - } - - conn.withStatement(""" - INSERT INTO bank_accounts( - internal_payto - ,owning_customer_id - ,is_public - ,is_taler_exchange - ,max_debt - ,conversion_rate_class_id - ) VALUES (?, ?, ?, ?, (?, ?)::taler_amount, ?) - """) { - bind(internalPayto.canonical) - bind(customerId) - bind(isPublic) - bind(isTalerExchange) - bind(maxDebt) - bind(conversionRateClassId) - try { - executeUpdate() - } catch (e: SQLException) { - logger.debug(e.message) - if (e.sqlState == PSQLState.UNIQUE_VIOLATION.state) { - conn.rollback() - return@serializableTransaction AccountCreationResult.PayToReuse - } else if (e.sqlState == PSQLState.FOREIGN_KEY_VIOLATION.state) { - conn.rollback() - return@serializableTransaction AccountCreationResult.UnknownConversionClass - } - throw e - } - } - - if (bonus.value != 0L || bonus.frac != 0) { - conn.withStatement(""" - SELECT out_balance_insufficient - FROM bank_transaction(?,'admin','bonus',(?,?)::taler_amount,?,true,NULL,NULL,NULL,NULL, NULL, NULL, NULL) - """) { - bind(internalPayto.canonical) - bind(bonus) - bind(timestamp) - one { - when { - it.getBoolean("out_balance_insufficient") -> { - conn.rollback() - AccountCreationResult.BonusBalanceInsufficient - } - else -> AccountCreationResult.Success(internalPayto.bank(name, db.ctx)) - } - } - } - } else { - AccountCreationResult.Success(internalPayto.bank(name, db.ctx)) - } - } - } - - /** Result status of account deletion */ - enum class AccountDeletionResult { - Success, - UnknownAccount, - BalanceNotZero, - TanRequired - } - - /** Delete account [username] */ - suspend fun delete( - username: String, - is2fa: Boolean - ): AccountDeletionResult = db.serializable( - """ - SELECT - out_not_found, - out_balance_not_zero, - out_tan_required - FROM account_delete(?,?,?) - """ - ) { - bind(username) - bind(Instant.now()) - bind(is2fa) - one { - when { - it.getBoolean("out_not_found") -> AccountDeletionResult.UnknownAccount - it.getBoolean("out_balance_not_zero") -> AccountDeletionResult.BalanceNotZero - it.getBoolean("out_tan_required") -> AccountDeletionResult.TanRequired - else -> AccountDeletionResult.Success - } - } - } - - /** Result status of customer account patch */ - sealed interface AccountPatchResult { - data object UnknownAccount: AccountPatchResult - data object NonAdminName: AccountPatchResult - data object NonAdminCashout: AccountPatchResult - data object NonAdminDebtLimit: AccountPatchResult - data object NonAdminConversionRateClass: AccountPatchResult - data object UnknownConversionClass: AccountPatchResult - data object MissingTanInfo: AccountPatchResult - data class Challenges(val validations: Tans): AccountPatchResult - data object Success: AccountPatchResult - } - - /** Change account [username] information */ - suspend fun reconfig( - username: String, - req: AccountReconfiguration, - isAdmin: Boolean, - is2fa: Boolean, - allowEditName: Boolean, - allowEditCashout: Boolean - ): AccountPatchResult = db.serializableTransaction { conn -> - val name = req.name - val cashoutPayto = req.cashout_payto_uri - val email = req.contact_data?.email ?: Option.None - val phone = req.contact_data?.phone ?: Option.None - val tan_channels = req.channels - val isPublic = req.is_public - val debtLimit = req.debit_threshold - val conversionRateClassId = req.conversion_rate_class_id - val channels = req.channels.get() - - val checkName = !isAdmin && !allowEditName && name != null - val checkCashout = !isAdmin && !allowEditCashout && cashoutPayto.isSome() - val checkDebtLimit = !isAdmin && debtLimit != null - val checkConversionRateClass = !isAdmin && conversionRateClassId.isSome() - - data class CurrentAccount( - val id: Long, - override val channels: Set<TanChannel>, - override val email: String?, - override val phone: String?, - val name: String, - val cashoutPayTo: String?, - val debtLimit: TalerAmount, - val conversionRateClassId: Long? - ): TanInfo - - // Get user ID and current data - val curr = conn.withStatement(""" - SELECT - customer_id, tan_channels, phone, email, name, cashout_payto - ,(max_debt).val AS max_debt_val - ,(max_debt).frac AS max_debt_frac - ,conversion_rate_class_id - FROM customers - JOIN bank_accounts - ON customer_id=owning_customer_id - WHERE username=? AND deleted_at IS NULL - """) { - bind(username) - oneOrNull { - CurrentAccount( - id = it.getLong("customer_id"), - channels = it.getEnumSet<TanChannel>("tan_channels"), - phone = it.getString("phone"), - email = it.getString("email"), - name = it.getString("name"), - cashoutPayTo = it.getOptIbanPayto("cashout_payto")?.simple(), - debtLimit = it.getAmount("max_debt", db.bankCurrency), - conversionRateClassId = it.getOptLong("conversion_rate_class_id"), - ) - } ?: return@serializableTransaction AccountPatchResult.UnknownAccount - } - - // Check tan info - val validations = req.requiredValidation(curr) - - // Check performed 2fa check - if (!isAdmin && !is2fa) { - // Check if mfa is required - if (curr.channels.isNotEmpty()) { - val tans = curr.mfa; - if (tans.size == 1) { - // Perform mfa and validation at the same time - return@serializableTransaction AccountPatchResult.Challenges(validations + tans) - } else { - return@serializableTransaction AccountPatchResult.Challenges(emptyList()) - } - } - - // Check if validation is required - if (validations.isNotEmpty()) { - return@serializableTransaction AccountPatchResult.Challenges(validations) - } - } - - - // Cashout payto without a receiver-name - val simpleCashoutPayto = cashoutPayto.get()?.simple() - - // Check reconfig rights - if (checkName && name != curr.name) - return@serializableTransaction AccountPatchResult.NonAdminName - if (checkCashout && simpleCashoutPayto != curr.cashoutPayTo) - return@serializableTransaction AccountPatchResult.NonAdminCashout - if (checkDebtLimit && debtLimit != curr.debtLimit) - return@serializableTransaction AccountPatchResult.NonAdminDebtLimit - if (checkConversionRateClass && conversionRateClassId.get() != curr.conversionRateClassId) - return@serializableTransaction AccountPatchResult.NonAdminConversionRateClass - - try { - // Update bank info - conn.dynamicUpdate( - "bank_accounts", - sequence { - if (isPublic != null) yield("is_public=?") - if (debtLimit != null) yield("max_debt=(?, ?)::taler_amount") - conversionRateClassId.some { yield("conversion_rate_class_id=?") } - }, - "WHERE owning_customer_id = ?" - ) { - isPublic?.let { bind(it) } - debtLimit?.let { bind(it) } - conversionRateClassId?.some { bind(it) } - bind(curr.id) - } - } catch (e: SQLException) { - logger.debug(e.message) - if (e.sqlState == PSQLState.FOREIGN_KEY_VIOLATION.state) { - conn.rollback() - return@serializableTransaction AccountPatchResult.UnknownConversionClass - } - throw e - } - - // Update customer info - conn.dynamicUpdate( - "customers", - sequence { - cashoutPayto.some { yield("cashout_payto=?") } - phone.some { yield("phone=?") } - email.some { yield("email=?") } - tan_channels.some { yield("tan_channels=sort_uniq(?::tan_enum[])") } - name?.let { yield("name=?") } - }, - "WHERE customer_id = ?" - ) { - cashoutPayto.some { bind(simpleCashoutPayto) } - phone.some { bind(it) } - email.some { bind(it) } - tan_channels.some { bind(it.toTypedArray()) } - name?.let { bind(it) } - bind(curr.id) - } - - // Invalidate current challenges - if (validations.isNotEmpty()) { - conn.withStatement("UPDATE tan_challenges SET expiration_date=0 WHERE customer=?") { - bind(curr.id) - executeUpdate() - } - } - - AccountPatchResult.Success - } - - - /** Result status of customer account auth patch */ - enum class AccountPatchAuthResult { - UnknownAccount, - OldPasswordMismatch, - TanRequired, - Success - } - - /** Change account [username] password to [newPw] if current match [oldPw] */ - suspend fun reconfigPassword( - username: String, - newPw: Password, - oldPw: String?, - is2fa: Boolean, - pwCrypto: PwCrypto - ): AccountPatchAuthResult = db.serializableTransaction { conn -> - val (customerId, currentPwh, tanRequired) = conn.withStatement(""" - SELECT customer_id, password_hash, NOT ? AND cardinality(tan_channels) > 0 - FROM customers WHERE username=? AND deleted_at IS NULL - """) { - bind(is2fa) - bind(username) - oneOrNull { - Triple(it.getLong(1), it.getString(2), it.getBoolean(3)) - } ?: return@serializableTransaction AccountPatchAuthResult.UnknownAccount - } - if (oldPw != null && !pwCrypto.checkpw(oldPw, currentPwh).match) { - AccountPatchAuthResult.OldPasswordMismatch - } else if (tanRequired) { - AccountPatchAuthResult.TanRequired - } else { - val newPwh = pwCrypto.hashpw(newPw.pw) - conn.withStatement("UPDATE customers SET password_hash=?, token_creation_counter=0 WHERE customer_id=?") { - bind(newPwh) - bind(customerId) - executeUpdate() - } - - AccountPatchAuthResult.Success - } - } - - /** Result status of customer account password check */ - sealed interface CheckPasswordResult { - data object UnknownAccount: CheckPasswordResult - data object PasswordMismatch: CheckPasswordResult - data object Locked: CheckPasswordResult - data class Success(val info: BankInfo): CheckPasswordResult - } - - /** Check password of account [username] against [pw], rehashing it if outdated and returning info */ - suspend fun checkPassword(username: String, pw: String, pwCrypto: PwCrypto): CheckPasswordResult { - // Get user current password hash - val res = db.serializable( - """ - SELECT - username, - password_hash, - token_creation_counter, - bank_account_id, - internal_payto, - is_taler_exchange, - name, - tan_channels, - email, - phone - FROM bank_accounts - JOIN customers ON customer_id=owning_customer_id - WHERE username=? AND deleted_at IS NULL - """ - ) { - bind(username) - oneOrNull { - val info = BankInfo( - username = it.getString("username"), - payto = it.getBankPayto("internal_payto", "name", db.ctx), - bankAccountId = it.getLong("bank_account_id"), - isTalerExchange = it.getBoolean("is_taler_exchange"), - channels = it.getEnumSet<TanChannel>("tan_channels"), - phone = it.getString("phone"), - email = it.getString("email") - ) - Triple(info, it.getString("password_hash"), it.getInt("token_creation_counter")) - } - } - if (res == null) return CheckPasswordResult.UnknownAccount - val (info, currentPwh, tokenCreationCounter) = res - - // Check locked - if (tokenCreationCounter >= MAX_TOKEN_CREATION_ATTEMPTS) return CheckPasswordResult.Locked - - // Check password - val check = pwCrypto.checkpw(pw, currentPwh) - if (!check.match) return CheckPasswordResult.PasswordMismatch - - // Reshah if outdated - if (check.outdated) { - val newPwh = pwCrypto.hashpw(pw) - db.serializable( - "UPDATE customers SET password_hash=? where username=? AND password_hash=?" - ) { - bind(newPwh) - bind(username) - bind(currentPwh) - executeUpdate() - } - } - - return CheckPasswordResult.Success(info) - } - - /** Get bank info of account [username] */ - suspend fun bankInfo(username: String): BankInfo? = db.serializable( - """ - SELECT - bank_account_id, - internal_payto, - name, - is_taler_exchange, - tan_channels, - email, - phone - FROM bank_accounts - JOIN customers ON customer_id=owning_customer_id - WHERE username=? - """ - ) { - bind(username) - oneOrNull { - BankInfo( - username = username, - payto = it.getBankPayto("internal_payto", "name", db.ctx), - bankAccountId = it.getLong("bank_account_id"), - isTalerExchange = it.getBoolean("is_taler_exchange"), - channels = it.getEnumSet<TanChannel>("tan_channels"), - phone = it.getString("phone"), - email = it.getString("email") - ) - } - } - - /** Get bank info only when [username] identifies a public account. */ - suspend fun publicBankInfo(username: String): BankInfo? = db.serializable( - """ - SELECT - bank_account_id, - internal_payto, - name, - is_taler_exchange, - tan_channels, - email, - phone - FROM bank_accounts - JOIN customers ON customer_id=owning_customer_id - WHERE username=? AND deleted_at IS NULL AND is_public=true - """ - ) { - bind(username) - oneOrNull { - BankInfo( - username = username, - payto = it.getBankPayto("internal_payto", "name", db.ctx), - bankAccountId = it.getLong("bank_account_id"), - isTalerExchange = it.getBoolean("is_taler_exchange"), - channels = it.getEnumSet<TanChannel>("tan_channels"), - phone = it.getString("phone"), - email = it.getString("email") - ) - } - } - - /** Check bank info of account [payto] */ - suspend fun checkInfo(payto: IbanPayto): AccountInfo? = db.serializable( - """ - SELECT FROM bank_accounts WHERE internal_payto=? - """ - ) { - bind(payto.canonical) - oneOrNull { - AccountInfo() - } - } - - /** Get data of account [username] */ - suspend fun get(username: String): AccountData? = db.serializable( - """ - SELECT - customers.name - ,email - ,phone - ,tan_channels - ,cashout_payto - ,internal_payto - ,(balance).val AS balance_val - ,(balance).frac AS balance_frac - ,has_debt - ,(max_debt).val AS max_debt_val - ,(max_debt).frac AS max_debt_frac - ,is_public - ,is_taler_exchange - ,CASE - WHEN deleted_at IS NOT NULL THEN 'deleted' - WHEN token_creation_counter > ? THEN 'locked' - ELSE 'active' - END as status, - conversion_rate_class_id, - (cashin_ratio).val as cashin_ratio_val, (cashin_ratio).frac as cashin_ratio_frac, - (cashin_fee).val as cashin_fee_val, (cashin_fee).frac as cashin_fee_frac, - (cashin_tiny_amount).val as cashin_tiny_amount_val, (cashin_tiny_amount).frac as cashin_tiny_amount_frac, - (cashin_min_amount).val as cashin_min_amount_val, (cashin_min_amount).frac as cashin_min_amount_frac, - cashin_rounding_mode, - (cashout_ratio).val as cashout_ratio_val, (cashout_ratio).frac as cashout_ratio_frac, - (cashout_fee).val as cashout_fee_val, (cashout_fee).frac as cashout_fee_frac, - (cashout_tiny_amount).val as cashout_tiny_amount_val, (cashout_tiny_amount).frac as cashout_tiny_amount_frac, - (cashout_min_amount).val as cashout_min_amount_val, (cashout_min_amount).frac as cashout_min_amount_frac, - cashout_rounding_mode - FROM customers - JOIN bank_accounts ON customer_id=owning_customer_id - CROSS JOIN LATERAL get_conversion_class_rate(conversion_rate_class_id) - WHERE username=? - """ - ) { - bind(MAX_TOKEN_CREATION_ATTEMPTS) - bind(username) - oneOrNull { - val name = it.getString("name") - val status: AccountStatus = it.getEnum("status") - val isTalerExchange = it.getBoolean("is_taler_exchange") - val channels: Set<TanChannel> = it.getEnumSet("tan_channels") - AccountData( - name = name, - contact_data = ChallengeContactData( - email = Option.Some(it.getString("email")), - phone = Option.Some(it.getString("phone")) - ), - tan_channel = channels.firstOrNull(), - tan_channels = channels, - cashout_payto_uri = it.getOptIbanPayto("cashout_payto")?.full(name), - payto_uri = it.getBankPayto("internal_payto", "name", db.ctx), - balance = Balance( - amount = it.getAmount("balance", db.bankCurrency), - credit_debit_indicator = - if (it.getBoolean("has_debt")) { - CreditDebitInfo.debit - } else { - CreditDebitInfo.credit - } - ), - debit_threshold = it.getAmount("max_debt", db.bankCurrency), - is_public = it.getBoolean("is_public"), - is_taler_exchange = isTalerExchange, - status = status, - is_locked = status == AccountStatus.locked, - conversion_rate = ConversionDAO.userRate(db, it, username,isTalerExchange), - conversion_rate_class_id = it.getOptLong("conversion_rate_class_id") - ) - } - } - - /** Get a page of all public accounts */ - suspend fun pagePublic(params: AccountParams): List<PublicAccount> - = db.page( - params.page, - "bank_account_id", - """ - SELECT - (balance).val AS balance_val, - (balance).frac AS balance_frac, - has_debt, - internal_payto, - username, - is_taler_exchange, - name, - bank_account_id - FROM bank_accounts JOIN customers - ON owning_customer_id = customer_id - WHERE is_public=true AND - ${if (params.usernameFilter != null) "name ILIKE ? AND" else ""} - deleted_at IS NULL AND - """, - { - if (params.usernameFilter != null) { - bind(params.usernameFilter) - } - } - ) { - PublicAccount( - username = it.getString("username"), - row_id = it.getLong("bank_account_id"), - payto_uri = it.getBankPayto("internal_payto", "name", db.ctx), - balance = Balance( - amount = it.getAmount("balance", db.bankCurrency), - credit_debit_indicator = if (it.getBoolean("has_debt")) { - CreditDebitInfo.debit - } else { - CreditDebitInfo.credit - } - ), - is_taler_exchange = it.getBoolean("is_taler_exchange") - ) - } - - /** Get a page of accounts */ - suspend fun pageAdmin(params: AccountParams): List<AccountMinimalData> - = db.page( - params.page, - "bank_account_id", - """ - SELECT - username - ,name - ,(balance).val AS balance_val - ,(balance).frac AS balance_frac - ,has_debt AS balance_has_debt - ,(max_debt).val as max_debt_val - ,(max_debt).frac as max_debt_frac - ,is_public - ,is_taler_exchange - ,internal_payto - ,bank_account_id - ,CASE - WHEN deleted_at IS NOT NULL THEN 'deleted' - WHEN token_creation_counter > ? THEN 'locked' - ELSE 'active' - END as status, - conversion_rate_class_id, - (cashin_ratio).val as cashin_ratio_val, (cashin_ratio).frac as cashin_ratio_frac, - (cashin_fee).val as cashin_fee_val, (cashin_fee).frac as cashin_fee_frac, - (cashin_tiny_amount).val as cashin_tiny_amount_val, (cashin_tiny_amount).frac as cashin_tiny_amount_frac, - (cashin_min_amount).val as cashin_min_amount_val, (cashin_min_amount).frac as cashin_min_amount_frac, - cashin_rounding_mode, - (cashout_ratio).val as cashout_ratio_val, (cashout_ratio).frac as cashout_ratio_frac, - (cashout_fee).val as cashout_fee_val, (cashout_fee).frac as cashout_fee_frac, - (cashout_tiny_amount).val as cashout_tiny_amount_val, (cashout_tiny_amount).frac as cashout_tiny_amount_frac, - (cashout_min_amount).val as cashout_min_amount_val, (cashout_min_amount).frac as cashout_min_amount_frac, - cashout_rounding_mode - FROM bank_accounts - JOIN customers ON owning_customer_id = customer_id - CROSS JOIN LATERAL get_conversion_class_rate(conversion_rate_class_id) - WHERE - ${if (params.usernameFilter != null) "name ILIKE ? AND" else ""} - ${when (params.conversionRateClassId) { - null -> "" - 0L -> "conversion_rate_class_id IS NULL AND" - else -> "conversion_rate_class_id=? AND" - }} - """, - { - bind(MAX_TOKEN_CREATION_ATTEMPTS) - if (params.usernameFilter != null) { - bind(params.usernameFilter) - } - if (params.conversionRateClassId != null && params.conversionRateClassId != 0L) { - bind(params.conversionRateClassId) - } - } - ) { - val status: AccountStatus = it.getEnum("status") - val isTalerExchange = it.getBoolean("is_taler_exchange") - val username = it.getString("username") - AccountMinimalData( - username = username, - row_id = it.getLong("bank_account_id"), - name = it.getString("name"), - balance = Balance( - amount = it.getAmount("balance", db.bankCurrency), - credit_debit_indicator = if (it.getBoolean("balance_has_debt")) { - CreditDebitInfo.debit - } else { - CreditDebitInfo.credit - } - ), - debit_threshold = it.getAmount("max_debt", db.bankCurrency), - is_public = it.getBoolean("is_public"), - is_taler_exchange = isTalerExchange, - payto_uri = it.getBankPayto("internal_payto", "name", db.ctx), - status = status, - is_locked = status == AccountStatus.locked, - conversion_rate = ConversionDAO.userRate(db, it, username, isTalerExchange), - conversion_rate_class_id = it.getOptLong("conversion_rate_class_id") - ) - } -} - -/** List all new tan channels that need to be validated */ -fun AccountReconfiguration.requiredValidation(current: TanInfo): Tans { - // Tan channels are either the new ones or the current one - val channels = this.channels.get() ?: current.channels - - val validated = current.mfa - - return channels.mapNotNull { channel -> - // Info are either the new one or the current ones - val info = when (channel) { - TanChannel.sms -> this.contact_data?.phone?.get() ?: current.phone - TanChannel.email -> this.contact_data?.email?.get() ?: current.email - } - if (info == null) { - throw conflict( - "missing info for tan channel $channel", - TalerErrorCode.BANK_MISSING_TAN_INFO - ) - } - val tan = Pair(channel, info) - // Check if tan is already used and therefore already validated - if (validated.contains(tan)) null else tan - } -} diff --git a/libeufin-bank/src/main/kotlin/tech/libeufin/bank/db/CashoutDAO.kt b/libeufin-bank/src/main/kotlin/tech/libeufin/bank/db/CashoutDAO.kt @@ -1,156 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2023-2025 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.bank.db - -import tech.libeufin.bank.* -import tech.libeufin.common.* -import tech.libeufin.common.db.* -import java.time.Instant - -/** Data access logic for cashout operations */ -class CashoutDAO(private val db: Database) { - /** Result of cashout operation creation */ - sealed interface CashoutCreationResult { - data class Success(val id: Long): CashoutCreationResult - data object UnderMin: CashoutCreationResult - data object BadConversion: CashoutCreationResult - data object AccountNotFound: CashoutCreationResult - data object AccountIsExchange: CashoutCreationResult - data object BalanceInsufficient: CashoutCreationResult - data object RequestUidReuse: CashoutCreationResult - data object NoCashoutPayto: CashoutCreationResult - data object TanRequired: CashoutCreationResult - } - - /** Create a new cashout operation */ - suspend fun create( - username: String, - requestUid: ShortHashCode, - amountDebit: TalerAmount, - amountCredit: TalerAmount, - subject: String, - timestamp: Instant, - is2fa: Boolean - ): CashoutCreationResult = db.serializable( - """ - SELECT - out_bad_conversion, - out_account_not_found, - out_account_is_exchange, - out_balance_insufficient, - out_request_uid_reuse, - out_no_cashout_payto, - out_tan_required, - out_cashout_id, - out_under_min - FROM cashout_create(?,?,(?,?)::taler_amount,(?,?)::taler_amount,?,?,?) - """ - ) { - bind(username) - bind(requestUid) - bind(amountDebit) - bind(amountCredit) - bind(subject) - bind(timestamp) - bind(is2fa) - one { - when { - it.getBoolean("out_under_min") -> CashoutCreationResult.UnderMin - it.getBoolean("out_bad_conversion") -> CashoutCreationResult.BadConversion - it.getBoolean("out_account_not_found") -> CashoutCreationResult.AccountNotFound - it.getBoolean("out_account_is_exchange") -> CashoutCreationResult.AccountIsExchange - it.getBoolean("out_balance_insufficient") -> CashoutCreationResult.BalanceInsufficient - it.getBoolean("out_request_uid_reuse") -> CashoutCreationResult.RequestUidReuse - it.getBoolean("out_no_cashout_payto") -> CashoutCreationResult.NoCashoutPayto - it.getBoolean("out_tan_required") -> CashoutCreationResult.TanRequired - else -> CashoutCreationResult.Success(it.getLong("out_cashout_id")) - } - } - } - - /** Get status of cashout operation [id] owned by [username] */ - suspend fun get(id: Long, username: String): CashoutStatusResponse? = db.serializable( - """ - SELECT - (amount_debit).val as amount_debit_val - ,(amount_debit).frac as amount_debit_frac - ,(amount_credit).val as amount_credit_val - ,(amount_credit).frac as amount_credit_frac - ,cashout_operations.subject - ,creation_time - ,transaction_date as confirmation_date - FROM cashout_operations - JOIN bank_accounts ON bank_account=bank_account_id - JOIN customers ON owning_customer_id=customer_id - LEFT JOIN bank_account_transactions ON local_transaction=bank_transaction_id - WHERE cashout_id=? AND username=? - """ - ) { - bind(id) - bind(username) - oneOrNull { - CashoutStatusResponse( - amount_debit = it.getAmount("amount_debit", db.bankCurrency), - amount_credit = it.getAmount("amount_credit", db.fiatCurrency!!), - subject = it.getString("subject"), - creation_time = it.getTalerTimestamp("creation_time"), - confirmation_time = when (val timestamp = it.getLong("confirmation_date")) { - 0L -> null - else -> TalerTimestamp(timestamp.asInstant()) - }, - ) - } - } - - /** Get a page of all cashout operations */ - suspend fun pageAll(params: PageParams): List<GlobalCashoutInfo> = - db.page(params, "cashout_id", """ - SELECT cashout_id, username - FROM cashout_operations - JOIN bank_accounts ON bank_account=bank_account_id - JOIN customers ON owning_customer_id=customer_id - WHERE - """) { - GlobalCashoutInfo( - cashout_id = it.getLong("cashout_id"), - username = it.getString("username"), - status = CashoutStatus.confirmed - ) - } - - /** Get a page of all cashout operations owned by [username] */ - suspend fun pageForUser(params: PageParams, username: String): List<CashoutInfo> = - db.page(params, "cashout_id", """ - SELECT cashout_id - FROM cashout_operations - WHERE bank_account=( - SELECT bank_account_id - FROM bank_accounts JOIN customers ON owning_customer_id=customer_id - WHERE deleted_at IS NULL AND username = ? - ) AND - """, - args = { bind(username) } - ) { - CashoutInfo( - cashout_id = it.getLong("cashout_id"), - status = CashoutStatus.confirmed - ) - } -} -\ No newline at end of file diff --git a/libeufin-bank/src/main/kotlin/tech/libeufin/bank/db/ConversionDAO.kt b/libeufin-bank/src/main/kotlin/tech/libeufin/bank/db/ConversionDAO.kt @@ -1,462 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2023-2025 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.bank.db - -import tech.libeufin.bank.* -import tech.libeufin.common.* -import tech.libeufin.common.db.* -import java.sql.* -import org.postgresql.util.PSQLState - -/** Data access logic for conversion */ -class ConversionDAO(private val db: Database) { - companion object { - fun userRate(db: Database, it: ResultSet, username: String, isTalerExchange: Boolean): ConversionRate? { - return if (db.fiatCurrency == null) { - null - } else if (username == "admin") { - ConversionRate( - cashin_ratio = DecimalNumber.ZERO, - cashin_fee = TalerAmount.zero(db.bankCurrency), - cashin_tiny_amount = TalerAmount.zero(db.bankCurrency), - cashin_rounding_mode = RoundingMode.zero, - cashin_min_amount = TalerAmount.zero(db.fiatCurrency), - cashout_ratio = DecimalNumber.ZERO, - cashout_fee = TalerAmount.zero(db.fiatCurrency), - cashout_tiny_amount = TalerAmount.zero(db.fiatCurrency), - cashout_rounding_mode = RoundingMode.zero, - cashout_min_amount = TalerAmount.zero(db.bankCurrency), - ) - } else if (isTalerExchange) { - ConversionRate( - cashin_ratio = it.getDecimal("cashin_ratio"), - cashin_fee = it.getAmount("cashin_fee", db.bankCurrency), - cashin_tiny_amount = it.getAmount("cashin_tiny_amount", db.bankCurrency), - cashin_rounding_mode = it.getEnum("cashin_rounding_mode"), - cashin_min_amount = it.getAmount("cashin_min_amount", db.fiatCurrency), - cashout_ratio = DecimalNumber.ZERO, - cashout_fee = TalerAmount.zero(db.fiatCurrency), - cashout_tiny_amount = TalerAmount.zero(db.fiatCurrency), - cashout_rounding_mode = RoundingMode.zero, - cashout_min_amount = TalerAmount.zero(db.bankCurrency), - ) - } else { - ConversionRate( - cashin_ratio = DecimalNumber.ZERO, - cashin_fee = TalerAmount.zero(db.bankCurrency), - cashin_tiny_amount = TalerAmount.zero(db.bankCurrency), - cashin_rounding_mode = RoundingMode.zero, - cashin_min_amount = TalerAmount.zero(db.fiatCurrency), - cashout_ratio = it.getDecimal("cashout_ratio"), - cashout_fee = it.getAmount("cashout_fee", db.fiatCurrency), - cashout_tiny_amount = it.getAmount("cashout_tiny_amount", db.fiatCurrency), - cashout_rounding_mode = it.getEnum("cashout_rounding_mode"), - cashout_min_amount = it.getAmount("cashout_min_amount", db.bankCurrency), - ) - } - } - } - - /** Update in-db conversion config */ - suspend fun updateConfig(cfg: ConversionRate) = db.serializable(""" - CALL config_set_conversion_rate( - (?, ?)::taler_amount, (?, ?)::taler_amount, (?, ?)::taler_amount, (?, ?)::taler_amount, ?::rounding_mode, - (?, ?)::taler_amount, (?, ?)::taler_amount, (?, ?)::taler_amount, (?, ?)::taler_amount, ?::rounding_mode - ) - """) { - bind(cfg.cashin_ratio) - bind(cfg.cashin_fee) - bind(cfg.cashin_tiny_amount) - bind(cfg.cashin_min_amount) - bind(cfg.cashin_rounding_mode) - bind(cfg.cashout_ratio) - bind(cfg.cashout_fee) - bind(cfg.cashout_tiny_amount) - bind(cfg.cashout_min_amount) - bind(cfg.cashout_rounding_mode) - executeUpdate() - } - - /** Get default conversion rate */ - suspend fun getDefaultRate(): ConversionRate = db.serializable(""" - SELECT - (cashin_ratio).val as cashin_ratio_val, (cashin_ratio).frac as cashin_ratio_frac, - (cashin_fee).val as cashin_fee_val, (cashin_fee).frac as cashin_fee_frac, - (cashin_tiny_amount).val as cashin_tiny_amount_val, (cashin_tiny_amount).frac as cashin_tiny_amount_frac, - (cashin_min_amount).val as cashin_min_amount_val, (cashin_min_amount).frac as cashin_min_amount_frac, - cashin_rounding_mode, - (cashout_ratio).val as cashout_ratio_val, (cashout_ratio).frac as cashout_ratio_frac, - (cashout_fee).val as cashout_fee_val, (cashout_fee).frac as cashout_fee_frac, - (cashout_tiny_amount).val as cashout_tiny_amount_val, (cashout_tiny_amount).frac as cashout_tiny_amount_frac, - (cashout_min_amount).val as cashout_min_amount_val, (cashout_min_amount).frac as cashout_min_amount_frac, - cashout_rounding_mode - FROM config_get_conversion_rate() - """) { - one { - ConversionRate( - cashin_ratio = it.getDecimal("cashin_ratio"), - cashin_fee = it.getAmount("cashin_fee", db.bankCurrency), - cashin_tiny_amount = it.getAmount("cashin_tiny_amount", db.bankCurrency), - cashin_rounding_mode = it.getEnum("cashin_rounding_mode"), - cashin_min_amount = it.getAmount("cashin_min_amount", db.fiatCurrency!!), - cashout_ratio = it.getDecimal("cashout_ratio"), - cashout_fee = it.getAmount("cashout_fee", db.fiatCurrency), - cashout_tiny_amount = it.getAmount("cashout_tiny_amount", db.fiatCurrency), - cashout_rounding_mode = it.getEnum("cashout_rounding_mode"), - cashout_min_amount = it.getAmount("cashout_min_amount", db.bankCurrency), - ) - } - } - - /** Get conversion class rate */ - suspend fun getClassRate(conversionRateClassId: Long): ConversionRate = db.serializable(""" - SELECT - (cashin_ratio).val as cashin_ratio_val, (cashin_ratio).frac as cashin_ratio_frac, - (cashin_fee).val as cashin_fee_val, (cashin_fee).frac as cashin_fee_frac, - (cashin_tiny_amount).val as cashin_tiny_amount_val, (cashin_tiny_amount).frac as cashin_tiny_amount_frac, - (cashin_min_amount).val as cashin_min_amount_val, (cashin_min_amount).frac as cashin_min_amount_frac, - cashin_rounding_mode, - (cashout_ratio).val as cashout_ratio_val, (cashout_ratio).frac as cashout_ratio_frac, - (cashout_fee).val as cashout_fee_val, (cashout_fee).frac as cashout_fee_frac, - (cashout_tiny_amount).val as cashout_tiny_amount_val, (cashout_tiny_amount).frac as cashout_tiny_amount_frac, - (cashout_min_amount).val as cashout_min_amount_val, (cashout_min_amount).frac as cashout_min_amount_frac, - cashout_rounding_mode - FROM get_conversion_class_rate(?) - """) { - bind(conversionRateClassId) - one { - ConversionRate( - cashin_ratio = it.getDecimal("cashin_ratio"), - cashin_fee = it.getAmount("cashin_fee", db.bankCurrency), - cashin_tiny_amount = it.getAmount("cashin_tiny_amount", db.bankCurrency), - cashin_rounding_mode = it.getEnum("cashin_rounding_mode"), - cashin_min_amount = it.getAmount("cashin_min_amount", db.fiatCurrency!!), - cashout_ratio = it.getDecimal("cashout_ratio"), - cashout_fee = it.getAmount("cashout_fee", db.fiatCurrency), - cashout_tiny_amount = it.getAmount("cashout_tiny_amount", db.fiatCurrency), - cashout_rounding_mode = it.getEnum("cashout_rounding_mode"), - cashout_min_amount = it.getAmount("cashout_min_amount", db.bankCurrency), - ) - } - } - - /** Get user rate */ - suspend fun getUserRate(username: String): Pair<Boolean, ConversionRate> = db.serializable(""" - SELECT - (cashin_ratio).val as cashin_ratio_val, (cashin_ratio).frac as cashin_ratio_frac, - (cashin_fee).val as cashin_fee_val, (cashin_fee).frac as cashin_fee_frac, - (cashin_tiny_amount).val as cashin_tiny_amount_val, (cashin_tiny_amount).frac as cashin_tiny_amount_frac, - (cashin_min_amount).val as cashin_min_amount_val, (cashin_min_amount).frac as cashin_min_amount_frac, - cashin_rounding_mode, - (cashout_ratio).val as cashout_ratio_val, (cashout_ratio).frac as cashout_ratio_frac, - (cashout_fee).val as cashout_fee_val, (cashout_fee).frac as cashout_fee_frac, - (cashout_tiny_amount).val as cashout_tiny_amount_val, (cashout_tiny_amount).frac as cashout_tiny_amount_frac, - (cashout_min_amount).val as cashout_min_amount_val, (cashout_min_amount).frac as cashout_min_amount_frac, - cashout_rounding_mode, - is_taler_exchange - FROM bank_accounts - JOIN customers ON customer_id=owning_customer_id - CROSS JOIN LATERAL get_conversion_class_rate(conversion_rate_class_id) - WHERE username=? - """) { - bind(username) - one { - val isTalerExchange = it.getBoolean("is_taler_exchange") - val rate = ConversionDAO.userRate(db, it, username, isTalerExchange)!! - Pair(isTalerExchange, rate) - } - } - - /** Clear in-db conversion config */ - suspend fun clearConfig() = db.serializable( - "DELETE FROM config WHERE key LIKE 'cashin%' OR key like 'cashout%'" - ) { - executeUpdate() - } - - /** Result of conversions operations */ - sealed interface ConversionResult { - data class Success(val converted: TalerAmount): ConversionResult - data object ToSmall: ConversionResult - data object IsExchange: ConversionResult - data object NotExchange: ConversionResult - } - - /** Perform [direction] conversion of [amount] using in-db [function] */ - private suspend fun conversion(amount: TalerAmount, function: String, direction: String, conversionRateClassId: Long?): ConversionResult = db.serializable( - "SELECT too_small, (converted).val AS amount_val, (converted).frac AS amount_frac FROM conversion_$function((?, ?)::taler_amount, ?, ?)" - ) { - bind(amount) - bind(direction) - bind(conversionRateClassId) - one { - when { - it.getBoolean("too_small") -> ConversionResult.ToSmall - else -> ConversionResult.Success( - it.getAmount("amount", if (amount.currency == db.bankCurrency) db.fiatCurrency!! else db.bankCurrency) - ) - } - } - } - private suspend fun userConversion(amount: TalerAmount, function: String, direction: String, username: String): ConversionResult = db.serializable( - """ - SELECT - is_taler_exchange, - too_small, - (converted).val AS amount_val, - (converted).frac AS amount_frac - FROM bank_accounts - JOIN customers ON customer_id=owning_customer_id, - LATERAL conversion_$function((?, ?)::taler_amount, ?, conversion_rate_class_id) - WHERE username=? - """ - ) { - bind(amount) - bind(direction) - bind(username) - one { - val isExchange = it.getBoolean("is_taler_exchange") - when { - direction == "cashout" && isExchange -> ConversionResult.IsExchange - direction == "cashin" && !isExchange -> ConversionResult.NotExchange - it.getBoolean("too_small") -> ConversionResult.ToSmall - else -> ConversionResult.Success( - it.getAmount("amount", if (amount.currency == db.bankCurrency) db.fiatCurrency!! else db.bankCurrency) - ) - } - } - } - - /** Convert [regional] amount to fiat using cashout rate */ - suspend fun defaultToCashout(regional: TalerAmount): ConversionResult = conversion(regional, "to", "cashout", null) - suspend fun classToCashout(id: Long, regional: TalerAmount): ConversionResult = conversion(regional, "to", "cashout", id) - suspend fun userToCashout(username: String, regional: TalerAmount): ConversionResult = userConversion(regional, "to", "cashout", username) - /** Convert [fiat] amount to regional using cashin rate */ - suspend fun defaultToCashin(fiat: TalerAmount): ConversionResult = conversion(fiat, "to", "cashin", null) - suspend fun classToCashin(id: Long, fiat: TalerAmount): ConversionResult = conversion(fiat, "to", "cashin", id) - suspend fun userToCashin(username: String, fiat: TalerAmount): ConversionResult = userConversion(fiat, "to", "cashin", username) - /** Convert [fiat] amount to regional using inverse cashout rate */ - suspend fun defaultFromCashout(fiat: TalerAmount): ConversionResult = conversion(fiat, "from", "cashout", null) - suspend fun classFromCashout(id: Long, fiat: TalerAmount): ConversionResult = conversion(fiat, "from", "cashout", id) - suspend fun userFromCashout(username: String, fiat: TalerAmount): ConversionResult = userConversion(fiat, "from", "cashout", username) - /** Convert [regional] amount to fiat using inverse cashin rate */ - suspend fun defaultFromCashin(regional: TalerAmount): ConversionResult = conversion(regional, "from", "cashin", null) - suspend fun classFromCashin(id: Long, regional: TalerAmount): ConversionResult = conversion(regional, "from", "cashin", id) - suspend fun userFromCashin(username: String, regional: TalerAmount): ConversionResult = userConversion(regional, "from", "cashin", username) - - /** Result status of conversion rate class creation */ - sealed interface ClassCreateResult { - data class Success(val id: Long): ClassCreateResult - data object NameReuse: ClassCreateResult - } - - /** Create a new conversion rate class */ - suspend fun createClass( - input: ConversionRateClassInput - ): ClassCreateResult = db.serializable( - """ - INSERT INTO conversion_rate_classes ( - name - ,description - ,cashin_ratio - ,cashin_fee - ,cashin_min_amount - ,cashin_rounding_mode - ,cashout_ratio - ,cashout_fee - ,cashout_min_amount - ,cashout_rounding_mode - ) VALUES ( - ?, ?, - ${optDecimal(input.cashin_ratio)}, - ${optAmount(input.cashin_fee)}, - ${optAmount(input.cashin_min_amount)}, - ?::rounding_mode, - ${optDecimal(input.cashout_ratio)}, - ${optAmount(input.cashout_fee)}, - ${optAmount(input.cashout_min_amount)}, - ?::rounding_mode - ) - RETURNING conversion_rate_class_id - """ - ) { - bind(input.name) - bind(input.description) - bind(input.cashin_ratio) - bind(input.cashin_fee) - bind(input.cashin_min_amount) - bind(input.cashin_rounding_mode) - bind(input.cashout_ratio) - bind(input.cashout_fee) - bind(input.cashout_min_amount) - bind(input.cashout_rounding_mode) - try { - one { - ClassCreateResult.Success(it.getLong("conversion_rate_class_id")) - } - } catch (e: SQLException) { - if (e.sqlState == PSQLState.UNIQUE_VIOLATION.state) ClassCreateResult.NameReuse - else throw e - } - - } - - /** Result status of conversion rate class patching */ - enum class ClassPatchResult { - Success, - Unknown, - NameReuse - } - - /** Patch a conversion rate class */ - suspend fun patchClass( - id: Long, - input: ConversionRateClassInput - ): ClassPatchResult = db.serializable( - """ - UPDATE conversion_rate_classes SET - name=? - ,description=? - ,cashin_ratio=${optDecimal(input.cashin_ratio)} - ,cashin_fee=${optAmount(input.cashin_fee)} - ,cashin_min_amount=${optAmount(input.cashin_min_amount)} - ,cashin_rounding_mode=?::rounding_mode - ,cashout_ratio=${optDecimal(input.cashout_ratio)} - ,cashout_fee=${optAmount(input.cashout_fee)} - ,cashout_min_amount=${optAmount(input.cashout_min_amount)} - ,cashout_rounding_mode=?::rounding_mode - WHERE conversion_rate_class_id=? - """ - ) { - bind(input.name) - bind(input.description) - bind(input.cashin_ratio) - bind(input.cashin_fee) - bind(input.cashin_min_amount) - bind(input.cashin_rounding_mode) - bind(input.cashout_ratio) - bind(input.cashout_fee) - bind(input.cashout_min_amount) - bind(input.cashout_rounding_mode) - bind(id) - try { - if (executeUpdateCheck()) { - ClassPatchResult.Success - } else { - ClassPatchResult.Unknown - } - } catch (e: SQLException) { - if (e.sqlState == PSQLState.UNIQUE_VIOLATION.state) ClassPatchResult.NameReuse - else throw e - } - } - - /** Delete a conversion rate class */ - suspend fun deleteClass( - id: Long - ): Boolean = db.serializable( - "DELETE FROM conversion_rate_classes WHERE conversion_rate_class_id=?" - ) { - bind(id) - executeUpdateCheck() - } - - /** Get conversion rate class [id] */ - suspend fun getClass(id: Long): ConversionRateClass? = db.serializable( - """ - SELECT - name - ,description - ,(cashin_ratio).val as cashin_ratio_val, (cashin_ratio).frac as cashin_ratio_frac - ,(cashin_fee).val as cashin_fee_val, (cashin_fee).frac as cashin_fee_frac - ,(cashin_min_amount).val as cashin_min_amount_val, (cashin_min_amount).frac as cashin_min_amount_frac - ,cashin_rounding_mode - ,(cashout_ratio).val as cashout_ratio_val, (cashout_ratio).frac as cashout_ratio_frac - ,(cashout_fee).val as cashout_fee_val, (cashout_fee).frac as cashout_fee_frac - ,(cashout_min_amount).val as cashout_min_amount_val, (cashout_min_amount).frac as cashout_min_amount_frac - ,cashout_rounding_mode - ,(SELECT count(*) FROM bank_accounts WHERE bank_accounts.conversion_rate_class_id=conversion_rate_classes.conversion_rate_class_id) as num_users - FROM conversion_rate_classes - WHERE conversion_rate_class_id=? - """ - ) { - bind(id) - oneOrNull { - ConversionRateClass( - name = it.getString("name"), - description = it.getString("description"), - conversion_rate_class_id = id, - num_users = it.getInt("num_users"), - cashin_ratio = it.getOptDecimal("cashin_ratio"), - cashin_fee = it.getOptAmount("cashin_fee", db.bankCurrency), - cashin_rounding_mode = it.getOptEnum<RoundingMode>("cashin_rounding_mode"), - cashin_min_amount = it.getOptAmount("cashin_min_amount", db.fiatCurrency!!), - cashout_ratio = it.getOptDecimal("cashout_ratio"), - cashout_fee = it.getOptAmount("cashout_fee", db.fiatCurrency), - cashout_rounding_mode = it.getOptEnum<RoundingMode>("cashout_rounding_mode"), - cashout_min_amount = it.getOptAmount("cashout_min_amount", db.bankCurrency), - ) - } - } - - /** Get a page of conversion rate classes */ - suspend fun pageClass(params: ClassParams): List<ConversionRateClass> - = db.page( - params.page, - "conversion_rate_class_id", - """ - SELECT - name - ,description - ,(cashin_ratio).val as cashin_ratio_val, (cashin_ratio).frac as cashin_ratio_frac - ,(cashin_fee).val as cashin_fee_val, (cashin_fee).frac as cashin_fee_frac - ,(cashin_min_amount).val as cashin_min_amount_val, (cashin_min_amount).frac as cashin_min_amount_frac - ,cashin_rounding_mode - ,(cashout_ratio).val as cashout_ratio_val, (cashout_ratio).frac as cashout_ratio_frac - ,(cashout_fee).val as cashout_fee_val, (cashout_fee).frac as cashout_fee_frac - ,(cashout_min_amount).val as cashout_min_amount_val, (cashout_min_amount).frac as cashout_min_amount_frac - ,cashout_rounding_mode - ,(SELECT count(*) FROM bank_accounts WHERE bank_accounts.conversion_rate_class_id=conversion_rate_classes.conversion_rate_class_id) as num_users - ,conversion_rate_class_id - FROM conversion_rate_classes - WHERE ${if (params.nameFilter != null) "name ILIKE ? AND" else ""} - """, - { - if (params.nameFilter != null) { - bind(params.nameFilter) - } - } - ) { - ConversionRateClass( - name = it.getString("name"), - description = it.getString("description"), - conversion_rate_class_id = it.getLong("conversion_rate_class_id"), - num_users = it.getInt("num_users"), - cashin_ratio = it.getOptDecimal("cashin_ratio"), - cashin_fee = it.getOptAmount("cashin_fee", db.bankCurrency), - cashin_rounding_mode = it.getOptEnum<RoundingMode>("cashin_rounding_mode"), - cashin_min_amount = it.getOptAmount("cashin_min_amount", db.fiatCurrency!!), - cashout_ratio = it.getOptDecimal("cashout_ratio"), - cashout_fee = it.getOptAmount("cashout_fee", db.fiatCurrency), - cashout_rounding_mode = it.getOptEnum<RoundingMode>("cashout_rounding_mode"), - cashout_min_amount = it.getOptAmount("cashout_min_amount", db.bankCurrency), - ) - } -} -\ No newline at end of file diff --git a/libeufin-bank/src/main/kotlin/tech/libeufin/bank/db/Database.kt b/libeufin-bank/src/main/kotlin/tech/libeufin/bank/db/Database.kt @@ -1,174 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2023, 2024, 2025, 2026 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.bank.db - -import kotlinx.coroutines.flow.Flow -import org.slf4j.Logger -import org.slf4j.LoggerFactory -import tech.libeufin.bank.* -import tech.libeufin.common.db.* -import tech.libeufin.common.* -import java.util.* -import java.util.concurrent.ConcurrentHashMap - -private val logger: Logger = LoggerFactory.getLogger("libeufin-bank-db") - -class Database( - dbConfig: DatabaseConfig, - internal val bankCurrency: String, - internal val fiatCurrency: String?, - internal val ctx: BankPaytoCtx -): DbPool(dbConfig, "libeufin-bank") { - // DAOs - val cashout = CashoutDAO(this) - val withdrawal = WithdrawalDAO(this) - val exchange = ExchangeDAO(this) - val conversion = ConversionDAO(this) - val account = AccountDAO(this) - val transaction = TransactionDAO(this) - val token = TokenDAO(this) - val tan = TanDAO(this) - val gc = GcDAO(this) - val transfer = TransferDAO(this) - - // Transaction flows, the keys are the bank account id - private val bankTxFlows = ConcurrentHashMap<Long, CountedSharedFlow<Long>>() - private val outgoingTxFlows = ConcurrentHashMap<Long, CountedSharedFlow<Long>>() - private val incomingTxFlows = ConcurrentHashMap<Long, CountedSharedFlow<Long>>() - private val revenueTxFlows = ConcurrentHashMap<Long, CountedSharedFlow<Long>>() - // Withdrawal confirmation flow, the key is the public withdrawal UUID - private val withdrawalFlow = ConcurrentHashMap<UUID, CountedSharedFlow<WithdrawalStatus>>() - - private val notificationWatcher = - watchNotifications(pgSource, "libeufin_bank", LoggerFactory.getLogger("libeufin-bank-db-watcher"), mapOf( - "bank_tx" to { - val (debtor, creditor, debitRow, creditRow) = it.split(' ', limit = 4).map { it.toLong() } - bankTxFlows[debtor]?.run { - flow.emit(debitRow) - } - bankTxFlows[creditor]?.run { - flow.emit(creditRow) - } - revenueTxFlows[creditor]?.run { - flow.emit(creditRow) - } - }, - "bank_outgoing_tx" to { - val (account, merchant, debitRow, creditRow) = it.split(' ', limit = 4).map { it.toLong() } - outgoingTxFlows[account]?.run { - flow.emit(debitRow) - } - }, - "bank_incoming_tx" to { - val (account, row) = it.split(' ', limit = 2).map { it.toLong() } - incomingTxFlows[account]?.run { - flow.emit(row) - } - }, - "bank_withdrawal_status" to { - val raw = it.split(' ', limit = 2) - val uuid = UUID.fromString(raw[0]) - val status = WithdrawalStatus.valueOf(raw[1]) - withdrawalFlow[uuid]?.run { - flow.emit(status) - } - } - )) - - override fun close() { - try { - notificationWatcher.close() - } finally { - super.close() - } - } - - /** Listen for new bank transactions for [account] */ - suspend fun <R> listenBank(account: Long, lambda: suspend (Flow<Long>) -> R): R - = listen(bankTxFlows, account, lambda) - /** Listen for new taler outgoing transactions from [exchange] */ - suspend fun <R> listenOutgoing(exchange: Long, lambda: suspend (Flow<Long>) -> R): R - = listen(outgoingTxFlows, exchange, lambda) - /** Listen for new taler incoming transactions to [exchange] */ - suspend fun <R> listenIncoming(exchange: Long, lambda: suspend (Flow<Long>) -> R): R - = listen(incomingTxFlows, exchange, lambda) - /** Listen for new incoming transactions to [merchant] */ - suspend fun <R> listenRevenue(merchant: Long, lambda: suspend (Flow<Long>) -> R): R - = listen(revenueTxFlows, merchant, lambda) - /** Listen for new withdrawal confirmations */ - suspend fun <R> listenWithdrawals(withdrawal: UUID, lambda: suspend (Flow<WithdrawalStatus>) -> R): R - = listen(withdrawalFlow, withdrawal, lambda) - - suspend fun monitor( - params: MonitorParams - ): MonitorResponse = serializable( - """ - SELECT - cashin_count - ,(cashin_regional_volume).val as cashin_regional_volume_val - ,(cashin_regional_volume).frac as cashin_regional_volume_frac - ,(cashin_fiat_volume).val as cashin_fiat_volume_val - ,(cashin_fiat_volume).frac as cashin_fiat_volume_frac - ,cashout_count - ,(cashout_regional_volume).val as cashout_regional_volume_val - ,(cashout_regional_volume).frac as cashout_regional_volume_frac - ,(cashout_fiat_volume).val as cashout_fiat_volume_val - ,(cashout_fiat_volume).frac as cashout_fiat_volume_frac - ,taler_in_count - ,(taler_in_volume).val as taler_in_volume_val - ,(taler_in_volume).frac as taler_in_volume_frac - ,taler_out_count - ,(taler_out_volume).val as taler_out_volume_val - ,(taler_out_volume).frac as taler_out_volume_frac - FROM stats_get_frame(?::timestamp, ?::stat_timeframe_enum) - """ - ) { - bind(params.date) - bind(params.timeframe) - oneOrNull { - fiatCurrency?.run { - MonitorWithConversion( - cashinCount = it.getLong("cashin_count"), - cashinRegionalVolume = it.getAmount("cashin_regional_volume", bankCurrency), - cashinFiatVolume = it.getAmount("cashin_fiat_volume", this), - cashoutCount = it.getLong("cashout_count"), - cashoutRegionalVolume = it.getAmount("cashout_regional_volume", bankCurrency), - cashoutFiatVolume = it.getAmount("cashout_fiat_volume", this), - talerInCount = it.getLong("taler_in_count"), - talerInVolume = it.getAmount("taler_in_volume", bankCurrency), - talerOutCount = it.getLong("taler_out_count"), - talerOutVolume = it.getAmount("taler_out_volume", bankCurrency), - ) - } ?: MonitorNoConversion( - talerInCount = it.getLong("taler_in_count"), - talerInVolume = it.getAmount("taler_in_volume", bankCurrency), - talerOutCount = it.getLong("taler_out_count"), - talerOutVolume = it.getAmount("taler_out_volume", bankCurrency), - ) - } ?: throw internalServerError("No result from DB procedure stats_get_frame") - } -} - -/** Result status of withdrawal or cashout operation abortion */ -enum class AbortResult { - Success, - UnknownOperation, - AlreadyConfirmed -} diff --git a/libeufin-bank/src/main/kotlin/tech/libeufin/bank/db/ExchangeDAO.kt b/libeufin-bank/src/main/kotlin/tech/libeufin/bank/db/ExchangeDAO.kt @@ -1,320 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2023, 2024, 2025, 2026 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.bank.db - -import tech.libeufin.common.* -import tech.libeufin.common.db.* -import java.time.Instant - -/** Data access logic for exchange specific logic */ -class ExchangeDAO(private val db: Database) { - /** Query [exchangeId] history of taler incoming transactions */ - suspend fun incomingHistory( - params: HistoryParams, - exchangeId: Long - ): List<IncomingBankTransaction> - = db.poolHistory(params, exchangeId, db::listenIncoming, """ - SELECT - bank_transaction_id - ,transaction_date - ,(amount).val AS amount_val - ,(amount).frac AS amount_frac - ,debtor_payto - ,debtor_name - ,type - ,metadata - ,authorization_pub - ,authorization_sig - FROM taler_exchange_incoming AS tfr - JOIN bank_account_transactions AS txs - ON bank_transaction=txs.bank_transaction_id - WHERE - """) { - val type = it.getEnum<IncomingType>("type") - when (type) { - IncomingType.reserve -> IncomingReserveTransaction( - row_id = it.getLong("bank_transaction_id"), - date = it.getTalerTimestamp("transaction_date"), - amount = it.getAmount("amount", db.bankCurrency), - debit_account = it.getBankPayto("debtor_payto", "debtor_name", db.ctx), - reserve_pub = EddsaPublicKey(it.getBytes("metadata")), - authorization_pub = it.getOptKey("authorization_pub"), - authorization_sig = it.getOptSig("authorization_sig") - ) - IncomingType.kyc -> IncomingKycAuthTransaction( - row_id = it.getLong("bank_transaction_id"), - date = it.getTalerTimestamp("transaction_date"), - amount = it.getAmount("amount", db.bankCurrency), - debit_account = it.getBankPayto("debtor_payto", "debtor_name", db.ctx), - account_pub = EddsaPublicKey(it.getBytes("metadata")), - authorization_pub = it.getOptKey("authorization_pub"), - authorization_sig = it.getOptSig("authorization_sig") - ) - IncomingType.map -> throw UnsupportedOperationException() - } - } - - /** Query [exchangeId] history of taler outgoing transactions */ - suspend fun outgoingHistory( - params: HistoryParams, - exchangeId: Long - ): List<OutgoingTransaction> - = db.poolHistory(params, exchangeId, db::listenOutgoing, """ - SELECT - bank_transaction_id - ,transaction_date - ,(txs.amount).val AS amount_val - ,(txs.amount).frac AS amount_frac - ,txs.creditor_payto - ,txs.creditor_name - ,wtid - ,exchange_base_url - ,transfer_operations.metadata - FROM taler_exchange_outgoing AS tfr - JOIN transfer_operations USING (exchange_outgoing_id) - JOIN bank_account_transactions AS txs - ON bank_transaction=txs.bank_transaction_id - WHERE - """) { - OutgoingTransaction( - row_id = it.getLong("bank_transaction_id"), - date = it.getTalerTimestamp("transaction_date"), - amount = it.getAmount("amount", db.bankCurrency), - credit_account = it.getBankPayto("creditor_payto", "creditor_name", db.ctx), - wtid = ShortHashCode(it.getBytes("wtid")), - exchange_base_url = it.getString("exchange_base_url"), - metadata = it.getString("metadata"), - debit_fee = null - ) - } - - /** Result of taler transfer transaction creation */ - sealed interface TransferResult { - /** Transaction [id] and wire transfer [timestamp] */ - data class Success(val id: Long, val timestamp: TalerTimestamp): TransferResult - data object NotAnExchange: TransferResult - data object UnknownExchange: TransferResult - data object BothPartyAreExchange: TransferResult - data object BalanceInsufficient: TransferResult - data object ReserveUidReuse: TransferResult - data object WtidReuse: TransferResult - data object AdminCreditor: TransferResult - } - - /** Perform a Taler transfer */ - suspend fun transfer( - req: TransferRequest, - username: String, - timestamp: Instant, - conversion: Boolean - ): TransferResult = db.serializable( - """ - SELECT - out_debtor_not_found - ,out_debtor_not_exchange - ,out_both_exchanges - ,out_request_uid_reuse - ,out_wtid_reuse - ,out_exchange_balance_insufficient - ,out_creditor_admin - ,out_tx_row_id - ,out_timestamp - FROM - taler_transfer ( - ?, ?, ?, - (?,?)::taler_amount, - ?, ?, ?, ?, ?, ? - ); - """ - ) { - val subject = fmtOutgoingSubject(req.wtid, req.exchange_base_url, req.metadata) - - bind(req.request_uid) - bind(req.wtid) - bind(subject) - bind(req.amount) - bind(req.exchange_base_url.url.toString()) - bind(req.metadata) - bind(req.credit_account.canonical) - bind(username) - bind(timestamp) - bind(conversion) - one { - when { - it.getBoolean("out_debtor_not_found") -> TransferResult.UnknownExchange - it.getBoolean("out_debtor_not_exchange") -> TransferResult.NotAnExchange - it.getBoolean("out_both_exchanges") -> TransferResult.BothPartyAreExchange - it.getBoolean("out_exchange_balance_insufficient") -> TransferResult.BalanceInsufficient - it.getBoolean("out_request_uid_reuse") -> TransferResult.ReserveUidReuse - it.getBoolean("out_wtid_reuse") -> TransferResult.WtidReuse - it.getBoolean("out_creditor_admin") -> TransferResult.AdminCreditor - else -> TransferResult.Success( - id = it.getLong("out_tx_row_id"), - timestamp = it.getTalerTimestamp("out_timestamp") - ) - } - } - } - - /** Get status of transfer [txId] of account [exchangeId] */ - suspend fun getTransfer( - exchangeId: Long, - txId: Long - ): TransferStatus? = db.serializable( - """ - SELECT - wtid - ,exchange_base_url - ,metadata - ,transfer_date - ,(amount).val AS amount_val - ,(amount).frac AS amount_frac - ,creditor_payto - ,status - ,status_msg - FROM transfer_operations - WHERE transfer_operation_id=? AND exchange_id=? - """ - ) { - bind(txId) - bind(exchangeId) - oneOrNull { - TransferStatus( - status = it.getEnum<TransferStatusState>("status"), - status_msg = it.getString("status_msg"), - amount = it.getAmount("amount", db.bankCurrency), - origin_exchange_url = it.getString("exchange_base_url"), - metadata = it.getString("metadata"), - wtid = ShortHashCode(it.getBytes("wtid")), - credit_account = it.getBankPayto("creditor_payto", null, db.ctx), - timestamp = it.getTalerTimestamp("transfer_date"), - ) - } - } - - /** Get a page of transfers status of account [exchangeId] */ - suspend fun pageTransfer( - params: PageParams, - exchangeId: Long, - status: TransferStatusState? - ): List<TransferListStatus> = db.page( - params, - "transfer_operation_id", - """ - SELECT - transfer_operation_id - ,transfer_date - ,(amount).val AS amount_val - ,(amount).frac AS amount_frac - ,creditor_payto - ,status - FROM transfer_operations - WHERE exchange_id=? AND ${ - when (status) { - null -> "" - else -> "status=?::transfer_status AND" - } - } - """, - { - bind(exchangeId) - if (status != null) { - bind(status) - } - } - ) { - TransferListStatus( - row_id = it.getLong("transfer_operation_id"), - status = it.getEnum<TransferStatusState>("status"), - amount = it.getAmount("amount", db.bankCurrency), - credit_account = it.getBankPayto("creditor_payto", null, db.ctx), - timestamp = it.getTalerTimestamp("transfer_date"), - ) - } - - /** Result of taler add incoming transaction creation */ - sealed interface AddIncomingResult { - /** Transaction [id] and wire transfer [timestamp] */ - data class Success(val id: Long, val timestamp: TalerTimestamp, val pending: Boolean): AddIncomingResult - data object NotAnExchange: AddIncomingResult - data object UnknownExchange: AddIncomingResult - data object UnknownDebtor: AddIncomingResult - data object BothPartyAreExchange: AddIncomingResult - data object ReservePubReuse: AddIncomingResult - data object UnknownMapping: AddIncomingResult - data object MappingReuse: AddIncomingResult - data object BalanceInsufficient: AddIncomingResult - } - - /** Add a new taler incoming transaction */ - suspend fun addIncoming( - amount: TalerAmount, - debitAccount: Payto, - subject: String, - username: String, - timestamp: Instant, - metadata: IncomingSubject - ): AddIncomingResult = db.serializable( - """ - SELECT - out_creditor_not_found - ,out_creditor_not_exchange - ,out_debtor_not_found - ,out_both_exchanges - ,out_reserve_pub_reuse - ,out_mapping_reuse - ,out_unknown_mapping - ,out_debitor_balance_insufficient - ,out_tx_row_id - ,out_pending - FROM - taler_add_incoming ( - ?, ?, (?,?)::taler_amount, - ?, ?, ?, ?::taler_incoming_type - ); - """ - ) { - bind(metadata.key) - bind(subject) - bind(amount) - bind(debitAccount.canonical) - bind(username) - bind(timestamp) - bind(metadata.type) - - one { - when { - it.getBoolean("out_creditor_not_found") -> AddIncomingResult.UnknownExchange - it.getBoolean("out_creditor_not_exchange") -> AddIncomingResult.NotAnExchange - it.getBoolean("out_debtor_not_found") -> AddIncomingResult.UnknownDebtor - it.getBoolean("out_both_exchanges") -> AddIncomingResult.BothPartyAreExchange - it.getBoolean("out_debitor_balance_insufficient") -> AddIncomingResult.BalanceInsufficient - it.getBoolean("out_reserve_pub_reuse") -> AddIncomingResult.ReservePubReuse - it.getBoolean("out_mapping_reuse") -> AddIncomingResult.MappingReuse - it.getBoolean("out_unknown_mapping") -> AddIncomingResult.UnknownMapping - else -> AddIncomingResult.Success( - id = it.getLong("out_tx_row_id"), - timestamp = TalerTimestamp(timestamp), - pending = it.getBoolean("out_pending") - ) - } - } - } -} -\ No newline at end of file diff --git a/libeufin-bank/src/main/kotlin/tech/libeufin/bank/db/GcDAO.kt b/libeufin-bank/src/main/kotlin/tech/libeufin/bank/db/GcDAO.kt @@ -1,86 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2024, 2025, 2026 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.bank.db - -import tech.libeufin.common.db.withStatement -import tech.libeufin.common.micros -import java.time.Duration -import java.time.Instant - -/** Data access logic for garbage collection */ -class GcDAO(private val db: Database) { - /** Run garbage collection */ - suspend fun collect( - timestamp: Instant, - abortAfter: Duration, - cleanAfter: Duration, - deleteAfter: Duration - ) = db.conn { conn -> - val abortAfterMicro = timestamp.minus(abortAfter).micros() - val cleanAfterMicro = timestamp.minus(cleanAfter).micros() - val deleteAfterMicro = timestamp.minus(deleteAfter).micros() - - // Abort pending operations - conn.withStatement( - """ - UPDATE taler_withdrawal_operations SET aborted = true WHERE creation_date < ? AND NOT EXISTS( - SELECT FROM prepared_transfers JOIN taler_withdrawal_operations USING (withdrawal_id) - ) - """ - ) { - bind(abortAfterMicro) - executeUpdate() - } - - // Clean aborted operations, expired challenges and expired tokens - for (smt in listOf( - """DELETE FROM taler_withdrawal_operations WHERE aborted = true AND creation_date < ? AND NOT EXISTS( - SELECT FROM prepared_transfers JOIN taler_withdrawal_operations USING (withdrawal_id) - )""", - "DELETE FROM tan_challenges WHERE expiration_date < ?", - "DELETE FROM bearer_tokens WHERE expiration_time < ?" - )) { - conn.withStatement(smt) { - bind(cleanAfterMicro) - executeUpdate() - } - } - - // Delete old bank transactions, linked operations are deleted by CASCADE - conn.withStatement( - "DELETE FROM bank_account_transactions WHERE transaction_date < ?" - ) { - bind(deleteAfterMicro) - executeUpdate() - } - - // Hard delete soft deleted customer without bank transactions, bank account are deleted by CASCADE - conn.withStatement(""" - DELETE FROM customers WHERE deleted_at IS NOT NULL AND NOT EXISTS( - SELECT FROM bank_account_transactions NATURAL JOIN bank_accounts - WHERE owning_customer_id=customer_id - ) - """) { - executeUpdate() - } - - // TODO clean stats - } -} -\ No newline at end of file diff --git a/libeufin-bank/src/main/kotlin/tech/libeufin/bank/db/TanDAO.kt b/libeufin-bank/src/main/kotlin/tech/libeufin/bank/db/TanDAO.kt @@ -1,249 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2023-2025 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.bank.db - -import tech.libeufin.bank.Operation -import tech.libeufin.bank.TanChannel -import tech.libeufin.common.db.* -import tech.libeufin.common.* -import tech.libeufin.bank.* -import tech.libeufin.bank.auth.* -import java.time.Duration -import java.time.Instant -import java.util.UUID -import java.util.concurrent.TimeUnit - -/** Data access logic for tan challenged */ -class TanDAO(private val db: Database) { - /** Create a new challenge */ - suspend fun new( - username: String, - op: Operation, - hbody: Base32Crockford64B, - salt: Base32Crockford16B, - code: String, - timestamp: Instant, - retryCounter: Int, - validityPeriod: Duration, - tanChannel: TanChannel, - tanInfo: String - ): UUID = db.serializable( - """ - INSERT INTO tan_challenges ( - hbody, - salt, - op, - code, - creation_date, - expiration_date, - retry_counter, - customer, - tan_channel, - tan_info, - uuid - ) VALUES ( - ?, - ?, - ?::op_enum, - ?, - ?, - ?, - ?, - (SELECT customer_id FROM customers WHERE username = ? AND deleted_at IS NULL), - ?::tan_enum, - ?, - gen_random_uuid() - ) RETURNING uuid - """ - ) { - bind(hbody) - bind(salt) - bind(op) - bind(code) - bind(timestamp) - bind(timestamp.micros() + TimeUnit.MICROSECONDS.convert(validityPeriod)) - bind(retryCounter) - bind(username) - bind(tanChannel) - bind(tanInfo) - one { - it.getObject(1) as UUID - } - } - - /** Result of TAN challenge transmission */ - sealed interface TanSendResult { - data class Send( - val tanInfo: String, - val tanChannel: TanChannel, - val tanCode: String, - val expiration: TalerTimestamp - ) - data class Success( - val expiration: TalerTimestamp, - val retransmission: TalerTimestamp - ): TanSendResult - data object Expired: TanSendResult - data object Solved: TanSendResult - data object NotFound: TanSendResult - data object TooMany: TanSendResult - } - - /** Request TAN challenge transmission */ - suspend fun send( - uuid: UUID, - timestamp: Instant, - maxActive: Int - ) = db.serializable( - """ - SELECT - (confirmation_date IS NOT NULL) as solved - ,retransmission_date - ,expiration_date - ,code - ,tan_channel - ,tan_info - ,retry_counter - -- If this is the first time we submit this challenge check there is not too many active challenges - ,(retransmission_date = 0 AND ( - SELECT count(*) >= ? - FROM tan_challenges as o - WHERE c.customer = o.customer - AND retransmission_date != 0 - AND confirmation_date IS NULL - AND expiration_date >= ? - )) AS too_many - FROM tan_challenges as c - WHERE uuid = ? - """ - ) { - bind(maxActive) - bind(timestamp) - bind(uuid) - oneOrNull { - when { - it.getBoolean("solved") -> TanSendResult.Solved - it.getInt("retry_counter") <= 0 -> TanSendResult.Expired - it.getBoolean("too_many") -> TanSendResult.TooMany - else -> { - val retransmission = it.getTalerTimestamp("retransmission_date") - val expiration = it.getTalerTimestamp("expiration_date") - if (expiration.instant.isBefore(timestamp)) { - TanSendResult.Expired - } else if (retransmission.instant.isBefore(timestamp)) { - TanSendResult.Send( - tanInfo = it.getString("tan_info"), - tanChannel = it.getEnum("tan_channel"), - tanCode = it.getString("code"), - expiration = expiration - ) - } else { - TanSendResult.Success( - expiration = expiration, - retransmission = retransmission - ) - } - } - } - } ?: TanSendResult.NotFound - } - - /** Mark TAN challenge transmission */ - suspend fun markSent( - uuid: UUID, - retransmission: Instant - ) = db.serializable( - "UPDATE tan_challenges SET retransmission_date = ? WHERE uuid = ?" - ) { - bind(retransmission) - bind(uuid) - executeUpdate() - } - - /** Result of TAN challenge solution */ - sealed interface TanSolveResult { - data class Success(val op: Operation, val channel: TanChannel?, val info: String?): TanSolveResult - data object NotFound: TanSolveResult - data object NoRetry: TanSolveResult - data object Expired: TanSolveResult - data object BadCode: TanSolveResult - } - - /** Solve TAN challenge */ - suspend fun solve( - uuid: UUID, - code: String, - timestamp: Instant - ) = db.serializable( - """ - SELECT - out_ok, out_no_op, out_no_retry, out_expired, - out_op, out_channel, out_info - FROM tan_challenge_try(?,?,?) - """ - ) { - bind(uuid) - bind(code) - bind(timestamp.micros()) - one { - when { - it.getBoolean("out_ok") -> TanSolveResult.Success( - op = it.getEnum("out_op"), - channel = it.getOptEnum<TanChannel>("out_channel"), - info = it.getString("out_info") - ) - it.getBoolean("out_no_op") -> TanSolveResult.NotFound - it.getBoolean("out_no_retry") -> TanSolveResult.NoRetry - it.getBoolean("out_expired") -> TanSolveResult.Expired - else -> TanSolveResult.BadCode - } - } - } - - data class SolvedChallenge( - val salt: Base32Crockford16B, - val hbody: Base32Crockford64B, - val channel: TanChannel, - val info: String, - val confirmed: Boolean, - val op: Operation - ) - - /** Get a TAN challenge [uuid] */ - suspend fun challenge(uuids: List<UUID>): List<SolvedChallenge> = db.serializable( - """ - SELECT salt, hbody, tan_channel, tan_info, op, (confirmation_date IS NOT NULL) as confirmed - FROM tan_challenges - WHERE uuid = ANY(?::uuid[]) - """ - ) { - bind(uuids.toTypedArray()) - all { - SolvedChallenge( - salt = Base32Crockford16B(it.getBytes("salt")), - hbody = Base32Crockford64B(it.getBytes("hbody")), - op = it.getEnum<Operation>("op"), - channel = it.getEnum<TanChannel>("tan_channel"), - info = it.getString("tan_info"), - confirmed = it.getBoolean("confirmed") - ) - } - } -} diff --git a/libeufin-bank/src/main/kotlin/tech/libeufin/bank/db/TokenDAO.kt b/libeufin-bank/src/main/kotlin/tech/libeufin/bank/db/TokenDAO.kt @@ -1,253 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2023-2025 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.bank.db - -import tech.libeufin.bank.* -import tech.libeufin.common.PageParams -import tech.libeufin.common.asInstant -import tech.libeufin.common.db.* -import tech.libeufin.common.micros -import java.time.Instant - -/** Data access logic for auth tokens */ -class TokenDAO(private val db: Database) { - /** Result status of token creation */ - sealed interface TokenCreationResult { - data object Success: TokenCreationResult - data object TanRequired: TokenCreationResult - } - - /** Create new token for [username] */ - suspend fun create( - username: String, - content: ByteArray, - creationTime: Instant, - expirationTime: Instant, - scope: TokenScope, - isRefreshable: Boolean, - description: String?, - is2fa: Boolean - ): TokenCreationResult = db.serializable( - """ - SELECT out_tan_required FROM create_token( - ?,?,?,?,?::token_scope_enum,?,?,? - ) - """ - ) { - bind(username) - bind(content) - bind(creationTime) - bind(expirationTime) - bind(scope) - bind(isRefreshable) - bind(description) - bind(is2fa) - one { - when { - it.getBoolean("out_tan_required") -> TokenCreationResult.TanRequired - else -> TokenCreationResult.Success - } - } - } - - /** - * Atomically create a replacement for [source] and shorten the source - * token's lifetime to [sourceExpirationCap]. LEAST makes the overlap - * deadline non-sliding when the same source is refreshed more than once. - */ - suspend fun refresh( - username: String, - source: ByteArray, - content: ByteArray, - creationTime: Instant, - expirationTime: Instant, - sourceExpirationCap: Instant, - scope: TokenScope, - isRefreshable: Boolean, - description: String? - ): Boolean = db.serializable( - """ - WITH source_token AS ( - UPDATE bearer_tokens - SET expiration_time=LEAST(expiration_time, ?) - WHERE content=? - AND bank_customer=( - SELECT customer_id FROM customers - WHERE username=? AND deleted_at IS NULL - ) - RETURNING bank_customer - ), replacement AS ( - INSERT INTO bearer_tokens ( - content, creation_time, expiration_time, scope, bank_customer, - is_refreshable, description, last_access - ) - SELECT ?, ?, ?, ?::token_scope_enum, bank_customer, ?, ?, ? - FROM source_token - RETURNING bearer_token_id - ) - SELECT EXISTS(SELECT FROM replacement) - """ - ) { - bind(sourceExpirationCap) - bind(source) - bind(username) - bind(content) - bind(creationTime) - bind(expirationTime) - bind(scope) - bind(isRefreshable) - bind(description) - bind(creationTime) - one { it.getBoolean(1) } - } - - /** Get info for [token] */ - suspend fun access(token: ByteArray, accessTime: Instant): BearerToken? = db.serializable( - """ - UPDATE bearer_tokens - SET last_access=? - FROM customers - WHERE bank_customer=customer_id AND content=? AND deleted_at IS NULL - RETURNING - creation_time, - expiration_time, - scope, - is_refreshable - """ - ) { - bind(accessTime) - bind(token) - oneOrNull { - BearerToken( - creationTime = it.getLong("creation_time").asInstant(), - expirationTime = it.getLong("expiration_time").asInstant(), - scope = it.getEnum("scope"), - isRefreshable = it.getBoolean("is_refreshable") - ) - } - } - - /** Get info for [token] and its associated bank account*/ - suspend fun accessInfo(token: ByteArray, accessTime: Instant): Pair<BearerToken, BankInfo>? = db.serializable( - """ - UPDATE bearer_tokens - SET last_access=? - FROM customers - JOIN bank_accounts ON customer_id=owning_customer_id - WHERE bank_customer=customer_id AND content=? AND deleted_at IS NULL - RETURNING - creation_time, - expiration_time, - scope, - is_refreshable, - username, - is_taler_exchange, - bank_account_id, - internal_payto, - name, - tan_channels, - email, - phone - """ - ) { - bind(accessTime) - bind(token) - oneOrNull { - Pair( - BearerToken( - creationTime = it.getLong("creation_time").asInstant(), - expirationTime = it.getLong("expiration_time").asInstant(), - scope = it.getEnum("scope"), - isRefreshable = it.getBoolean("is_refreshable") - ), - BankInfo( - username = it.getString("username"), - payto = it.getBankPayto("internal_payto", "name", db.ctx), - bankAccountId = it.getLong("bank_account_id"), - isTalerExchange = it.getBoolean("is_taler_exchange"), - channels = it.getEnumSet<TanChannel>("tan_channels"), - phone = it.getString("phone"), - email = it.getString("email") - ) - ) - } - } - - /** Delete token [token] */ - suspend fun delete(token: ByteArray) = db.serializable( - "DELETE FROM bearer_tokens WHERE content = ?" - ) { - bind(token) - executeUpdate() - } - - /** Delete token [id] owned by the account identified by [username]. */ - suspend fun deleteById(id: Long, username: String) = db.serializable( - """ - DELETE FROM bearer_tokens - WHERE bearer_token_id = ? - AND bank_customer=( - SELECT customer_id FROM customers - WHERE username=? AND deleted_at IS NULL - ) - """ - ) { - bind(id) - bind(username) - executeUpdateCheck() - } - - /** Get a page of all tokens of [username] accounts */ - suspend fun page(params: PageParams, username: String, timestamp: Instant): List<TokenInfo> - = db.page( - params, - "bearer_token_id", - """ - SELECT - creation_time, - expiration_time, - scope, - is_refreshable, - description, - last_access, - bearer_token_id - FROM bearer_tokens - WHERE - expiration_time > ? AND - bank_customer=(SELECT customer_id FROM customers WHERE deleted_at IS NULL AND username = ?) - AND - """, - { - bind(timestamp.micros()) - bind(username) - } - ) { - TokenInfo( - creation_time = it.getTalerTimestamp("creation_time"), - expiration = it.getTalerTimestamp("expiration_time"), - scope = it.getEnum("scope"), - refreshable = it.getBoolean("is_refreshable"), - description = it.getString("description"), - last_access = it.getTalerTimestamp("last_access"), - row_id = it.getLong("bearer_token_id"), - token_id = it.getLong("bearer_token_id") - ) - } -} diff --git a/libeufin-bank/src/main/kotlin/tech/libeufin/bank/db/TransactionDAO.kt b/libeufin-bank/src/main/kotlin/tech/libeufin/bank/db/TransactionDAO.kt @@ -1,210 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2023, 2024, 2025, 2026 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.bank.db - -import org.slf4j.Logger -import org.slf4j.LoggerFactory -import tech.libeufin.bank.BankAccountTransactionInfo -import tech.libeufin.common.* -import tech.libeufin.common.db.* -import java.sql.Types -import java.time.Instant - -private val logger: Logger = LoggerFactory.getLogger("libeufin-bank-tx-dao") - -/** Data access logic for transactions */ -class TransactionDAO(private val db: Database) { - /** Result status of bank transaction creation */ - sealed interface BankTransactionResult { - data class Success(val id: Long): BankTransactionResult - data object UnknownCreditor: BankTransactionResult - data object AdminCreditor: BankTransactionResult - data object UnknownDebtor: BankTransactionResult - data object BothPartySame: BankTransactionResult - data object BalanceInsufficient: BankTransactionResult - data object BadAmount: BankTransactionResult - data object TanRequired: BankTransactionResult - data object RequestUidReuse: BankTransactionResult - } - - /** Create a new transaction */ - suspend fun create( - creditAccountPayto: Payto, - debitAccountUsername: String, - subject: String, - amount: TalerAmount, - timestamp: Instant, - is2fa: Boolean, - requestUid: ShortHashCode?, - wireTransferFees: TalerAmount, - minAmount: TalerAmount, - maxAmount: TalerAmount - ): BankTransactionResult { - val (type: IncomingType?, metadata: EddsaPublicKey?, bounceCause: String?) = runCatching { parseIncomingSubject(subject) }.fold( - onSuccess = { metadata -> - if (metadata is IncomingSubject.AdminBalanceAdjust) { - Triple(null, null, "unsupported admin balance adjust") - } else { - Triple(metadata.type, metadata.key, null) - } - }, - onFailure = { e -> Triple(null, null, "malformed metadata - ${e.message}") } - ) - return db.serializable(""" - SELECT - out_creditor_not_found - ,out_debtor_not_found - ,out_same_account - ,out_balance_insufficient - ,out_bad_amount - ,out_request_uid_reuse - ,out_tan_required - ,out_credit_bank_account_id - ,out_debit_bank_account_id - ,out_credit_row_id - ,out_debit_row_id - ,out_creditor_is_exchange - ,out_debtor_is_exchange - ,out_creditor_admin - ,out_idempotent - FROM bank_transaction(?,?,?,(?,?)::taler_amount,?,?,?,(?,?)::taler_amount,(?,?)::taler_amount,(?,?)::taler_amount,?::taler_incoming_type,?,?) - """ - ) { - bind(creditAccountPayto.canonical) - bind(debitAccountUsername) - bind(subject) - bind(amount) - bind(timestamp) - bind(is2fa) - bind(requestUid) - bind(wireTransferFees) - bind(minAmount) - bind(maxAmount) - bind(type) - bind(metadata) - bind(bounceCause) - one { - when { - it.getBoolean("out_creditor_not_found") -> BankTransactionResult.UnknownCreditor - it.getBoolean("out_debtor_not_found") -> BankTransactionResult.UnknownDebtor - it.getBoolean("out_same_account") -> BankTransactionResult.BothPartySame - it.getBoolean("out_balance_insufficient") -> BankTransactionResult.BalanceInsufficient - it.getBoolean("out_bad_amount") -> BankTransactionResult.BadAmount - it.getBoolean("out_creditor_admin") -> BankTransactionResult.AdminCreditor - it.getBoolean("out_request_uid_reuse") -> BankTransactionResult.RequestUidReuse - it.getBoolean("out_idempotent") -> BankTransactionResult.Success(it.getLong("out_debit_row_id")) - it.getBoolean("out_tan_required") -> BankTransactionResult.TanRequired - else -> BankTransactionResult.Success(it.getLong("out_debit_row_id")) - } - } - } - } - - /** Get transaction [rowId] owned by [username] */ - suspend fun get(rowId: Long, username: String): BankAccountTransactionInfo? = db.serializable( - """ - SELECT - creditor_payto - ,creditor_name - ,debtor_payto - ,debtor_name - ,subject - ,(amount).val AS amount_val - ,(amount).frac AS amount_frac - ,transaction_date - ,direction - ,bank_transaction_id - FROM bank_account_transactions - JOIN bank_accounts ON bank_account_transactions.bank_account_id=bank_accounts.bank_account_id - JOIN customers ON customer_id=owning_customer_id - WHERE bank_transaction_id=? AND username=? - """ - ) { - bind(rowId) - bind(username) - oneOrNull { - BankAccountTransactionInfo( - creditor_payto_uri = it.getBankPayto("creditor_payto", "creditor_name", db.ctx), - debtor_payto_uri = it.getBankPayto("debtor_payto", "debtor_name", db.ctx), - amount = it.getAmount("amount", db.bankCurrency), - direction = it.getEnum("direction"), - subject = it.getString("subject"), - date = it.getTalerTimestamp("transaction_date"), - row_id = it.getLong("bank_transaction_id") - ) - } - } - - /** Pool [accountId] transactions history */ - suspend fun pollHistory( - params: HistoryParams, - accountId: Long - ): List<BankAccountTransactionInfo> { - return db.poolHistory(params, accountId, db::listenBank, """ - SELECT - bank_transaction_id - ,transaction_date - ,(amount).val AS amount_val - ,(amount).frac AS amount_frac - ,debtor_payto - ,debtor_name - ,creditor_payto - ,creditor_name - ,subject - ,direction - FROM bank_account_transactions WHERE - """) { - BankAccountTransactionInfo( - row_id = it.getLong("bank_transaction_id"), - date = it.getTalerTimestamp("transaction_date"), - creditor_payto_uri = it.getBankPayto("creditor_payto", "creditor_name", db.ctx), - debtor_payto_uri = it.getBankPayto("debtor_payto", "debtor_name", db.ctx), - amount = it.getAmount("amount", db.bankCurrency), - subject = it.getString("subject"), - direction = it.getEnum("direction") - ) - } - } - - /** Query [accountId] history of incoming transactions to its account */ - suspend fun revenueHistory( - params: HistoryParams, - accountId: Long - ): List<RevenueIncomingBankTransaction> - = db.poolHistory(params, accountId, db::listenRevenue, """ - SELECT - bank_transaction_id - ,transaction_date - ,(amount).val AS amount_val - ,(amount).frac AS amount_frac - ,debtor_payto - ,debtor_name - ,subject - FROM bank_account_transactions WHERE direction='credit' AND - """) { - RevenueIncomingBankTransaction( - row_id = it.getLong("bank_transaction_id"), - date = it.getTalerTimestamp("transaction_date"), - amount = it.getAmount("amount", db.bankCurrency), - debit_account = it.getBankPayto("debtor_payto", "debtor_name", db.ctx), - subject = it.getString("subject") - ) - } -} -\ No newline at end of file diff --git a/libeufin-bank/src/main/kotlin/tech/libeufin/bank/db/TransferDAO.kt b/libeufin-bank/src/main/kotlin/tech/libeufin/bank/db/TransferDAO.kt @@ -1,88 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2026 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.bank.db - -import tech.libeufin.common.* -import tech.libeufin.common.db.* -import java.time.Instant -import java.util.UUID - -/** Data access logic for transfer specific logic */ -class TransferDAO(private val db: Database) { - /** Result of prepared transfer registration */ - sealed interface RegistrationResult { - data class Success(val uuid: UUID?): RegistrationResult - data object NotExchange: RegistrationResult - data object ReservePubReuse: RegistrationResult - data object UnknownCreditor: RegistrationResult - } - - /** Register a prepared transfer */ - suspend fun register( - account: Payto, - type: TransferType, - accountPub: EddsaPublicKey, - authPub: EddsaPublicKey, - authSig: EddsaSignature, - recurrent: Boolean, - amount: TalerAmount, - timestamp: Instant - ): RegistrationResult = db.serializable( - """ - SELECT out_unknown_creditor, out_not_exchange, out_reserve_pub_reuse, out_withdrawal_uuid - FROM register_prepared_transfers ( - ?, ?::taler_incoming_type, ?, ?, ?, ?, (?, ?)::taler_amount, ?, ? - ) - """ - ) { - bind(account.canonical) - bind(type) - bind(accountPub) - bind(authPub) - bind(authSig) - bind(recurrent) - bind(amount) - bind(timestamp) - bind("Taler prepared MAP:$authPub") - one { - when { - it.getBoolean("out_unknown_creditor") -> RegistrationResult.UnknownCreditor - it.getBoolean("out_not_exchange") -> RegistrationResult.NotExchange - it.getBoolean("out_reserve_pub_reuse") -> RegistrationResult.ReservePubReuse - else -> RegistrationResult.Success(it.getOptObject("out_withdrawal_uuid")) - } - } - } - - /** Unregister a prepared transfer */ - suspend fun unregister( - authorizationPub: EddsaPublicKey, - timestamp: Instant - ) = db.serializable( - "SELECT out_found FROM delete_prepared_transfers(?,?)", - { - bind(authorizationPub) - bind(timestamp) - one { - it.getBoolean(1) - } - } - ) -} -\ No newline at end of file diff --git a/libeufin-bank/src/main/kotlin/tech/libeufin/bank/db/WithdrawalDAO.kt b/libeufin-bank/src/main/kotlin/tech/libeufin/bank/db/WithdrawalDAO.kt @@ -1,408 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2023, 2024, 2025, 2026 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.bank.db - -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch -import kotlinx.coroutines.withTimeoutOrNull -import tech.libeufin.bank.* -import tech.libeufin.common.EddsaPublicKey -import tech.libeufin.common.Payto -import tech.libeufin.common.TalerAmount -import tech.libeufin.common.db.* -import tech.libeufin.common.micros -import java.time.Instant -import java.util.* - -/** Data access logic for withdrawal operations */ -class WithdrawalDAO(private val db: Database) { - /** Result status of withdrawal operation creation */ - enum class WithdrawalCreationResult { - Success, - UnknownAccount, - AccountIsExchange, - BalanceInsufficient, - BadAmount - } - - /** Create a new withdrawal operation */ - suspend fun create( - username: String, - uuid: UUID, - amount: TalerAmount?, - suggested_amount: TalerAmount?, - no_amount_to_wallet: Boolean, - timestamp: Instant, - wireTransferFees: TalerAmount, - minAmount: TalerAmount, - maxAmount: TalerAmount - ): WithdrawalCreationResult = db.serializable( - """ - SELECT - out_account_not_found, - out_account_is_exchange, - out_balance_insufficient, - out_bad_amount - FROM create_taler_withdrawal( - ?,?, - ${optAmount(amount)}, - ${optAmount(suggested_amount)}, - ?, ?, (?, ?)::taler_amount, (?, ?)::taler_amount, (?, ?)::taler_amount - ); - """ - ) { - - bind(username) - bind(uuid) - bind(amount) - bind(suggested_amount) - bind(no_amount_to_wallet) - bind(timestamp.micros()) - bind(wireTransferFees) - bind(minAmount) - bind(maxAmount) - one { - when { - it.getBoolean("out_account_not_found") -> WithdrawalCreationResult.UnknownAccount - it.getBoolean("out_account_is_exchange") -> WithdrawalCreationResult.AccountIsExchange - it.getBoolean("out_balance_insufficient") -> WithdrawalCreationResult.BalanceInsufficient - it.getBoolean("out_bad_amount") -> WithdrawalCreationResult.BadAmount - else -> WithdrawalCreationResult.Success - } - } - } - - /** Abort withdrawal operation [uuid] */ - suspend fun abort( - uuid: UUID - ): AbortResult = db.serializable( - """ - SELECT - out_no_op, - out_already_confirmed - FROM abort_taler_withdrawal(?) - """ - ) { - bind(uuid) - one { - when { - it.getBoolean("out_no_op") -> AbortResult.UnknownOperation - it.getBoolean("out_already_confirmed") -> AbortResult.AlreadyConfirmed - else -> AbortResult.Success - } - } - } - - /** Result withdrawal operation selection */ - sealed interface WithdrawalSelectionResult { - data class Success(val status: WithdrawalStatus): WithdrawalSelectionResult - data object UnknownOperation: WithdrawalSelectionResult - data object AlreadySelected: WithdrawalSelectionResult - data object ReservePubReuse: WithdrawalSelectionResult - data object UnknownAccount: WithdrawalSelectionResult - data object AccountIsNotExchange: WithdrawalSelectionResult - data object AmountDiffers: WithdrawalSelectionResult - data object BalanceInsufficient: WithdrawalSelectionResult - data object BadAmount: WithdrawalSelectionResult - data object AlreadyAborted: WithdrawalSelectionResult - } - - /** Set details ([exchangePayto] & [reservePub] & [amount]) for withdrawal operation [uuid] */ - suspend fun setDetails( - uuid: UUID, - exchangePayto: Payto, - reservePub: EddsaPublicKey, - amount: TalerAmount?, - wireTransferFees: TalerAmount, - minAmount: TalerAmount, - maxAmount: TalerAmount - ): WithdrawalSelectionResult = db.serializable( - """ - SELECT - out_no_op, - out_already_selected, - out_reserve_pub_reuse, - out_account_not_found, - out_account_is_not_exchange, - out_status, - out_amount_differs, - out_balance_insufficient, - out_bad_amount, - out_aborted - FROM select_taler_withdrawal( - ?, ?, ?, ?, - ${optAmount(amount)}, - (?,?)::taler_amount, (?,?)::taler_amount, (?,?)::taler_amount - ); - """ - ) { - bind(uuid) - bind(reservePub) - bind("Taler withdrawal $reservePub") - bind(exchangePayto.canonical) - bind(amount) - bind(wireTransferFees) - bind(minAmount) - bind(maxAmount) - one { - when { - it.getBoolean("out_aborted") -> WithdrawalSelectionResult.AlreadyAborted - it.getBoolean("out_balance_insufficient") -> WithdrawalSelectionResult.BalanceInsufficient - it.getBoolean("out_bad_amount") -> WithdrawalSelectionResult.BadAmount - it.getBoolean("out_no_op") -> WithdrawalSelectionResult.UnknownOperation - it.getBoolean("out_already_selected") -> WithdrawalSelectionResult.AlreadySelected - it.getBoolean("out_amount_differs") -> WithdrawalSelectionResult.AmountDiffers - it.getBoolean("out_reserve_pub_reuse") -> WithdrawalSelectionResult.ReservePubReuse - it.getBoolean("out_account_not_found") -> WithdrawalSelectionResult.UnknownAccount - it.getBoolean("out_account_is_not_exchange") -> WithdrawalSelectionResult.AccountIsNotExchange - else -> WithdrawalSelectionResult.Success(it.getEnum("out_status")) - } - } - } - - /** Result status of withdrawal operation confirmation */ - enum class WithdrawalConfirmationResult { - Success, - UnknownOperation, - BalanceInsufficient, - BadAmount, - NotSelected, - AlreadyAborted, - TanRequired, - MissingAmount, - AmountDiffers, - ReservePubReuse - } - - /** Confirm withdrawal operation [uuid] */ - suspend fun confirm( - username: String, - uuid: UUID, - timestamp: Instant, - amount: TalerAmount?, - is2fa: Boolean, - wireTransferFees: TalerAmount, - minAmount: TalerAmount, - maxAmount: TalerAmount - ): WithdrawalConfirmationResult = db.serializable( - """ - SELECT - out_no_op, - out_balance_insufficient, - out_bad_amount, - out_not_selected, - out_aborted, - out_tan_required, - out_missing_amount, - out_amount_differs, - out_reserve_pub_reuse - FROM confirm_taler_withdrawal( - ?,?,?,?,(?,?)::taler_amount,(?,?)::taler_amount,(?,?)::taler_amount, ${optAmount(amount)} - ); - """ - ) { - bind(username) - bind(uuid) - bind(timestamp) - bind(is2fa) - bind(wireTransferFees) - bind(minAmount) - bind(maxAmount) - bind(amount) - one { - when { - it.getBoolean("out_no_op") -> WithdrawalConfirmationResult.UnknownOperation - it.getBoolean("out_balance_insufficient") -> WithdrawalConfirmationResult.BalanceInsufficient - it.getBoolean("out_bad_amount") -> WithdrawalConfirmationResult.BadAmount - it.getBoolean("out_not_selected") -> WithdrawalConfirmationResult.NotSelected - it.getBoolean("out_aborted") -> WithdrawalConfirmationResult.AlreadyAborted - it.getBoolean("out_tan_required") -> WithdrawalConfirmationResult.TanRequired - it.getBoolean("out_missing_amount") -> WithdrawalConfirmationResult.MissingAmount - it.getBoolean("out_amount_differs") -> WithdrawalConfirmationResult.AmountDiffers - it.getBoolean("out_reserve_pub_reuse") -> WithdrawalConfirmationResult.ReservePubReuse - else -> WithdrawalConfirmationResult.Success - } - } - } - - /** Get withdrawal operation [uuid] linked account username */ - suspend fun getUsername(uuid: UUID): String? = db.serializable( - """ - SELECT username - FROM taler_withdrawal_operations - JOIN bank_accounts ON wallet_bank_account=bank_account_id - JOIN customers ON customer_id=owning_customer_id - WHERE withdrawal_uuid=? - """ - ) { - bind(uuid) - oneOrNull { it.getString(1) } - } - - private suspend fun <T> poll( - uuid: UUID, - params: StatusParams, - status: (T) -> WithdrawalStatus, - load: suspend () -> T? - ): T? { - return if (params.polling.timeout_ms > 0) { - db.listenWithdrawals(uuid) { flow -> - coroutineScope { - // Start buffering notification before loading transactions to not miss any - val polling = launch { - withTimeoutOrNull(params.polling.timeout_ms) { - flow.first { it != params.old_state } - } - } - // Initial loading - val init = load() - // Long polling if there is no operation or its not confirmed - if (init?.run { status(this) == params.old_state } != false) { - polling.join() - load() - } else { - polling.cancel() - init - } - } - } - } else { - load() - } - } - - /** Pool public info of operation [uuid] */ - suspend fun pollInfo(uuid: UUID, params: StatusParams): WithdrawalPublicInfo? = - poll(uuid, params, status = { it.status }) { - db.serializable( - """ - SELECT - CASE - WHEN confirmation_done THEN 'confirmed' - WHEN aborted THEN 'aborted' - WHEN selection_done THEN 'selected' - ELSE 'pending' - END as status - ,(amount).val as amount_val - ,(amount).frac as amount_frac - ,(suggested_amount).val as suggested_amount_val - ,(suggested_amount).frac as suggested_amount_frac - ,selection_done - ,aborted - ,confirmation_done - ,reserve_pub - ,wallet_user.username - ,no_amount_to_wallet - ,exchange_account.internal_payto as exchange_payto - ,exchange_user.name as exchange_name - FROM taler_withdrawal_operations - JOIN bank_accounts AS wallet_account ON wallet_bank_account=wallet_account.bank_account_id - JOIN customers AS wallet_user ON wallet_user.customer_id=wallet_account.owning_customer_id - LEFT JOIN bank_accounts AS exchange_account ON exchange_bank_account=exchange_account.bank_account_id - LEFT JOIN customers AS exchange_user ON exchange_user.customer_id=exchange_account.owning_customer_id - WHERE withdrawal_uuid=? - """ - ) { - bind(uuid) - oneOrNull { - WithdrawalPublicInfo( - status = it.getEnum("status"), - amount = it.getOptAmount("amount", db.bankCurrency), - suggested_amount = it.getOptAmount("suggested_amount", db.bankCurrency), - username = it.getString("username"), - selected_exchange_account = it.getOptBankPayto("exchange_payto", "exchange_name", db.ctx), - selected_reserve_pub = it.getBytes("reserve_pub")?.run(::EddsaPublicKey), - no_amount_to_wallet = it.getBoolean("no_amount_to_wallet") - ) - } - } - } - - /** Pool public status of operation [uuid] */ - suspend fun pollStatus( - uuid: UUID, - params: StatusParams, - wire: WireMethod, - maxAmount: TalerAmount - ): BankWithdrawalOperationStatus? = - poll(uuid, params, status = { it.status }) { - db.serializable( - """ - SELECT - CASE - WHEN confirmation_done THEN 'confirmed' - WHEN aborted THEN 'aborted' - WHEN selection_done THEN 'selected' - ELSE 'pending' - END as status - ,(amount).val as amount_val - ,(amount).frac as amount_frac - ,(suggested_amount).val as suggested_amount_val - ,(suggested_amount).frac as suggested_amount_frac - ,selection_done - ,aborted - ,confirmation_done - ,wallet_account.internal_payto - ,wallet_user.name - ,reserve_pub - ,exchange_account.internal_payto as exchange_payto - ,exchange_user.name as exchange_name - ,(max_amount).val as max_amount_val - ,(max_amount).frac as max_amount_frac - ,no_amount_to_wallet - FROM taler_withdrawal_operations - JOIN bank_accounts AS wallet_account ON wallet_bank_account=wallet_account.bank_account_id - JOIN customers AS wallet_user ON wallet_user.customer_id=wallet_account.owning_customer_id - LEFT JOIN bank_accounts AS exchange_account ON exchange_bank_account=exchange_account.bank_account_id - LEFT JOIN customers AS exchange_user ON exchange_user.customer_id=exchange_account.owning_customer_id - ,account_max_amount(wallet_account.bank_account_id, (?, ?)::taler_amount) AS max_amount - WHERE withdrawal_uuid=? - """ - ) { - bind(maxAmount) - bind(uuid) - oneOrNull { - BankWithdrawalOperationStatus( - status = it.getEnum("status"), - amount = it.getOptAmount("amount", db.bankCurrency), - suggested_amount = it.getOptAmount("suggested_amount", db.bankCurrency), - max_amount = it.getAmount("max_amount", db.bankCurrency), - selection_done = it.getBoolean("selection_done"), - transfer_done = it.getBoolean("confirmation_done"), - aborted = it.getBoolean("aborted"), - sender_wire = it.getBankPayto("internal_payto", "name", db.ctx), - confirm_transfer_url = null, - suggested_exchange = null, - selected_exchange_account = it.getOptBankPayto("exchange_payto", "exchange_name", db.ctx), - no_amount_to_wallet = it.getBoolean("no_amount_to_wallet"), - selected_reserve_pub = it.getBytes("reserve_pub")?.run(::EddsaPublicKey), - wire_types = listOf( - when (wire) { - WireMethod.IBAN -> "iban" - WireMethod.X_TALER_BANK -> "x-taler-bank" - } - ), - currency = db.bankCurrency - ) - } - } - } -} -\ No newline at end of file diff --git a/libeufin-bank/src/main/kotlin/tech/libeufin/bank/helpers.kt b/libeufin-bank/src/main/kotlin/tech/libeufin/bank/helpers.kt @@ -1,130 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2023, 2024, 2025, 2026 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.bank - -import io.ktor.http.* -import io.ktor.util.* -import io.ktor.server.application.* -import io.ktor.server.plugins.* -import io.ktor.server.request.* -import io.ktor.server.routing.* -import io.ktor.server.util.* -import kotlinx.serialization.KSerializer -import kotlinx.serialization.Serializable -import kotlinx.serialization.descriptors.PrimitiveKind -import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder -import tech.libeufin.bank.auth.pathUsername -import tech.libeufin.bank.auth.AUTH_INFO -import tech.libeufin.bank.db.AccountDAO.AccountCreationResult -import tech.libeufin.bank.db.Database -import tech.libeufin.bank.BankConfig -import tech.libeufin.common.* -import tech.libeufin.common.api.intercept -import java.util.* - -private val BANK_INFO = AttributeKey<BankInfo>("bank_info") - -/** Retrieve the bank account info for the selected username*/ -suspend fun ApplicationCall.bankInfo(db: Database): BankInfo { - val username = this.pathUsername - - // CHeck cached bank info - val cachedInfo = this.attributes.getOrNull(BANK_INFO) - if (cachedInfo != null) { - require(cachedInfo.username == username) - return cachedInfo - } - - // Check cached auth info - val authInfo = this.attributes.getOrNull(AUTH_INFO) - if (authInfo != null && authInfo.username == username) { - this.attributes.put(BANK_INFO, authInfo) - return authInfo - } - - // Else load from db - val info = db.account.bankInfo(pathUsername) ?: throw unknownAccount(pathUsername) - this.attributes.put(BANK_INFO, info) - return info -} - -/** - * Builds the taler://withdraw-URI. Such URI will serve the requests - * from wallets, when they need to manage the operation. For example, - * a URI like taler://withdraw/$BANK_URL/taler-integration/$WO_ID needs - * the bank to implement the Taler integratino API at the following base URL: - * - * https://$BANK_URL/taler-integration - */ -fun BankConfig.talerWithdrawUri(id: UUID): String { - val base = this.baseUrl.url - val protocol = if (base.protocol.name == "http") "taler+http" else "taler" - val port = if (base.port != -1) ":${base.port}" else "" - return "${protocol}://withdraw/${base.host}${port}${base.encodedPath}taler-integration/${id}" -} - -fun BankConfig.withdrawConfirmUrl(id: UUID): String { - val base = this.baseUrl.url - return "${base}webui/#/operation/${id}" -} - -/** - * This function creates the admin account ONLY IF it was - * NOT found in the database. It sets it to a random password that - * is only meant to be overridden by a dedicated CLI tool. - * - * It returns false in case of problems, true otherwise. - */ -suspend fun createAdminAccount(db: Database, cfg: BankConfig, pw: String? = null): AccountCreationResult { - var pwStr = pw ?: Base32Crockford32B.secureRand().toString() - - val payto = when (cfg.wireMethod) { - WireMethod.IBAN -> IbanPayto.rand() - WireMethod.X_TALER_BANK -> XTalerBankPayto.forUsername("admin") - } - - return db.account.create( - username = "admin", - password = pwStr, - name = "Bank administrator", - internalPayto = payto, - isPublic = false, - isTalerExchange = false, - maxDebt = cfg.defaultDebtLimit, - bonus = TalerAmount(0, 0, cfg.regionalCurrency), - checkPaytoIdempotent = false, - email = null, - phone = null, - cashoutPayto = null, - tanChannels = emptySet(), - pwCrypto = cfg.pwCrypto, - conversionRateClassId = null, - ) -} - -fun Route.conditional(implemented: Boolean, callback: Route.() -> Unit): Route = - intercept("Conditional", callback) { - if (!implemented) { - throw notImplemented() - } - } -\ No newline at end of file diff --git a/libeufin-bank/src/main/kotlin/tech/libeufin/bank/params.kt b/libeufin-bank/src/main/kotlin/tech/libeufin/bank/params.kt @@ -1,145 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2023-2025 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.bank - -import io.ktor.http.* -import tech.libeufin.common.* -import java.time.Instant -import java.time.LocalDateTime -import java.time.ZoneOffset -import java.time.temporal.TemporalAdjusters - -data class MonitorParams( - val timeframe: Timeframe, - val date: LocalDateTime -) { - constructor(timeframe: Timeframe, timestamp: LocalDateTime, which: Int) : this( - timeframe, - when (timeframe) { - Timeframe.hour -> timestamp.withHour(which) - Timeframe.day -> timestamp.withDayOfMonth(which) - Timeframe.month -> timestamp.withMonth(which) - Timeframe.year -> timestamp.withYear(which) - } - ) - constructor(timeframe: Timeframe, secs: Long) : this( - timeframe, - LocalDateTime.ofInstant(Instant.ofEpochSecond(secs), ZoneOffset.UTC) - ) - companion object { - val names = Timeframe.entries.map { it.name } - val names_fmt = names.joinToString() - - fun extract(params: Parameters): MonitorParams { - val raw = params["timeframe"] ?: "hour" - if (!names.contains(raw)) { - throw badRequest("Param 'timeframe' must be one of $names_fmt", TalerErrorCode.GENERIC_PARAMETER_MALFORMED) - } - val timeframe = Timeframe.valueOf(raw) - val now = LocalDateTime.now(ZoneOffset.UTC) - val which = params.int("which") - val dateS = params.long("date_s") - if (which != null && dateS != null) { - throw badRequest("Cannot use both 'date_s' and deprecated 'which'", TalerErrorCode.GENERIC_PARAMETER_MALFORMED) - } - return if (which != null) { - val lastDayOfMonth = now.with(TemporalAdjusters.lastDayOfMonth()).dayOfMonth - when { - timeframe == Timeframe.hour && (0 > which || which > 23) -> - throw badRequest("For hour timestamp param 'which' must be between 00 to 23", TalerErrorCode.GENERIC_PARAMETER_MALFORMED) - timeframe == Timeframe.day && (1 > which || which > lastDayOfMonth) -> - throw badRequest("For day timestamp param 'which' must be between 1 to $lastDayOfMonth", TalerErrorCode.GENERIC_PARAMETER_MALFORMED) - timeframe == Timeframe.month && (1 > which || which > 12) -> - throw badRequest("For month timestamp param 'which' must be between 1 to 12", TalerErrorCode.GENERIC_PARAMETER_MALFORMED) - timeframe == Timeframe.year && (1 > which|| which > 9999) -> - throw badRequest("For year timestamp param 'which' must be between 0001 to 9999", TalerErrorCode.GENERIC_PARAMETER_MALFORMED) - else -> {} - } - MonitorParams(timeframe, now, which) - } else if (dateS != null) { - MonitorParams(timeframe, dateS) - } else { - MonitorParams(timeframe, now) - } - } - } -} - -data class AccountParams( - val page: PageParams, - val usernameFilter: String?, - val conversionRateClassId: Long? -) { - companion object { - fun extract(params: Parameters): AccountParams { - val usernameFilter = params["filter_name"]?.run { "%$this%" } - val conversionRateClassId = params.long("conversion_rate_class_id") - return AccountParams(PageParams.extract(params), usernameFilter, conversionRateClassId) - } - } -} - -data class ClassParams( - val page: PageParams, val nameFilter: String? -) { - companion object { - fun extract(params: Parameters): ClassParams { - val usernameFilter = params["filter_name"]?.run { "%$this%" } - return ClassParams(PageParams.extract(params), usernameFilter) - } - } -} - -data class RateParams( - val debit: TalerAmount?, val credit: TalerAmount? -) { - companion object { - fun extract(params: Parameters): RateParams { - val debit = params.amount("amount_debit") - val credit = params.amount("amount_credit") - if (debit == null && credit == null) { - throw badRequest("Either param 'amount_debit' or 'amount_credit' is required", TalerErrorCode.GENERIC_PARAMETER_MISSING) - } else if (debit != null && credit != null) { - throw badRequest("Cannot have both 'amount_debit' and 'amount_credit' params", TalerErrorCode.GENERIC_PARAMETER_MALFORMED) - } - return RateParams(debit, credit) - } - } -} - -data class StatusParams( - val polling: PollingParams, - val old_state: WithdrawalStatus -) { - companion object { - private val names = WithdrawalStatus.entries.map { it.name } - private val names_fmt = names.joinToString() - fun extract(params: Parameters): StatusParams { - val old_state = params["old_state"] ?: "pending" - if (!names.contains(old_state)) { - throw badRequest("Param 'old_state' must be one of $names_fmt", TalerErrorCode.GENERIC_PARAMETER_MALFORMED) - } - return StatusParams( - polling = PollingParams.extract(params), - old_state = WithdrawalStatus.valueOf(old_state) - ) - } - } -} -\ No newline at end of file diff --git a/crates/libeufin-bank/src/mfa.rs b/libeufin-bank/src/mfa.rs diff --git a/crates/libeufin-bank/src/payto.rs b/libeufin-bank/src/payto.rs diff --git a/crates/libeufin-bank/src/pw.rs b/libeufin-bank/src/pw.rs diff --git a/libeufin-bank/src/test/kotlin/AmountTest.kt b/libeufin-bank/src/test/kotlin/AmountTest.kt @@ -1,403 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2023-2025 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/> - */ - -import io.ktor.http.* -import org.junit.Test -import tech.libeufin.common.* -import tech.libeufin.common.db.* -import tech.libeufin.common.test.* -import kotlin.test.* - -class AmountTest { - // Test amount computation in db - @Test - fun computationTest() = bankSetup { db -> db.conn { conn -> - conn.execSQLUpdate("UPDATE libeufin_bank.bank_accounts SET balance.val = 100000 WHERE internal_payto = '${customerPayto.canonical}'") - val stmt = conn.talerStatement(""" - UPDATE libeufin_bank.bank_accounts - SET balance = (?, ?)::taler_amount - ,has_debt = ? - ,max_debt = (?, ?)::taler_amount - WHERE internal_payto = '${merchantPayto.canonical}' - """) - suspend fun routine( - balance: TalerAmount, - hasDebt: Boolean, - maxDebt: TalerAmount, - amount: TalerAmount - ): Boolean { - stmt.bind(balance) - stmt.bind(hasDebt) - stmt.bind(maxDebt) - - // Check bank transaction - stmt.executeUpdate() - val txRes = client.postA("/accounts/merchant/transactions") { - json { - "payto_uri" to "$customerPayto?message=" - "amount" to amount - } - } - val txBool = when { - txRes.isStatus(HttpStatusCode.OK, null) -> true - txRes.isStatus(HttpStatusCode.Conflict, TalerErrorCode.BANK_UNALLOWED_DEBIT) -> false - else -> throw Exception("Unexpected error $txRes") - } - - // Check whithdraw - stmt.bind(balance) - stmt.bind(hasDebt) - stmt.bind(maxDebt) - stmt.executeUpdate() - for ((amount, suggested) in listOf(Pair(amount, null), Pair(null, amount), Pair(amount, amount))) { - val wRes = client.postA("/accounts/merchant/withdrawals") { - json { - "amount" to amount - "suggested_amount" to suggested - } - } - val wBool = when { - wRes.isStatus(HttpStatusCode.OK, null) -> true - wRes.isStatus(HttpStatusCode.Conflict, TalerErrorCode.BANK_UNALLOWED_DEBIT) -> false - else -> throw Exception("Unexpected error $wRes") - } - // Logic must be the same - assertEquals(wBool, txBool) - } - - return txBool - } - - // Balance enough, assert for true - assert(routine( - balance = TalerAmount(10, 0, "KUDOS"), - hasDebt = false, - maxDebt = TalerAmount(100, 0, "KUDOS"), - amount = TalerAmount(8, 0, "KUDOS"), - )) - // Balance still sufficient, thanks for big enough debt permission. Assert true. - assert(routine( - balance = TalerAmount(10, 0, "KUDOS"), - hasDebt = false, - maxDebt = TalerAmount(100, 0, "KUDOS"), - amount = TalerAmount(80, 0, "KUDOS"), - )) - // Balance not enough, max debt cannot cover, asserting for false. - assert(!routine( - balance = TalerAmount(10, 0, "KUDOS"), - hasDebt = true, - maxDebt = TalerAmount(50, 0, "KUDOS"), - amount = TalerAmount(80, 0, "KUDOS"), - )) - // Balance becomes enough, due to a larger max debt, asserting for true. - assert(routine( - balance = TalerAmount(10, 0, "KUDOS"), - hasDebt = false, - maxDebt = TalerAmount(70, 0, "KUDOS"), - amount = TalerAmount(80, 0, "KUDOS"), - )) - // Max debt not enough for the smallest fraction, asserting for false - assert(!routine( - balance = TalerAmount(0, 0, "KUDOS"), - hasDebt = false, - maxDebt = TalerAmount(0, 1, "KUDOS"), - amount = TalerAmount(0, 2, "KUDOS"), - )) - // Same as above, but already in debt. - assert(!routine( - balance = TalerAmount(0, 1, "KUDOS"), - hasDebt = true, - maxDebt = TalerAmount(0, 1, "KUDOS"), - amount = TalerAmount(0, 1, "KUDOS"), - )) - }} - - // Max withdrawal amount computation in db - @Test - fun maxComputationTest() = bankSetup { db -> db.conn { conn -> - val update = conn.talerStatement(""" - UPDATE libeufin_bank.bank_accounts - SET balance = (?, ?)::taler_amount - ,has_debt = ? - ,max_debt = (?, ?)::taler_amount - WHERE bank_account_id = 1 - """) - val select = conn.talerStatement(""" - SELECT - (max_amount).val as max_amount_val - ,(max_amount).frac as max_amount_frac - FROM account_max_amount(1, (?, ?)::taler_amount) AS max_amount - """) - suspend fun routine( - balance: TalerAmount, - hasDebt: Boolean, - maxDebt: TalerAmount - ): TalerAmount { - update.apply { - bind(balance) - bind(hasDebt) - bind(maxDebt) - executeUpdate() - } - select.bind(TalerAmount.max("KUDOS")) - return select.one { it.getAmount("max_amount", "KUDOS") } - } - - // Without debt - assertEquals(TalerAmount(110, 3, "KUDOS"), routine( - balance = TalerAmount(10, 1, "KUDOS"), - hasDebt = false, - maxDebt = TalerAmount(100, 2, "KUDOS"), - )) - // With debt - assertEquals(TalerAmount(90, 1, "KUDOS"), routine( - balance = TalerAmount(10, 1, "KUDOS"), - hasDebt = true, - maxDebt = TalerAmount(100, 2, "KUDOS"), - )) - }} - - @Test - fun parseRoundTrip() { - for (amount in listOf("EUR:4", "EUR:0.02", "EUR:4.12")) { - assertEquals(amount, TalerAmount(amount).toString()) - } - } - - @Test - fun normalize() = dbSetup { db -> - db.conn { conn -> - val stmt = conn.talerStatement("SELECT normalized.val, normalized.frac FROM amount_normalize((?, ?)::taler_amount) as normalized") - fun TalerAmount.db(): TalerAmount { - stmt.bind(value) - stmt.bind(frac) - return stmt.one { - TalerAmount( - it.getLong(1), - it.getInt(2), - "EUR" - ) - } - } - - fun assertNormalize(from: TalerAmount, to: TalerAmount) { - val normalized = from.normalize() - assertEquals(to, normalized, "Bad normalization") - val dbNormalized = from.db() - assertEquals(normalized, dbNormalized, "DB vs code behavior") - } - - fun assertErr(from: TalerAmount, msg: String) { - assertFails { from.normalize() } - assertException(msg) { from.db() } - } - - assertNormalize(TalerAmount(4L, 2 * TalerAmount.FRACTION_BASE, "EUR"), TalerAmount("EUR:6")) - assertNormalize(TalerAmount(4L, 2 * TalerAmount.FRACTION_BASE + 1, "EUR"), TalerAmount("EUR:6.00000001")) - assertNormalize(TalerAmount("EUR:${TalerAmount.MAX_VALUE}.99999999"), TalerAmount("EUR:${TalerAmount.MAX_VALUE}.99999999")) - assertErr(TalerAmount(Long.MAX_VALUE, TalerAmount.FRACTION_BASE, "EUR"), "ERROR: bigint out of range") - assertErr(TalerAmount(TalerAmount.MAX_VALUE, TalerAmount.FRACTION_BASE , "EUR"), "ERROR: amount value overflowed") - - for (amount in listOf(TalerAmount.max("EUR"), TalerAmount.zero("EUR"))) { - assertNormalize(amount, amount) - } - } - } - - @Test - fun add() = dbSetup { db -> - db.conn { conn -> - val stmt = conn.talerStatement("SELECT sum.val, sum.frac FROM amount_add((?, ?)::taler_amount, (?, ?)::taler_amount) as sum") - fun TalerAmount.db(increment: TalerAmount): TalerAmount { - stmt.bind(value) - stmt.bind(frac) - stmt.bind(increment) - return stmt.one { - TalerAmount( - it.getLong(1), - it.getInt(2), - "EUR" - ) - } - } - - fun assertAdd(a: TalerAmount, b: TalerAmount, sum: TalerAmount) { - val codeSum = a + b - assertEquals(sum, codeSum, "Bad sum") - val dbSum = a.db(b) - assertEquals(codeSum, dbSum, "DB vs code behavior") - } - - fun assertErr(a: TalerAmount, b: TalerAmount, msg: String) { - assertFails { a + b } - assertException(msg) { a.db(b) } - } - - assertAdd(TalerAmount.max("EUR"), TalerAmount.zero("EUR"), TalerAmount.max("EUR")) - assertAdd(TalerAmount.zero("EUR"), TalerAmount.zero("EUR"), TalerAmount.zero("EUR")) - assertAdd(TalerAmount("EUR:6.41"), TalerAmount("EUR:4.69"), TalerAmount("EUR:11.1")) - assertAdd(TalerAmount("EUR:${TalerAmount.MAX_VALUE}"), TalerAmount("EUR:0.99999999"), TalerAmount("EUR:${TalerAmount.MAX_VALUE}.99999999")) - assertErr(TalerAmount(TalerAmount.MAX_VALUE - 5, 0, "EUR"), TalerAmount(6, 0, "EUR"), "ERROR: amount value overflowed") - assertErr(TalerAmount(Long.MAX_VALUE, 0, "EUR"), TalerAmount(1, 0, "EUR"), "ERROR: bigint out of range") - assertErr(TalerAmount(TalerAmount.MAX_VALUE - 5, TalerAmount.FRACTION_BASE - 1, "EUR"), TalerAmount(5, 2, "EUR"), "ERROR: amount value overflowed") - assertErr(TalerAmount(0, Int.MAX_VALUE, "EUR"), TalerAmount(0, 1, "EUR"), "ERROR: integer out of range") - } - } - - @Test - fun conversionApply() = dbSetup { db -> - db.conn { conn -> - fun apply(nb: TalerAmount, times: DecimalNumber, tiny: DecimalNumber = DecimalNumber("0.00000001"), roundingMode: String = "zero"): TalerAmount { - val stmt = conn.talerStatement("SELECT (result).val, (result).frac FROM conversion_apply_ratio((?, ?)::taler_amount, (?, ?)::taler_amount, (0, 0)::taler_amount, (?, ?)::taler_amount, ?::rounding_mode)") - stmt.bind(nb) - stmt.bind(times) - stmt.bind(tiny) - stmt.bind(roundingMode) - return stmt.one { - TalerAmount( - it.getLong(1), - it.getInt(2), - nb.currency - ) - } - } - - assertEquals(TalerAmount("EUR:30.0629"), apply(TalerAmount("EUR:6.41"), DecimalNumber("4.69"))) - assertEquals(TalerAmount("EUR:6.41000641"), apply(TalerAmount("EUR:6.41"), DecimalNumber("1.000001"))) - assertEquals(TalerAmount("EUR:2.49999997"), apply(TalerAmount("EUR:0.99999999"), DecimalNumber("2.5"))) - assertEquals(TalerAmount("EUR:${TalerAmount.MAX_VALUE}.99999999"), apply(TalerAmount("EUR:${TalerAmount.MAX_VALUE}.99999999"), DecimalNumber("1"))) - assertEquals(TalerAmount("EUR:${TalerAmount.MAX_VALUE}"), apply(TalerAmount("EUR:${TalerAmount.MAX_VALUE/4}"), DecimalNumber("4"))) - assertException("ERROR: amount value overflowed") { apply(TalerAmount(TalerAmount.MAX_VALUE/3, 0, "EUR"), DecimalNumber("3.00000001")) } - assertException("ERROR: amount value overflowed") { apply(TalerAmount((TalerAmount.MAX_VALUE+2)/2, 0, "EUR"), DecimalNumber("2")) } - assertException("ERROR: numeric field overflow") { apply(TalerAmount(Long.MAX_VALUE, 0, "EUR"), DecimalNumber("1")) } - - // Check rounding mode - for ((mode, rounding) in listOf( - Pair("zero", listOf(Pair(1, listOf(10, 11, 12, 12, 14, 15, 16, 17, 18, 19)))), - Pair("up", listOf(Pair(1, listOf(10)), Pair(2, listOf(11, 12, 12, 14, 15, 16, 17, 18, 19)))), - Pair("nearest", listOf(Pair(1, listOf(10, 11, 12, 12, 14)), Pair(2, listOf(15, 16, 17, 18, 19)))) - )) { - for ((rounded, amounts) in rounding) { - for (amount in amounts) { - // Check euro - assertEquals(TalerAmount("EUR:0.0$rounded"), apply(TalerAmount("EUR:$amount"), DecimalNumber("0.001"), DecimalNumber("0.01"), mode)) - // Check kudos - assertEquals(TalerAmount("KUDOS:0.0000000$rounded"), apply(TalerAmount("KUDOS:0.$amount"), DecimalNumber("0.0000001"), roundingMode = mode)) - } - } - } - - // Check hungarian rounding - for ((mode, rounding) in listOf( - Pair("zero", listOf(Pair(10, listOf(10, 11, 12, 13, 14)), Pair(15, listOf(15, 16, 17, 18, 19)))), - Pair("up", listOf(Pair(10, listOf(10)), Pair(15, listOf(11, 12, 13, 14, 15)), Pair(20, listOf(16, 17, 18, 19)))), - Pair("nearest", listOf(Pair(10, listOf(10, 11, 12)), Pair(15, listOf(13, 14, 15, 16, 17)), Pair(20, listOf(18, 19)))) - )) { - for ((rounded, amounts) in rounding) { - for (amount in amounts) { - assertEquals(TalerAmount("HUF:$rounded"), apply(TalerAmount("HUF:$amount"), DecimalNumber("1"), DecimalNumber("5"), mode)) - } - } - } - for (mode in listOf("zero", "up", "nearest")) { - assertEquals(TalerAmount("HUF:5"), apply(TalerAmount("HUF:5"), DecimalNumber("1"), DecimalNumber("1"), mode)) - } - } - } - - @Test - fun conversionRevert() = dbSetup { db -> - db.conn { conn -> - val applyStmt = conn.talerStatement("SELECT (result).val, (result).frac FROM conversion_apply_ratio((?, ?)::taler_amount, (?, ?)::taler_amount, (0, 0)::taler_amount, (?, ?)::taler_amount, ?::rounding_mode)") - fun TalerAmount.apply(ratio: DecimalNumber, tiny: DecimalNumber = DecimalNumber("0.00000001"), roundingMode: String = "zero"): TalerAmount { - applyStmt.bind(this) - applyStmt.bind(ratio) - applyStmt.bind(tiny) - applyStmt.bind(roundingMode) - return applyStmt.one { - TalerAmount( - it.getLong(1), - it.getInt(2), - currency - ) - } - } - - val revertStmt = conn.talerStatement("SELECT (result).val, (result).frac FROM conversion_revert_ratio((?, ?)::taler_amount, (?, ?)::taler_amount, (0, 0)::taler_amount, (?, ?)::taler_amount, ?::rounding_mode, (?, ?)::taler_amount)") - fun TalerAmount.revert(ratio: DecimalNumber, tiny: DecimalNumber = DecimalNumber("0.00000001"), roundingMode: String = "zero", reverseTiny: DecimalNumber = DecimalNumber("0.00000001")): TalerAmount { - revertStmt.bind(this) - revertStmt.bind(ratio) - revertStmt.bind(tiny) - revertStmt.bind(roundingMode) - revertStmt.bind(reverseTiny) - return revertStmt.one { - TalerAmount( - it.getLong(1), - it.getInt(2), - currency - ) - } - } - - assertEquals(TalerAmount("EUR:6.41"), TalerAmount("EUR:30.0629").revert(DecimalNumber("4.69"))) - assertEquals(TalerAmount("EUR:6.41"), TalerAmount("EUR:6.41000641").revert(DecimalNumber("1.000001"))) - assertEquals(TalerAmount("EUR:1"), TalerAmount("EUR:2.49999998").revert(DecimalNumber("2.5"))) - assertEquals(TalerAmount("EUR:${TalerAmount.MAX_VALUE}.99999999"), TalerAmount("EUR:${TalerAmount.MAX_VALUE}.99999999").revert(DecimalNumber("1"))) - assertEquals(TalerAmount("EUR:${TalerAmount.MAX_VALUE}"), TalerAmount("EUR:${TalerAmount.MAX_VALUE/4}").revert(DecimalNumber("0.25"))) - assertException("ERROR: amount value overflowed") { TalerAmount(TalerAmount.MAX_VALUE/4, 0, "EUR").revert(DecimalNumber("0.24999999")) } - assertException("ERROR: amount value overflowed") { TalerAmount((TalerAmount.MAX_VALUE+2)/2, 0, "EUR").revert(DecimalNumber("0.5")) } - assertException("ERROR: numeric field overflow") { TalerAmount(Long.MAX_VALUE, 0, "EUR").revert(DecimalNumber("1")) } - - - for (mode in sequenceOf("zero", "up", "nearest")) { - for (tiny in sequenceOf("0.01", "0.00000001", "1", "2", "3", "5").map(::DecimalNumber)) { - for (amount in sequenceOf(10, 11, 12, 12, 14, 15, 16, 17, 18, 19).map { TalerAmount("EUR:$it") }) { - for (ratio in sequenceOf("1", "1.25", "1.26", "0.01", "0.001", "0.00000001").map(::DecimalNumber)) { - for (reverseTiny in sequenceOf("0.01", "0.00000001", "1").map(::DecimalNumber)) { - // Apply ratio - val rounded = amount.apply(ratio, tiny, mode) - // Revert ratio - val revert = rounded.revert(ratio, tiny, mode, reverseTiny) - // Check applying ratio again give the same result - val check = revert.apply(ratio, tiny, mode) - println("$amount $rounded $revert $check $ratio $tiny $mode") - assertEquals(rounded, check) - } - } - } - } - } - } - } - - @Test - fun apiError() = bankSetup { - val base = obj { - "payto_uri" to "$exchangePayto?message=payout" - } - - // Check OK - client.postA("/accounts/merchant/transactions") { - json(base) { "amount" to "KUDOS:0.3ABC" } - }.assertBadRequest(TalerErrorCode.BANK_BAD_FORMAT_AMOUNT) - client.postA("/accounts/merchant/transactions") { - json(base) { "amount" to "KUDOS:999999999999999999" } - }.assertBadRequest(TalerErrorCode.BANK_NUMBER_TOO_BIG) - } -} -\ No newline at end of file diff --git a/libeufin-bank/src/test/kotlin/BankIntegrationApiTest.kt b/libeufin-bank/src/test/kotlin/BankIntegrationApiTest.kt @@ -1,293 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2023 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/> - */ - -import io.ktor.client.request.* -import org.junit.Test -import tech.libeufin.bank.BankAccountCreateWithdrawalResponse -import tech.libeufin.bank.BankWithdrawalOperationPostResponse -import tech.libeufin.bank.BankWithdrawalOperationStatus -import tech.libeufin.bank.WithdrawalStatus -import tech.libeufin.common.* -import tech.libeufin.common.test.* -import java.util.* -import kotlin.test.assertEquals - -class BankIntegrationApiTest { - // GET /taler-integration/config - @Test - fun config() = bankSetup { - client.get("/taler-integration/config").assertOk() - } - - // GET /taler-integration/withdrawal-operation/UUID - @Test - fun get() = bankSetup { - // Check OK - for (valid in listOf( - Pair(null, null), - Pair("KUDOS:1.0", null), - Pair(null, "KUDOS:2.0") , - Pair("KUDOS:3.0", "KUDOS:4.0") - )) { - val amount = valid.first?.run(::TalerAmount) - val suggested = valid.second?.run(::TalerAmount) - client.postA("/accounts/merchant/withdrawals") { - json { - "amount" to amount - "suggested_amount" to suggested - } - }.assertOkJson<BankAccountCreateWithdrawalResponse> { - val uuid = it.taler_withdraw_uri.split("/").last() - client.get("/taler-integration/withdrawal-operation/$uuid") - .assertOkJson<BankWithdrawalOperationStatus> { - assert(!it.selection_done) - assert(!it.aborted) - assert(!it.transfer_done) - assertEquals(it.card_fees, TalerAmount.zero("KUDOS")) - assertEquals(it.min_amount, TalerAmount.zero("KUDOS")) - assertEquals(it.max_amount, TalerAmount("KUDOS:10")) - assertEquals(amount, it.amount) - assertEquals(suggested, it.suggested_amount) - assertEquals(listOf("iban"), it.wire_types) - assertEquals("KUDOS", it.currency) - } - } - } - - // Check polling - statusRoutine<BankWithdrawalOperationStatus>("/taler-integration/withdrawal-operation") { it.status } - - // Check unknown - client.get("/taler-integration/withdrawal-operation/${UUID.randomUUID()}") - .assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) - - // Check bad UUID - client.get("/taler-integration/withdrawal-operation/chocolate") - .assertBadRequest(TalerErrorCode.GENERIC_PARAMETER_MALFORMED) - } - - // POST /taler-integration/withdrawal-operation/UUID - @Test - fun select() = bankSetup { - val reserve_pub = EddsaPublicKey.randEdsaKey() - val req = obj { - "reserve_pub" to reserve_pub - "selected_exchange" to exchangePayto.canonical - } - - // Check bad UUID - client.post("/taler-integration/withdrawal-operation/chocolate") { - json(req) - }.assertBadRequest(TalerErrorCode.GENERIC_PARAMETER_MALFORMED) - - // Check unknown - client.post("/taler-integration/withdrawal-operation/${UUID.randomUUID()}") { - json(req) - }.assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) - - client.postA("/accounts/merchant/withdrawals") { - json { "amount" to "KUDOS:1" } - }.assertOkJson<BankAccountCreateWithdrawalResponse> { - val uuid = it.withdrawal_id - - // Check OK - client.post("/taler-integration/withdrawal-operation/$uuid") { - json(req) - }.assertOkJson<BankWithdrawalOperationPostResponse> { - assertEquals(WithdrawalStatus.selected, it.status) - assertEquals("http://localhost:8080/webui/#/operation/$uuid", it.confirm_transfer_url) - } - // Check idempotence - client.post("/taler-integration/withdrawal-operation/$uuid") { - json(req) - }.assertOkJson<BankWithdrawalOperationPostResponse> { - assertEquals(WithdrawalStatus.selected, it.status) - assertEquals("http://localhost:8080/webui/#/operation/$uuid", it.confirm_transfer_url) - } - // Check already selected - client.post("/taler-integration/withdrawal-operation/$uuid") { - json(req) { - "reserve_pub" to EddsaPublicKey.randEdsaKey() - } - }.assertConflict(TalerErrorCode.BANK_WITHDRAWAL_OPERATION_RESERVE_SELECTION_CONFLICT) - } - - client.postA("/accounts/merchant/withdrawals") { - json { "amount" to "KUDOS:1" } - }.assertOkJson<BankAccountCreateWithdrawalResponse> { - val uuid = it.withdrawal_id - - // Check reserve_pub_reuse - client.post("/taler-integration/withdrawal-operation/$uuid") { - json(req) - }.assertConflict(TalerErrorCode.BANK_DUPLICATE_RESERVE_PUB_SUBJECT) - - // Check amount differs - client.post("/taler-integration/withdrawal-operation/$uuid") { - json(req) { - "amount" to "KUDOS:2" - } - }.assertConflict(TalerErrorCode.BANK_AMOUNT_DIFFERS) - - // Check unknown account - client.post("/taler-integration/withdrawal-operation/$uuid") { - json { - "reserve_pub" to EddsaPublicKey.randEdsaKey() - "selected_exchange" to unknownPayto - } - }.assertConflict(TalerErrorCode.BANK_UNKNOWN_ACCOUNT) - - // Check account not exchange - client.post("/taler-integration/withdrawal-operation/$uuid") { - json { - "reserve_pub" to EddsaPublicKey.randEdsaKey() - "selected_exchange" to merchantPayto - } - }.assertConflict(TalerErrorCode.BANK_ACCOUNT_IS_NOT_EXCHANGE) - - client.post("/taler-integration/withdrawal-operation/$uuid") { - json { - "reserve_pub" to EddsaPublicKey.randEdsaKey() - "selected_exchange" to exchangePayto.canonical - "amount" to "KUDOS:1" - } - }.assertOkJson<BankWithdrawalOperationPostResponse>() - } - - // Check select aborted - client.postA("/accounts/merchant/withdrawals") { - json { "amount" to "KUDOS:1" } - }.assertOkJson<BankAccountCreateWithdrawalResponse> { - val uuid = it.withdrawal_id - client.postA("/accounts/merchant/withdrawals/$uuid/abort").assertNoContent() - - // Check error - client.postA("/taler-integration/withdrawal-operation/$uuid") { - json { - "reserve_pub" to EddsaPublicKey.randEdsaKey() - "selected_exchange" to exchangePayto.canonical - } - }.assertConflict(TalerErrorCode.BANK_UPDATE_ABORT_CONFLICT) - } - - client.postA("/accounts/merchant/withdrawals") { - json {} - }.assertOkJson<BankAccountCreateWithdrawalResponse> { - val uuid = it.withdrawal_id - - // Check insufficient fund - client.post("/taler-integration/withdrawal-operation/$uuid") { - json { - "reserve_pub" to EddsaPublicKey.randEdsaKey() - "selected_exchange" to exchangePayto.canonical - "amount" to "KUDOS:11" - } - }.assertConflict(TalerErrorCode.BANK_UNALLOWED_DEBIT) - - client.post("/taler-integration/withdrawal-operation/$uuid") { - json { - "reserve_pub" to EddsaPublicKey.randEdsaKey() - "selected_exchange" to exchangePayto.canonical - "amount" to "KUDOS:1.1" - } - }.assertOkJson<BankWithdrawalOperationPostResponse>() - client.get("/taler-integration/withdrawal-operation/$uuid") - .assertOkJson<BankWithdrawalOperationStatus> { - assertEquals(TalerAmount("KUDOS:1.1"), it.amount) - assertEquals(TalerAmount("KUDOS:10"), it.max_amount) - } - } - } - - @Test - fun selectWithFee() = bankSetup(conf = "test_with_fees.conf") { - val uuid = client.postA("/accounts/merchant/withdrawals") { - json {} - }.assertOkJson<BankAccountCreateWithdrawalResponse>().withdrawal_id - // Check insufficient fund - for (amount in listOf("KUDOS:11", "KUDOS:10", "KUDOS:0", "KUDOS:150")) { - client.post("/taler-integration/withdrawal-operation/$uuid") { - json { - "reserve_pub" to EddsaPublicKey.randEdsaKey() - "selected_exchange" to exchangePayto.canonical - "amount" to amount - } - }.assertConflict(TalerErrorCode.BANK_UNALLOWED_DEBIT) - } - - // Check OK - client.post("/taler-integration/withdrawal-operation/$uuid") { - json { - "reserve_pub" to EddsaPublicKey.randEdsaKey() - "selected_exchange" to exchangePayto.canonical - "amount" to "KUDOS:9" - } - }.assertOk() - } - - // POST /taler-integration/withdrawal-operation/UUID/abort - @Test - fun abort() = bankSetup { - // Check abort created - client.postA("/accounts/merchant/withdrawals") { - json { "amount" to "KUDOS:1" } - }.assertOkJson<BankAccountCreateWithdrawalResponse> { - val uuid = it.withdrawal_id - - // Check OK - client.postA("/taler-integration/withdrawal-operation/$uuid/abort").assertNoContent() - // Check idempotence - client.postA("/taler-integration/withdrawal-operation/$uuid/abort").assertNoContent() - } - - // Check abort selected - client.postA("/accounts/merchant/withdrawals") { - json { "amount" to "KUDOS:1" } - }.assertOkJson<BankAccountCreateWithdrawalResponse> { - val uuid = it.withdrawal_id - withdrawalSelect(uuid) - - // Check OK - client.postA("/taler-integration/withdrawal-operation/$uuid/abort").assertNoContent() - // Check idempotence - client.postA("/taler-integration/withdrawal-operation/$uuid/abort").assertNoContent() - } - - // Check abort confirmed - client.postA("/accounts/merchant/withdrawals") { - json { "amount" to "KUDOS:1" } - }.assertOkJson<BankAccountCreateWithdrawalResponse> { - val uuid = it.withdrawal_id - withdrawalSelect(uuid) - client.postA("/accounts/merchant/withdrawals/$uuid/confirm").assertNoContent() - - // Check error - client.postA("/taler-integration/withdrawal-operation/$uuid/abort") - .assertConflict(TalerErrorCode.BANK_ABORT_CONFIRM_CONFLICT) - } - - // Check bad UUID - client.postA("/taler-integration/withdrawal-operation/chocolate/abort") - .assertBadRequest(TalerErrorCode.GENERIC_PARAMETER_MALFORMED) - - // Check unknown - client.postA("/taler-integration/withdrawal-operation/${UUID.randomUUID()}/abort") - .assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) - } -} -\ No newline at end of file diff --git a/libeufin-bank/src/test/kotlin/CommonApiTest.kt b/libeufin-bank/src/test/kotlin/CommonApiTest.kt @@ -1,33 +0,0 @@ -/* - * 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/> - */ - -import io.ktor.client.request.* -import io.ktor.http.* -import org.junit.Test -import tech.libeufin.common.TalerErrorCode -import tech.libeufin.common.assertNotFound -import tech.libeufin.common.assertStatus - -class CommonApiTest { - @Test - fun commonErr() = bankSetup { - client.get("/unknown").assertNotFound(TalerErrorCode.GENERIC_ENDPOINT_UNKNOWN) - client.post("/config").assertStatus(HttpStatusCode.MethodNotAllowed, TalerErrorCode.GENERIC_METHOD_INVALID) - } -} -\ No newline at end of file diff --git a/libeufin-bank/src/test/kotlin/ConversionApiTest.kt b/libeufin-bank/src/test/kotlin/ConversionApiTest.kt @@ -1,203 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2023-2025 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/> - */ - -import io.ktor.client.request.* -import io.ktor.http.* -import org.junit.Test -import tech.libeufin.bank.* -import tech.libeufin.common.* -import tech.libeufin.common.test.* -import kotlin.test.assertEquals - -class ConversionApiTest { - // GET /conversion-info/config - @Test - fun config() = bankSetup { - client.get("/conversion-info/config").assertOkJson<ConversionConfig>() - client.get("/conversion-rate-classes/1/conversion-info/config").assertOkJson<ConversionConfig>() - client.get("/accounts/merchant/conversion-info/config").assertOkJson<ConversionConfig>() - } - - // POST /conversion-info/conversion-rate - @Test - fun conversionRate() = bankSetup { - val ok = obj { - "cashin_ratio" to "0.8" - "cashin_fee" to "KUDOS:0.02" - "cashin_tiny_amount" to "KUDOS:0.01" - "cashin_rounding_mode" to "nearest" - "cashin_min_amount" to "EUR:0" - "cashout_ratio" to "1.25" - "cashout_fee" to "EUR:0.003" - "cashout_tiny_amount" to "EUR:0.01" - "cashout_rounding_mode" to "zero" - "cashout_min_amount" to "KUDOS:0.1" - } - authRoutine(HttpMethod.Post, "/conversion-info/conversion-rate", requireAdmin = true) - authRoutine(HttpMethod.Post, "/accounts/merchant/conversion-info/conversion-rate", requireAdmin = true) - authRoutine(HttpMethod.Post, "/conversion-rate-classes/1/conversion-info/conversion-rate", requireAdmin = true) - for (prefix in sequenceOf("", "/conversion-rate-classes/1", "/accounts/merchant")) { - // Good rates - client.postAdmin("$prefix/conversion-info/conversion-rate") { - json(ok) - }.assertNoContent() - // Bad currency - client.postAdmin("$prefix/conversion-info/conversion-rate") { - json(ok) { - "cashout_fee" to "CHF:0.003" - } - }.assertBadRequest(TalerErrorCode.GENERIC_CURRENCY_MISMATCH) - // Zero tiny amount - client.postAdmin("$prefix/conversion-info/conversion-rate") { - json(ok) { - "cashout_tiny_amount" to "EUR:0" - } - }.assertBadRequest(TalerErrorCode.GENERIC_JSON_INVALID) - client.postAdmin("$prefix/conversion-info/conversion-rate") { - json(ok) { - "cashin_tiny_amount" to "KUDOS:0" - } - }.assertBadRequest(TalerErrorCode.GENERIC_JSON_INVALID) - // Subcent cashout tiny amount - client.postAdmin("$prefix/conversion-info/conversion-rate") { - json(ok) { - "cashout_tiny_amount" to "EUR:0.0001" - } - }.assertBadRequest(TalerErrorCode.GENERIC_JSON_INVALID) - } - } - - // GET /conversion-info/rate - @Test - fun userRate() = bankSetup { - authRoutine(HttpMethod.Get, "/accounts/merchant/conversion-info/rate", allowAdmin = true, optional = true) - authRoutine(HttpMethod.Get, "/conversion-rate-classes/1/conversion-info/rate", requireAdmin = true) - client.get("/conversion-info/rate").assertOkJson<ConversionRate>() - client.getA("/accounts/merchant/conversion-info/rate").assertOkJson<ConversionRate>() - client.get("/accounts/exchange/conversion-info/rate").assertOkJson<ConversionRate>() - client.getAdmin("/conversion-rate-classes/1/conversion-info/rate").assertOkJson<ConversionRate>() - } - - // GET /conversion-info/cashout-rate - @Test - fun cashoutRate() = bankSetup { - authRoutine(HttpMethod.Get, "/accounts/merchant/conversion-info/cashout-rate?amount_debit=KUDOS:1", allowAdmin = true) - authRoutine(HttpMethod.Get, "/conversion-rate-classes/1/conversion-info/cashout-rate?amount_debit=KUDOS:1", requireAdmin = true) - for (prefix in sequenceOf("", "/conversion-rate-classes/1", "/accounts/merchant")) { - // Check conversion to - client.getAdmin("$prefix/conversion-info/cashout-rate?amount_debit=KUDOS:1").assertOkJson<ConversionResponse> { - assertEquals(TalerAmount("KUDOS:1"), it.amount_debit) - assertEquals(TalerAmount("EUR:1.25"), it.amount_credit) - } - // Check conversion from - client.getAdmin("$prefix/conversion-info/cashout-rate?amount_credit=EUR:1.257").assertOkJson<ConversionResponse> { - assertEquals(TalerAmount("KUDOS:1.01"), it.amount_debit) - assertEquals(TalerAmount("EUR:1.257"), it.amount_credit) - } - - // Too small - client.getAdmin("$prefix/conversion-info/cashout-rate?amount_debit=KUDOS:0.0008") - .assertConflict(TalerErrorCode.BANK_BAD_CONVERSION) - // No amount - client.getAdmin("$prefix/conversion-info/cashout-rate") - .assertBadRequest(TalerErrorCode.GENERIC_PARAMETER_MISSING) - // Both amount - client.getAdmin("$prefix/conversion-info/cashout-rate?amount_debit=EUR:1&amount_credit=KUDOS:1") - .assertBadRequest(TalerErrorCode.GENERIC_PARAMETER_MALFORMED) - // Wrong format - client.getAdmin("$prefix/conversion-info/cashout-rate?amount_debit=1") - .assertBadRequest(TalerErrorCode.GENERIC_PARAMETER_MALFORMED) - client.getAdmin("$prefix/conversion-info/cashout-rate?amount_credit=1") - .assertBadRequest(TalerErrorCode.GENERIC_PARAMETER_MALFORMED) - // Wrong currency - client.getAdmin("$prefix/conversion-info/cashout-rate?amount_debit=EUR:1") - .assertBadRequest(TalerErrorCode.GENERIC_CURRENCY_MISMATCH) - client.getAdmin("$prefix/conversion-info/cashout-rate?amount_credit=KUDOS:1") - .assertBadRequest(TalerErrorCode.GENERIC_CURRENCY_MISMATCH) - } - client.getA("/accounts/exchange/conversion-info/cashout-rate?amount_debit=KUDOS:1") - .assertConflict(TalerErrorCode.BANK_ACCOUNT_IS_EXCHANGE) - } - - // GET /conversion-info/cashin-rate - @Test - fun cashinRate() = bankSetup { - authRoutine(HttpMethod.Get, "/conversion-rate-classes/1/conversion-info/cashin-rate?amount_debit=EUR:1", requireAdmin = true) - for (prefix in sequenceOf("", "/conversion-rate-classes/1", "/accounts/exchange")) { - for ((amount, converted) in listOf( - Pair(0.75, 0.58), Pair(0.32, 0.24), Pair(0.66, 0.51) - )) { - // Check conversion to - client.getAdmin("$prefix/conversion-info/cashin-rate?amount_debit=EUR:$amount").assertOkJson<ConversionResponse> { - assertEquals(TalerAmount("KUDOS:$converted"), it.amount_credit) - assertEquals(TalerAmount("EUR:$amount"), it.amount_debit) - } - // Check conversion from - client.getAdmin("$prefix/conversion-info/cashin-rate?amount_credit=KUDOS:$converted").assertOkJson<ConversionResponse> { - assertEquals(TalerAmount("KUDOS:$converted"), it.amount_credit) - assertEquals(TalerAmount("EUR:$amount"), it.amount_debit) - } - } - - // No amount - client.getAdmin("$prefix/conversion-info/cashin-rate") - .assertBadRequest(TalerErrorCode.GENERIC_PARAMETER_MISSING) - // Both amount - client.getAdmin("$prefix/conversion-info/cashin-rate?amount_debit=KUDOS:1&amount_credit=EUR:1") - .assertBadRequest(TalerErrorCode.GENERIC_PARAMETER_MALFORMED) - // Wrong format - client.getAdmin("$prefix/conversion-info/cashin-rate?amount_debit=1") - .assertBadRequest(TalerErrorCode.GENERIC_PARAMETER_MALFORMED) - client.getAdmin("$prefix/conversion-info/cashin-rate?amount_credit=1") - .assertBadRequest(TalerErrorCode.GENERIC_PARAMETER_MALFORMED) - // Wrong currency - client.getAdmin("$prefix/conversion-info/cashin-rate?amount_debit=KUDOS:1") - .assertBadRequest(TalerErrorCode.GENERIC_CURRENCY_MISMATCH) - client.getAdmin("$prefix/conversion-info/cashin-rate?amount_credit=EUR:1") - .assertBadRequest(TalerErrorCode.GENERIC_CURRENCY_MISMATCH) - } - client.get("/accounts/merchant/conversion-info/cashin-rate?amount_debit=EUR:1") - .assertConflict(TalerErrorCode.BANK_ACCOUNT_IS_NOT_EXCHANGE) - } - - @Test - fun noRate() = bankSetup { db -> - db.serializable("DELETE FROM config WHERE key='conversion_rate'") { - executeUpdate() - } - for (prefix in sequenceOf("", "/conversion-rate-classes/1", "/accounts/merchant")) { - client.getAdmin("$prefix/conversion-info/config") - .assertOkJson<ConversionConfig>() - client.getAdmin("$prefix/conversion-info/cashout-rate") - .assertBadRequest(TalerErrorCode.GENERIC_PARAMETER_MISSING) - client.getAdmin("$prefix/conversion-info/cashout-rate?amount_credit=EUR:1") - .assertConflict(TalerErrorCode.BANK_BAD_CONVERSION) - } - } - - @Test - fun notImplemented() = bankSetup("test_no_conversion.conf") { - for (prefix in sequenceOf("", "/conversion-rate-classes/1", "/accounts/merchant")) { - client.get("$prefix/conversion-info/cashin-rate") - .assertNotImplemented() - client.get("$prefix/conversion-info/cashout-rate") - .assertNotImplemented() - } - } -} -\ No newline at end of file diff --git a/libeufin-bank/src/test/kotlin/CoreBankApiTest.kt b/libeufin-bank/src/test/kotlin/CoreBankApiTest.kt @@ -1,2584 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2023, 2024, 2025, 2026 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/> - */ - -import io.ktor.client.request.* -import io.ktor.client.statement.* -import io.ktor.http.* -import io.ktor.server.testing.* -import kotlinx.serialization.json.JsonElement -import org.junit.Test -import tech.libeufin.bank.* -import tech.libeufin.bank.auth.TOKEN_PREFIX -import tech.libeufin.common.* -import tech.libeufin.common.crypto.CryptoUtil -import tech.libeufin.common.db.* -import tech.libeufin.common.test.* -import java.time.Duration -import java.time.Instant -import java.util.* -import kotlin.test.* - -class CoreBankSecurityTest { - @Test - fun passwordUpdate() = bankSetup { db -> - suspend fun currentHash(): String { - return db.serializable( - "SELECT password_hash FROM customers WHERE username='customer'" - ) { - one { - it.getString(1) - } - } - } - - // Set outdated hash - val password = "customer-password" - val pwh = CryptoUtil.hashStringSHA256(password).encodeBase64() - val hash = "sha256\$$pwh" - db.serializable( - "UPDATE customers SET password_hash=? WHERE username='customer'" - ) { - bind(hash) - executeUpdate() - } - assertEquals(hash, currentHash()) - - // Check hash is updated - client.getA("/accounts/customer").assertOk() - val newHash = currentHash() - assert(hash != newHash) - - // Check hash stay the same - client.getA("/accounts/customer").assertOk() - assertEquals(newHash, currentHash()) - } -} - -class CoreBankConfigTest { - // GET /config - @Test - fun config() = bankSetup { - client.get("/config").assertOk() - } - - // GET /monitor - @Test - fun monitor() = bankSetup { - authRoutine(HttpMethod.Get, "/monitor", requireAdmin = true) - // Check OK - client.getAdmin("/monitor?timeframe=day&which=25").assertOk() - client.getAdmin("/monitor?timeframe=day=which=25").assertBadRequest(TalerErrorCode.GENERIC_PARAMETER_MALFORMED) - } -} - -class CoreBankTokenApiTest { - // POST /accounts/USERNAME/token - @Test - fun post() = bankSetup { db -> - authRoutine(HttpMethod.Post, "/accounts/merchant/token") - - // Unknown account - client.post("/accounts/merchant/token") { - basicAuth("unknown", "password") - }.assertUnauthorized() - - // Wrong password - client.post("/accounts/merchant/token") { - basicAuth("merchant", "wrong-password") - }.assertUnauthorized() - - // Wrong account - client.post("/accounts/merchant/token") { - basicAuth("exchange", "merchant-password") - }.assertUnauthorized() - - // New default token - client.postPw("/accounts/merchant/token") { - json { "scope" to "readonly" } - }.assertOkJson<TokenSuccessResponse> { - // Checking that the token lifetime defaulted to 24 hours. - val token = db.token.access(Base32Crockford.decode(it.access_token.removePrefix(TOKEN_PREFIX)), Instant.now()) - val lifeTime = Duration.between(token!!.creationTime, token.expirationTime) - assertEquals(Duration.ofDays(1), lifeTime) - } - - // Check default duration - client.postPw("/accounts/merchant/token") { - json { "scope" to "readonly" } - }.assertOkJson<TokenSuccessResponse> { - // Checking that the token lifetime defaulted to 24 hours. - val token = db.token.access(Base32Crockford.decode(it.access_token.removePrefix(TOKEN_PREFIX)), Instant.now()) - val lifeTime = Duration.between(token!!.creationTime, token.expirationTime) - assertEquals(Duration.ofDays(1), lifeTime) - } - - // Check valid refresh scope - for ((fromScope, toScope) in listOf( - "readwrite" to "readwrite", - "readonly" to "readonly", - "revenue" to "revenue", - "readwrite" to "readonly", - "readwrite" to "revenue", - "readonly" to "revenue", - )) { - client.postPw("/accounts/merchant/token") { - json { - "scope" to fromScope - "refreshable" to true - } - }.assertOkJson<TokenSuccessResponse> { - val token = it.access_token - client.post("/accounts/merchant/token") { - headers[HttpHeaders.Authorization] = "Bearer $token" - json { "scope" to toScope } - }.assertOk() - } - } - - // Check invalid refresh scope - for ((fromScope, toScope) in listOf( - "readonly" to "readwrite", - "revenue" to "readonly", - "revenue" to "readwrite" - )) { - client.postPw("/accounts/merchant/token") { - json { - "scope" to fromScope - "refreshable" to true - } - }.assertOkJson<TokenSuccessResponse> { - val token = it.access_token - client.post("/accounts/merchant/token") { - headers[HttpHeaders.Authorization] = "Bearer $token" - json { "scope" to toScope } - }.assertForbidden(TalerErrorCode.GENERIC_TOKEN_PERMISSION_INSUFFICIENT) - } - } - - // Check no refreshable - client.postPw("/accounts/merchant/token") { - json { - "scope" to "readonly" - } - }.assertOkJson<TokenSuccessResponse> { - val token = it.access_token - client.post("/accounts/merchant/token") { - headers[HttpHeaders.Authorization] = "Bearer $token" - json { "scope" to "readonly" } - }.assertForbidden(TalerErrorCode.GENERIC_TOKEN_PERMISSION_INSUFFICIENT) - } - - // Check 'forever' case. - client.postPw("/accounts/merchant/token") { - json { - "scope" to "readonly" - "duration" to obj { - "d_us" to "forever" - } - } - }.assertOkJson<TokenSuccessResponse> { - assertEquals(Instant.MAX, it.expiration.instant) - } - - // Check too big or invalid durations - client.postPw("/accounts/merchant/token") { - json { - "scope" to "readonly" - "duration" to obj { - "d_us" to "invalid" - } - } - }.assertBadRequest() - client.postPw("/accounts/merchant/token") { - json { - "scope" to "readonly" - "duration" to obj { - "d_us" to Long.MAX_VALUE - } - } - }.assertBadRequest() - client.postPw("/accounts/merchant/token") { - json { - "scope" to "readonly" - "duration" to obj { - "d_us" to -1 - } - } - }.assertBadRequest() - } - - @Test - fun post2FA() = bankSetup { db -> - // Setup a known phone 2FA - client.patchA("/accounts/merchant") { - json { - "contact_data" to obj { - "phone" to "+12345" - } - "tan_channel" to "sms" - } - }.assertChallenge().assertNoContent() - - // Check creating a token requires to solve an unauthenticated challenge - val challenge = client.postPw("/accounts/merchant/token") { - json { "scope" to "readonly" } - }.assertAcceptedJson<ChallengeResponse>().challenges[0] - client.post("/accounts/merchant/challenge/${challenge.challenge_id}") - .assertOk() - assertEquals("REDACTED", challenge.tan_info) // Check phone number is hidden - val code = tanCode("+12345") - client.post("/accounts/merchant/challenge/${challenge.challenge_id}/confirm") { - json { "tan" to code } - }.assertNoContent() - client.postPw("/accounts/merchant/token") { - headers[TALER_CHALLENGE_IDS] = "${challenge.challenge_id}" - json { "scope" to "readonly" } - }.assertOkJson<TokenSuccessResponse>() - } - - @Test - fun locked() = bankSetup { db -> - // Setup a known phone 2FA - client.patchA("/accounts/merchant") { - json { - "contact_data" to obj { - "phone" to "+12345" - } - "tan_channel" to "sms" - } - }.assertChallenge().assertNoContent() - - suspend fun blockAccount() { - var counter = MAX_TOKEN_CREATION_ATTEMPTS + 1 - while (counter > 0) { - val challenge = client.postPw("/accounts/merchant/token") { - json { "scope" to "readonly" } - }.assertAcceptedJson<ChallengeResponse>().challenges[0] - client.post("/accounts/merchant/challenge/${challenge.challenge_id}") - .assertOk() - while (counter > 0) { - val error = client.post("/accounts/merchant/challenge/${challenge.challenge_id}/confirm"){ - json { "tan" to "bad code" } - }.json<TalerError>() - counter -= 1 - when (error.code) { - TalerErrorCode.BANK_TAN_CHALLENGE_FAILED.code -> continue - TalerErrorCode.BANK_TAN_RATE_LIMITED.code, TalerErrorCode.BANK_TAN_CHALLENGE_EXPIRED.code -> break - else -> throw Exception("$error") - } - } - } - client.postPw("/accounts/merchant/token") { - json { "scope" to "readonly" } - }.assertForbidden(TalerErrorCode.BANK_ACCOUNT_LOCKED) - } - - blockAccount() - - // Check token still works - client.getA("/accounts/merchant").assertOkJson<AccountData> { - assertTrue(it.is_locked) - } - - // Check admin can unlock - client.patchAdmin("/accounts/merchant/auth") { - json { - "new_password" to "merchant-password" - } - }.assertNoContent() - client.getA("/accounts/merchant").assertOkJson<AccountData> { - assertFalse(it.is_locked) - } - blockAccount() - - // Check token can unlock - client.patchA("/accounts/merchant/auth") { - json { - "old_password" to "merchant-password" - "new_password" to "merchant-password" - } - }.assertChallenge().assertNoContent() - client.getA("/accounts/merchant").assertOkJson<AccountData> { - assertFalse(it.is_locked) - } - } - - // DELETE /accounts/USERNAME/token - @Test - fun delete() = bankSetup { - val token = client.postPw("/accounts/merchant/token") { - json { "scope" to "readonly" } - }.assertOkJson<TokenSuccessResponse>().access_token - // Check OK - client.delete("/accounts/merchant/token") { - headers[HttpHeaders.Authorization] = "Bearer $token" - }.assertNoContent() - // Check token no longer work - client.delete("/accounts/merchant/token") { - headers[HttpHeaders.Authorization] = "Bearer $token" - }.assertUnauthorized(TalerErrorCode.GENERIC_TOKEN_UNKNOWN) - } - - // DELETE /accounts/USERNAME/tokens/TOKEN_ID - @Test - fun deleteById() = bankSetup { - authRoutine(HttpMethod.Delete, "/accounts/merchant/tokens/1t", allowAdmin = true) - - val token = client.postPw("/accounts/merchant/token") { - json { "scope" to "readonly" } - }.assertOkJson<TokenSuccessResponse>().access_token - // Check OK - client.deleteA("/accounts/merchant/tokens/2").assertNoContent() - client.deleteA("/accounts/merchant/tokens/2").assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) - // Check token no longer work - client.delete("/accounts/merchant/token") { - headers[HttpHeaders.Authorization] = "Bearer $token" - }.assertUnauthorized(TalerErrorCode.GENERIC_TOKEN_UNKNOWN) - - // A user cannot delete another account's token by putting its ID - // below a path the user does own. - val customerToken = client.postPw("/accounts/customer/token") { - json { "scope" to "readonly" } - }.assertOkJson<TokenSuccessResponse>().access_token - val customerTokenId = client.getA("/accounts/customer/tokens") - .assertOkJson<TokenInfos>().tokens.first().token_id - client.deleteA("/accounts/merchant/tokens/$customerTokenId") - .assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) - client.get("/accounts/customer") { - headers[HttpHeaders.Authorization] = "Bearer $customerToken" - }.assertOk() - - // Administrators can delete it when the owning account is explicit. - client.deleteAdmin("/accounts/customer/tokens/$customerTokenId") - .assertNoContent() - } - - @Test - fun refreshOverlapIsFixedAndNonSliding() = bankSetup { db -> - val source = client.postPw("/accounts/merchant/token") { - json { - "scope" to "readonly" - "refreshable" to true - "duration" to obj { "d_us" to 86_400_000_000L } - "description" to "refresh source" - } - }.assertOkJson<TokenSuccessResponse>().access_token - val sourceId = client.getA("/accounts/merchant/tokens") - .assertOkJson<TokenInfos>().tokens - .single { it.description == "refresh source" }.token_id - - suspend fun sourceExpiration(): Instant = db.serializable( - "SELECT expiration_time FROM bearer_tokens WHERE bearer_token_id=?" - ) { - bind(sourceId) - one { it.getLong(1).asInstant() } - } - - val beforeRefresh = Instant.now() - repeat(2) { - client.post("/accounts/merchant/token") { - headers[HttpHeaders.Authorization] = "Bearer $source" - json { - "scope" to "readonly" - "description" to "replacement-$it" - } - }.assertOk() - } - val firstDeadline = sourceExpiration() - assertFalse(firstDeadline.isBefore(beforeRefresh + Duration.ofMinutes(5))) - assertFalse(firstDeadline.isAfter(Instant.now() + Duration.ofMinutes(5))) - - // A retry after a lost response may create another replacement, but - // must not extend the original token's overlap deadline. - client.post("/accounts/merchant/token") { - headers[HttpHeaders.Authorization] = "Bearer $source" - json { - "scope" to "readonly" - "description" to "replacement-lost-response-retry" - } - }.assertOk() - assertEquals(firstDeadline, sourceExpiration()) - val replacements = client.getA("/accounts/merchant/tokens") - .assertOkJson<TokenInfos>().tokens - .count { it.description?.startsWith("replacement-") == true } - assertEquals(3, replacements) - } - - // GET /accounts/USERNAME/tokens - @Test - fun get() = bankSetup { - // Check OK - for (account in listOf("merchant", "customer")) { - client.getA("/accounts/$account/tokens").assertOkJson<TokenInfos> { - assertEquals(1, it.tokens.size) - } - } - client.postPw("/accounts/merchant/token") { - json { "scope" to "readonly" } - }.assertOk() - client.postPw("/accounts/merchant/token") { - json { "scope" to "readwrite" } - }.assertOk() - client.postPw("/accounts/customer/token") { - json { - "scope" to "revenue" - "description" to "description" - } - }.assertOk() - client.getA("/accounts/merchant/tokens").assertOkJson<TokenInfos> { - assertEquals(3, it.tokens.size) - for (token in it.tokens) { - assertNull(token.description) - } - } - client.getA("/accounts/customer/tokens").assertOkJson<TokenInfos> { - assertEquals(2, it.tokens.size) - assertEquals("description", it.tokens[0].description) - } - val serialized = client.getA("/accounts/customer/tokens") - .assertOk().bodyAsText() - assertContains(serialized, "\"refreshable\"") - assertFalse(serialized.contains("\"isRefreshable\"")) - } -} - -class CoreBankAccountsApiTest { - // POST /accounts - @Test - fun create() = bankSetup { - // Check generated payto - obj { - "username" to "john" - "password" to "password" - "name" to "John" - }.let { req -> - // Check Ok - val payto = client.post("/accounts") { - json(req) - }.assertOkJson<RegisterAccountResponse>().internal_payto_uri - // Check idempotency - client.post("/accounts") { - json(req) - }.assertOkJson<RegisterAccountResponse> { - assertEquals(payto, it.internal_payto_uri) - } - // Check idempotency with payto - client.post("/accounts") { - json(req) { - "payto_uri" to payto - } - }.assertOk() - // Check payto conflict - client.post("/accounts") { - json(req) { - "payto_uri" to IbanPayto.rand() - } - }.assertConflict(TalerErrorCode.BANK_REGISTER_USERNAME_REUSE) - } - - // Check given payto - val payto = IbanPayto.rand() - val req = obj { - "username" to "foo" - "password" to "password" - "name" to "Jane" - "is_public" to true - "payto_uri" to payto - "is_taler_exchange" to true - } - // Check Ok - client.post("/accounts") { - json(req) - }.assertOkJson<RegisterAccountResponse> { - assertEquals(payto.full("Jane"), it.internal_payto_uri) - } - // Testing idempotency - client.post("/accounts") { - json(req) - }.assertOkJson<RegisterAccountResponse> { - assertEquals(payto.full("Jane"), it.internal_payto_uri) - } - // Check admin only debit_threshold - obj { - "username" to "bat" - "password" to "password" - "name" to "Bat" - "debit_threshold" to "KUDOS:42" - }.let { req -> - client.post("/accounts") { - json(req) - }.assertConflict(TalerErrorCode.BANK_NON_ADMIN_PATCH_DEBT_LIMIT) - client.postAdmin("/accounts") { - json(req) - }.assertOk() - } - - // Check admin only conversion_rate_class_id - createConversionRateClass() - obj { - "username" to "bat2" - "password" to "password" - "name" to "Bat" - "conversion_rate_class_id" to 1 - }.let { req -> - client.post("/accounts") { - json(req) - }.assertConflict(TalerErrorCode.BANK_NON_ADMIN_SET_CONVERSION_RATE_CLASS) - client.postAdmin("/accounts") { - json(req) - }.assertOk() - } - - // Check admin only tan_channel - obj { - "username" to "bat3" - "password" to "password" - "name" to "Bat" - "contact_data" to obj { - "phone" to "+456" - } - "tan_channel" to "sms" - }.let { req -> - client.post("/accounts") { - json(req) - }.assertConflict(TalerErrorCode.BANK_NON_ADMIN_SET_TAN_CHANNEL) - client.postAdmin("/accounts") { - json(req) - }.assertOk() - } - - // Check both tan channels - client.postAdmin("/accounts") { - json { - "username" to "bat2" - "password" to "password" - "name" to "Bat" - "tan_channel" to "sms" - "tan_channels" to emptyList<String>() - } - }.assertBadRequest() - - // Check tan info - val channels = listOf("sms", "email") - for (channel in channels) { - client.postAdmin("/accounts") { - json { - "username" to "bat2" - "password" to "password" - "name" to "Bat" - "tan_channel" to channel - } - }.assertConflict(TalerErrorCode.BANK_MISSING_TAN_INFO) - client.postAdmin("/accounts") { - json { - "username" to "bat2" - "password" to "password" - "name" to "Bat" - "tan_channels" to listOf(channel) - } - }.assertConflict(TalerErrorCode.BANK_MISSING_TAN_INFO) - } - client.postAdmin("/accounts") { - json { - "username" to "bat2" - "password" to "password" - "name" to "Bat" - "tan_channels" to channels - } - }.assertConflict(TalerErrorCode.BANK_MISSING_TAN_INFO) - - // Check unknown conversion rate class - client.postAdmin("/accounts") { - json { - "username" to "new_account" - "password" to "password" - "name" to "New Account" - "conversion_rate_class_id" to 42 - } - }.assertConflict(TalerErrorCode.BANK_CONVERSION_RATE_CLASS_UNKNOWN) - - // Reserved account - RESERVED_ACCOUNTS.forEach { - client.post("/accounts") { - json { - "username" to it - "password" to "password" - "name" to "John Smith" - } - }.assertConflict(TalerErrorCode.BANK_RESERVED_USERNAME_CONFLICT) - } - - // Malformed username - listOf("bad@username", "long".repeat(50)).forEach { - client.post("/accounts") { - json { - "username" to it - "password" to "password" - "name" to "John Smith" - } - }.assertBadRequest() - } - - // Non exchange account - client.post("/accounts") { - json { - "username" to "exchange" - "password" to "password" - "name" to "Exchange" - } - }.assertConflict(TalerErrorCode.END) - - // Testing username conflict - client.post("/accounts") { - json(req) { - "name" to "Foo" - } - }.assertConflict(TalerErrorCode.BANK_REGISTER_USERNAME_REUSE) - // Testing payto conflict - client.post("/accounts") { - json(req) { - "username" to "bar" - } - }.assertConflict(TalerErrorCode.BANK_REGISTER_PAYTO_URI_REUSE) - client.getAdmin("/accounts/bar").assertNotFound(TalerErrorCode.BANK_UNKNOWN_ACCOUNT) - // Testing bad payto kind - client.post("/accounts") { - json(req) { - "username" to "bar" - "password" to "bar-password" - "name" to "Mr Bar" - "payto_uri" to "payto://x-taler-bank/bank.hostname.test/bar" - } - }.assertBadRequest() - // Testing short password - client.post("/accounts") { - json(req) { - "password" to "short" - } - }.assertConflict(TalerErrorCode.BANK_PASSWORD_TOO_SHORT) - // Testing long password - client.post("/accounts") { - json(req) { - "password" to "loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong-password" - } - }.assertConflict(TalerErrorCode.BANK_PASSWORD_TOO_LONG) - - // Check cashout payto receiver name logic - client.post("/accounts") { - json { - "username" to "cashout_guess" - "password" to "cashout_guess-password" - "name" to "Mr Guess My Name" - "cashout_payto_uri" to payto - } - }.assertOk() - client.getA("/accounts/cashout_guess").assertOkJson<AccountData> { - assertEquals(payto.full("Mr Guess My Name"), it.cashout_payto_uri) - } - client.post("/accounts") { - json { - "username" to "cashout_keep" - "password" to "cashout_keep-password" - "name" to "Mr Keep My Name" - "cashout_payto_uri" to payto.full("Santa Claus") - } - }.assertOk() - client.getA("/accounts/cashout_keep").assertOkJson<AccountData> { - assertEquals(payto.full("Mr Keep My Name"), it.cashout_payto_uri) - } - - // Check input restrictions - obj { - "username" to "username" - "password" to "password" - "name" to "Name" - }.let { req -> - client.post("/accounts") { - json(req) { "username" to "bad/username" } - }.assertBadRequest() - client.post("/accounts") { - json(req) { "username" to " spaces " } - }.assertBadRequest() - client.post("/accounts") { - json(req) { - "contact_data" to obj { - "phone" to " +456" - } - } - }.assertBadRequest() - client.post("/accounts") { - json(req) { - "contact_data" to obj { - "phone" to " test@mail.com" - } - } - }.assertBadRequest() - } - } - - // Test account created with bonus - @Test - fun createBonus() = bankSetup(conf = "test_bonus.conf") { - val req = obj { - "username" to "foo" - "password" to "password-xyz" - "name" to "Mallory" - } - - setMaxDebt("admin", "KUDOS:10000") - - // Check ok - repeat(100) { - client.postAdmin("/accounts") { - json(req) { - "username" to "foo$it" - } - }.assertOk() - assertBalance("foo$it", "+KUDOS:100") - } - assertBalance("admin", "-KUDOS:10000") - - // Check insufficient fund - client.postAdmin("/accounts") { - json(req) { - "username" to "bar" - } - }.assertConflict(TalerErrorCode.BANK_UNALLOWED_DEBIT) - client.getAdmin("/accounts/bar").assertNotFound(TalerErrorCode.BANK_UNKNOWN_ACCOUNT) - } - - // Test admin-only account creation - @Test - fun createRestricted() = bankSetup(conf = "test_restrict.conf") { - authRoutine(HttpMethod.Post, "/accounts", requireAdmin = true) - client.postAdmin("/accounts") { - json { - "username" to "baz" - "password" to "password-xyz" - "name" to "Mallory" - } - }.assertOk() - } - - // Test admin-only account creation - @Test - fun createTanErr() = bankSetup(conf = "test_tan_err.conf") { - client.postAdmin("/accounts") { - json { - "username" to "baz" - "password" to "xyz" - "name" to "Mallory" - "tan_channel" to "email" - } - }.assertConflict(TalerErrorCode.BANK_TAN_CHANNEL_NOT_SUPPORTED) - } - - // POST /accounts - @Test - fun createNoCheck() = bankSetup("test_no_password_check.conf") { - // Testing short password - client.post("/accounts") { - json { - "username" to "short" - "name" to "John Smith" - "password" to "short" - } - }.assertOk() - // Testing long password - client.post("/accounts") { - json { - "username" to "long" - "name" to "Jane Smith" - "password" to "loooooooooooooooooooooooooooooooooooooooooooooooooooooooong-password" - } - }.assertOk() - } - - // DELETE /accounts/USERNAME - @Test - fun delete() = bankSetup { db -> - authRoutine(HttpMethod.Delete, "/accounts/merchant", allowAdmin = true) - - // Reserved account - RESERVED_ACCOUNTS.forEach { - client.deleteAdmin("/accounts/$it") - .assertConflict(TalerErrorCode.BANK_RESERVED_USERNAME_CONFLICT) - } - client.deleteA("/accounts/exchange") - .assertConflict(TalerErrorCode.BANK_RESERVED_USERNAME_CONFLICT) - - client.post("/accounts") { - json { - "username" to "john" - "password" to "john-password" - "name" to "John" - "payto_uri" to genTmpPayTo() - } - }.assertOk() - fillTanInfo("john") - // Fail to delete, due to a non-zero balance. - tx("customer", "KUDOS:1", "john") - client.deleteA("/accounts/john") - .assertConflict(TalerErrorCode.BANK_ACCOUNT_BALANCE_NOT_ZERO) - // Successful deletion - tx("john", "KUDOS:1", "customer") - client.deleteA("/accounts/john") - .assertChallenge() - .assertNoContent() - // Account no longer exists - client.deleteA("/accounts/john") - .assertUnauthorized(TalerErrorCode.GENERIC_TOKEN_UNKNOWN) - client.deleteAdmin("/accounts/john") - .assertNotFound(TalerErrorCode.BANK_UNKNOWN_ACCOUNT) - } - - @Test - fun softDelete() = bankSetup { db -> - // Create all kind of operations - val token = client.postPw("/accounts/customer/token") { - json { "scope" to "readonly" } - }.assertOkJson<TokenSuccessResponse>().access_token - val tx_id = client.postA("/accounts/customer/transactions") { - json { - "payto_uri" to "$exchangePayto?message=payout" - "amount" to "KUDOS:0.3" - } - }.assertOkJson<TransactionCreateResponse>().row_id - val withdrawal_id = client.postA("/accounts/customer/withdrawals") { - json { "amount" to "KUDOS:9.0" } - }.assertOkJson<BankAccountCreateWithdrawalResponse>().withdrawal_id - fillCashoutInfo("customer") - val cashout_id = client.postA("/accounts/customer/cashouts") { - json { - "request_uid" to ShortHashCode.rand() - "amount_debit" to "KUDOS:1" - "amount_credit" to convert("KUDOS:1") - } - }.assertOkJson<CashoutResponse>().cashout_id - fillTanInfo("customer") - client.postA("/accounts/customer/transactions") { - json { - "payto_uri" to "$exchangePayto?message=payout" - "amount" to "KUDOS:0.3" - } - }.assertAcceptedJson<ChallengeResponse>() - - // Delete account - tx("merchant", "KUDOS:1", "customer") - assertBalance("customer", "+KUDOS:0") - client.deleteA("/accounts/customer") - .assertChallenge() - .assertNoContent() - - // Check account can no longer username - client.delete("/accounts/customer/token") { - headers[HttpHeaders.Authorization] = "Bearer $token" - }.assertUnauthorized(TalerErrorCode.GENERIC_TOKEN_UNKNOWN) - client.getA("/accounts/customer/transactions/$tx_id") - .assertUnauthorized(TalerErrorCode.GENERIC_TOKEN_UNKNOWN) - client.getA("/accounts/customer/cashouts/$cashout_id") - .assertUnauthorized(TalerErrorCode.GENERIC_TOKEN_UNKNOWN) - client.postA("/accounts/customer/withdrawals/$withdrawal_id/confirm") - .assertUnauthorized(TalerErrorCode.GENERIC_TOKEN_UNKNOWN) - - // But admin can still see existing operations - client.getAdmin("/accounts/customer/transactions/$tx_id") - .assertOkJson<BankAccountTransactionInfo>() - client.getAdmin("/accounts/customer/cashouts/$cashout_id") - .assertOkJson<CashoutStatusResponse>() - client.get("/withdrawals/$withdrawal_id") - .assertOkJson<WithdrawalPublicInfo>() - - // GC - db.gc.collect(Instant.now(), Duration.ZERO, Duration.ZERO, Duration.ZERO) - client.getAdmin("/accounts/customer/transactions/$tx_id") - .assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) - client.getAdmin("/accounts/customer/cashouts/$cashout_id") - .assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) - client.get("/withdrawals/$withdrawal_id") - .assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) - } - - // Test admin-only account deletion - @Test - fun deleteRestricted() = bankSetup(conf = "test_restrict.conf") { - authRoutine(HttpMethod.Post, "/accounts", requireAdmin = true) - // Exchange is still restricted - client.deleteAdmin("/accounts/exchange") { - }.assertConflict(TalerErrorCode.BANK_RESERVED_USERNAME_CONFLICT) - } - - // Test delete exchange account - @Test - fun deleteNoConversion() = bankSetup(conf = "test_no_conversion.conf") { - // Exchange is no longer restricted - client.deleteA("/accounts/exchange").assertNoContent() - } - - suspend fun ApplicationTestBuilder.checkAdminOnly( - req: JsonElement, - error: TalerErrorCode - ) { - // Check restricted - client.patchA("/accounts/merchant") { - json(req) - }.assertConflict(error) - // Check admin always can - client.patchAdmin("/accounts/merchant") { - json(req) - }.assertNoContent() - // Check idempotent - client.patchA("/accounts/merchant") { - json(req) - }.assertNoContent() - } - - // PATCH /accounts/USERNAME - @Test - fun reconfig() = bankSetup { - authRoutine(HttpMethod.Patch, "/accounts/merchant", allowAdmin = true) - - // Check tan info - val channels = listOf("sms", "email") - for (channel in channels) { - client.patchA("/accounts/merchant") { - json { "tan_channel" to channel } - }.assertConflict(TalerErrorCode.BANK_MISSING_TAN_INFO) - client.patchA("/accounts/merchant") { - json { "tan_channels" to listOf(channel) } - }.assertConflict(TalerErrorCode.BANK_MISSING_TAN_INFO) - } - client.patchA("/accounts/merchant") { - json { "tan_channels" to channels } - }.assertConflict(TalerErrorCode.BANK_MISSING_TAN_INFO) - - // Successful attempt now - val cashout = IbanPayto.rand() - val req = obj { - "cashout_payto_uri" to cashout - "name" to "Roger" - "is_public" to true - "contact_data" to obj { - "phone" to "+99" - "email" to "foo@example.com" - } - } - client.patchA("/accounts/merchant") { - json(req) - }.assertNoContent() - // Checking idempotence - client.patchA("/accounts/merchant") { - json(req) - }.assertNoContent() - - checkAdminOnly( - obj(req) { "debit_threshold" to "KUDOS:100" }, - TalerErrorCode.BANK_NON_ADMIN_PATCH_DEBT_LIMIT - ) - createConversionRateClass() - checkAdminOnly( - obj(req) { "conversion_rate_class_id" to 1 }, - TalerErrorCode.BANK_NON_ADMIN_SET_CONVERSION_RATE_CLASS - ) - - // Check unknown conversion rate class - client.patchAdmin("/accounts/merchant") { - json(req) { "conversion_rate_class_id" to 42} - }.assertConflict(TalerErrorCode.BANK_CONVERSION_RATE_CLASS_UNKNOWN) - - // Check currency - client.patchAdmin("/accounts/merchant") { - json(req) { "debit_threshold" to "EUR:100" } - }.assertBadRequest(TalerErrorCode.GENERIC_CURRENCY_MISMATCH) - - // Check patch - client.getA("/accounts/merchant").assertOkJson<AccountData> { obj -> - assertEquals("Roger", obj.name) - assertEquals(cashout.full(obj.name), obj.cashout_payto_uri) - assertEquals("+99", obj.contact_data?.phone?.get()) - assertEquals("foo@example.com", obj.contact_data?.email?.get()) - assertEquals(TalerAmount("KUDOS:100"), obj.debit_threshold) - assert(obj.is_public) - assert(!obj.is_taler_exchange) - } - - // Check keep values when there is no changes - client.patchA("/accounts/merchant") { - json { } - }.assertNoContent() - client.getA("/accounts/merchant").assertOkJson<AccountData> { obj -> - assertEquals("Roger", obj.name) - assertEquals(cashout.full(obj.name), obj.cashout_payto_uri) - assertEquals("+99", obj.contact_data?.phone?.get()) - assertEquals("foo@example.com", obj.contact_data?.email?.get()) - assertEquals(TalerAmount("KUDOS:100"), obj.debit_threshold) - assert(obj.is_public) - assert(!obj.is_taler_exchange) - } - - // Admin cannot be public - client.patchA("/accounts/admin") { - json { - "is_public" to true - } - }.assertConflict(TalerErrorCode.END) - - // Exchange must be exchange - client.patchA("/accounts/exchange") { - json { - "is_taler_exchange" to false - } - }.assertConflict(TalerErrorCode.END) - - // Check cashout payto receiver name logic - client.post("/accounts") { - json { - "username" to "cashout" - "password" to "cashout-password" - "name" to "Mr Cashout Cashout" - } - }.assertOk() - val canonical = Payto.parse(cashout.canonical).expectIban() - for ((cashout, name, expect) in listOf( - Triple(cashout.canonical, null, canonical.full("Mr Cashout Cashout")), - Triple(cashout.canonical, "New name", canonical.full("New name")), - Triple(cashout.full("Full name"), null, cashout.full("New name")), - Triple(cashout.full("Full second name"), "Another name", cashout.full("Another name")) - )) { - client.patchAdmin("/accounts/cashout") { - json { - "cashout_payto_uri" to cashout - if (name != null) "name" to name - } - }.assertNoContent() - client.getA("/accounts/cashout").assertOkJson<AccountData> { obj -> - assertEquals(expect, obj.cashout_payto_uri) - } - } - - // Check 2FA - fillTanInfo("merchant") - client.patchA("/accounts/merchant") { - json { "is_public" to false } - }.assertChallenge { - client.getA("/accounts/merchant").assertOkJson<AccountData> { obj -> - assert(obj.is_public) - } - }.assertNoContent() - client.getA("/accounts/merchant").assertOkJson<AccountData> { obj -> - assert(!obj.is_public) - } - } - - // Test admin-only account patch - @Test - fun patchRestricted() = bankSetup(conf = "test_restrict.conf") { - // Check restricted - checkAdminOnly( - obj { "name" to "Another Foo" }, - TalerErrorCode.BANK_NON_ADMIN_PATCH_LEGAL_NAME - ) - checkAdminOnly( - obj { "cashout_payto_uri" to IbanPayto.rand() }, - TalerErrorCode.BANK_NON_ADMIN_PATCH_CASHOUT - ) - // Check idempotent - client.getA("/accounts/merchant").assertOkJson<AccountData> { obj -> - client.patchA("/accounts/merchant") { - json { - "name" to obj.name - "cashout_payto_uri" to obj.cashout_payto_uri - "debit_threshold" to obj.debit_threshold - } - }.assertNoContent() - } - } - - // Test TAN check account patch - @Test - fun patchTanErr() = bankSetup(conf = "test_tan_err.conf") { - // Check unsupported TAN channel - client.patchA("/accounts/customer") { - json { - "tan_channel" to "email" - } - }.assertConflict(TalerErrorCode.BANK_TAN_CHANNEL_NOT_SUPPORTED) - } - - // PATCH /accounts/USERNAME/auth - @Test - fun passwordChange() = bankSetup { - authRoutine(HttpMethod.Patch, "/accounts/merchant/auth", allowAdmin = true) - - // Changing the password. - client.patchA("/accounts/customer/auth") { - json { - "old_password" to "customer-password" - "new_password" to "new-password" - } - }.assertNoContent() - // Previous password should fail. - client.post("/accounts/customer/token") { - basicAuth("customer", "customer-password") - }.assertUnauthorized() - // New password should succeed. - client.post("/accounts/customer/token") { - basicAuth("customer", "new-password") - json { "scope" to "readonly" } - }.assertOk() - client.patchA("/accounts/customer/auth") { - json { - "old_password" to "new-password" - "new_password" to "customer-password" - } - }.assertNoContent() - - - // Check require test old password - client.patchA("/accounts/customer/auth") { - json { - "old_password" to "bad-password" - "new_password" to "new-password" - } - }.assertConflict(TalerErrorCode.BANK_PATCH_BAD_OLD_PASSWORD) - - // Check require old password for user - client.patchA("/accounts/customer/auth") { - json { - "new_password" to "new-password" - } - }.assertConflict(TalerErrorCode.BANK_NON_ADMIN_PATCH_MISSING_OLD_PASSWORD) - // Testing short password - client.patchA("/accounts/merchant/auth") { - json { - "old_password" to "ignored" - "new_password" to "short" - } - }.assertConflict(TalerErrorCode.BANK_PASSWORD_TOO_SHORT) - // Testing long password - client.patchA("/accounts/merchant/auth") { - json { - "old_password" to "ignored" - "new_password" to "loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong-password" - } - }.assertConflict(TalerErrorCode.BANK_PASSWORD_TOO_LONG) - - // Check admin - client.patchAdmin("/accounts/customer/auth") { - json { - "new_password" to "customer-password" - } - }.assertNoContent() - - // Check 2FA - fillTanInfo("customer") - client.patchA("/accounts/customer/auth") { - json { - "old_password" to "customer-password" - "new_password" to "it-password" - } - }.assertChallenge().assertNoContent() - client.patchAdmin("/accounts/customer/auth") { - json { - "new_password" to "new-password" - } - }.assertNoContent() - - - // Check 2FA after password check - client.patchA("/accounts/customer/auth") { - json { - "old_password" to "password" - "new_password" to "new-password" - } - }.assertConflict(TalerErrorCode.BANK_PATCH_BAD_OLD_PASSWORD) - } - - // PATCH /accounts/USERNAME/auth - @Test - fun passwordChangeNoCheck() = bankSetup("test_no_password_check.conf") { - // Testing short password - client.patchA("/accounts/merchant/auth") { - json { - "old_password" to "merchant-password" - "new_password" to "short" - } - }.assertNoContent() - // Testing long password - client.patchA("/accounts/merchant/auth") { - json { - "old_password" to "short" - "new_password" to "looooooooooooooooooooooooooooooooooooooooooooooooooooooooong-password" - } - }.assertNoContent() - } - - // GET /public-accounts and GET /accounts - @Test - fun list() = bankSetup(conf = "test_no_conversion.conf") { db -> - authRoutine(HttpMethod.Get, "/accounts", requireAdmin = true) - // Remove default accounts - val defaultAccounts = listOf("merchant", "exchange", "customer") - defaultAccounts.forEach { - client.deleteAdmin("/accounts/$it").assertNoContent() - } - client.getAdmin("/accounts").assertOkJson<ListBankAccountsResponse> { - for (account in it.accounts) { - assertNull(account.conversion_rate) - if (defaultAccounts.contains(account.username)) { - assertEquals(AccountStatus.deleted, account.status) - } else { - assertEquals(AccountStatus.active, account.status) - } - } - } - db.gc.collect(Instant.now(), Duration.ZERO, Duration.ZERO, Duration.ZERO) - // Check error when no public accounts - client.get("/public-accounts").assertNoContent() - client.getAdmin("/accounts").assertOkJson<ListBankAccountsResponse>() - } - - @Test - fun listConversionClass() = bankSetup(conf = "test.conf") { db -> - repeat(3) { - createConversionRateClass() - } - - // Gen some public and private accounts - repeat(5) { - client.postAdmin("/accounts") { - val mod = it%3 - val rateClassId = if (mod in 1..3) mod else null - json { - "username" to "$it" - "password" to "password" - "name" to "Mr 1$it" - "is_public" to (it%2 == 0) - "conversion_rate_class_id" to rateClassId - } - }.assertOk() - } - // All public - client.get("/public-accounts").assertOkJson<PublicAccountsResponse> { - assertEquals(3, it.public_accounts.size) - it.public_accounts.forEach { - assertEquals(0, (it.username.toInt() - 10) % 2) - } - } - // Conversion rate - client.getAdmin("/accounts").assertOkJson<ListBankAccountsResponse> { - for (account in it.accounts) { - val rate = client.getAdmin("/accounts/${account.username}/conversion-info/rate").assertOkJson<ConversionRate>() - assertEquals(account.conversion_rate, rate) - } - } - // Filtering - suspend fun checkIds(query: String, vararg ids: String) { - val res = client.getAdmin("/accounts?$query") - val list = listOf(*ids) - if (list.isEmpty()) { - res.assertNoContent() - } else { - res.assertOkJson<ListBankAccountsResponse> { - assertEquals(list, it.accounts.map { it.username }) - } - } - } - checkIds("", "4", "3", "2", "1", "0", "admin", "customer", "exchange", "merchant") - checkIds("filter_name=1", "4", "3", "2", "1", "0") - checkIds("filter_name=3", "3") - checkIds("conversion_rate_class_id=1", "4", "1") - checkIds("conversion_rate_class_id=2", "2") - checkIds("conversion_rate_class_id=3") - checkIds("conversion_rate_class_id=4") - checkIds("conversion_rate_class_id=0", "3", "0", "admin", "customer", "exchange", "merchant") - checkIds("conversion_rate_class_id=0&filter_name=1", "3", "0") - for ((id, num) in mapOf(1 to 2, 2 to 1, 3 to 0)) { - client.getAdmin("/conversion-rate-classes/$id").assertOkJson<ConversionRateClass> { - assertEquals(it.num_users, num) - } - } - } - - // GET /accounts/USERNAME - @Test - fun get() = bankSetup { - authRoutine(HttpMethod.Get, "/accounts/merchant", allowAdmin = true) - // Check ok - client.getA("/accounts/merchant").assertOkJson<AccountData> { - assertEquals("Merchant", it.name) - } - } -} - -class CoreBankTransactionsApiTest { - // GET /transactions - @Test - fun history() = bankSetup { - historyRoutine<BankAccountTransactionsResponse>( - url = "/accounts/customer/transactions", - ids = { it.transactions.map { it.row_id } }, - registered = listOf( - { - // Transactions from merchant to exchange - tx("merchant", "KUDOS:0.1", "customer") - }, - { - // Transactions from exchange to merchant - tx("customer", "KUDOS:0.1", "merchant") - }, - { - // Transactions from merchant to exchange - tx("merchant", "KUDOS:0.1", "customer") - }, - { - // Cashout from merchant - cashout("KUDOS:0.1") - } - ), - ignored = listOf( - { - // Ignore transactions of other accounts - tx("merchant", "KUDOS:0.1", "exchange") - }, - { - // Ignore transactions of other accounts - tx("exchange", "KUDOS:0.1", "merchant") - } - ) - ) - } - - @Test - fun publicHistoryIsAnonymousAndPrivateHistoryIsHidden() = bankSetup { - tx("merchant", "KUDOS:0.1", "customer", "public subject") - client.patchAdmin("/accounts/merchant") { - json { "is_public" to true } - }.assertNoContent() - - val authenticated = client.getA("/accounts/merchant/transactions") - .assertOkJson<BankAccountTransactionsResponse>() - val anonymousResponse = client.get("/accounts/merchant/transactions") - .assertOk() - assertEquals("no-store", anonymousResponse.headers[HttpHeaders.CacheControl]) - val anonymous = anonymousResponse.json<BankAccountTransactionsResponse>() - assertEquals(authenticated, anonymous) - assertEquals("public subject", anonymous.transactions.single().subject) - - client.get("/accounts/customer/transactions") - .assertNotFound(TalerErrorCode.BANK_UNKNOWN_ACCOUNT) - client.get("/accounts/missing/transactions") - .assertNotFound(TalerErrorCode.BANK_UNKNOWN_ACCOUNT) - - // Making an account private takes effect immediately. - client.patchAdmin("/accounts/merchant") { - json { "is_public" to false } - }.assertNoContent() - client.get("/accounts/merchant/transactions") - .assertNotFound(TalerErrorCode.BANK_UNKNOWN_ACCOUNT) - } - - // GET /transactions/T_ID - @Test - fun testById() = bankSetup { - authRoutine(HttpMethod.Get, "/accounts/merchant/transactions/42", allowAdmin = true) - - // Create transaction - tx("merchant", "KUDOS:0.3", "exchange", "tx") - // Check OK - client.getA("/accounts/merchant/transactions/1") - .assertOkJson<BankAccountTransactionInfo> { tx -> - assertEquals("tx", tx.subject) - assertEquals(TalerAmount("KUDOS:0.3"), tx.amount) - } - // Check unknown transaction - client.getA("/accounts/merchant/transactions/3") - .assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) - // Check another user's transaction - client.getA("/accounts/merchant/transactions/2") - .assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) - } - - // POST /transactions - @Test - fun create() = bankSetup { db -> - authRoutine(HttpMethod.Post, "/accounts/merchant/transactions") - - val valid_req = obj { - "payto_uri" to "$exchangePayto?message=payout" - "amount" to "KUDOS:0.3" - } - - // Check OK - client.postA("/accounts/merchant/transactions") { - json(valid_req) - }.assertOkJson<TransactionCreateResponse> { - client.getA("/accounts/merchant/transactions/${it.row_id}") - .assertOkJson<BankAccountTransactionInfo> { tx -> - assertEquals("payout", tx.subject) - assertEquals(TalerAmount("KUDOS:0.3"), tx.amount) - } - } - - // Check idempotency - ShortHashCode.rand().let { requestUid -> - val id = client.postA("/accounts/merchant/transactions") { - json(valid_req) { - "request_uid" to requestUid - } - }.assertOkJson<TransactionCreateResponse>().row_id - client.postA("/accounts/merchant/transactions") { - json(valid_req) { - "request_uid" to requestUid - } - }.assertOkJson<TransactionCreateResponse> { - assertEquals(id, it.row_id) - } - client.postA("/accounts/merchant/transactions") { - json(valid_req) { - "request_uid" to requestUid - "amount" to "KUDOS:42" - } - }.assertConflict(TalerErrorCode.BANK_TRANSFER_REQUEST_UID_REUSED) - } - - // Check amount in payto_uri - client.postA("/accounts/merchant/transactions") { - json { - "payto_uri" to "$exchangePayto?message=payout2&amount=KUDOS:1.05" - } - }.assertOkJson <TransactionCreateResponse> { - client.getA("/accounts/merchant/transactions/${it.row_id}") - .assertOkJson<BankAccountTransactionInfo> { tx -> - assertEquals("payout2", tx.subject) - assertEquals(TalerAmount("KUDOS:1.05"), tx.amount) - } - } - - // Check amount in payto_uri precedence - client.postA("/accounts/merchant/transactions") { - json { - "payto_uri" to "$exchangePayto?message=payout3&amount=KUDOS:1.05" - "amount" to "KUDOS:10.003" - } - }.assertOkJson<TransactionCreateResponse> { - client.getA("/accounts/merchant/transactions/${it.row_id}") - .assertOkJson<BankAccountTransactionInfo> { tx -> - assertEquals("payout3", tx.subject) - assertEquals(TalerAmount("KUDOS:1.05"), tx.amount) - } - } - // Testing the wrong currency - client.postA("/accounts/merchant/transactions") { - json(valid_req) { - "amount" to "EUR:3.3" - } - }.assertBadRequest(TalerErrorCode.GENERIC_CURRENCY_MISMATCH) - // Surpassing the debt limit - client.postA("/accounts/merchant/transactions") { - json(valid_req) { - "amount" to "KUDOS:555" - } - }.assertConflict(TalerErrorCode.BANK_UNALLOWED_DEBIT) - // Missing message - client.postA("/accounts/merchant/transactions") { - json(valid_req) { - "payto_uri" to "$exchangePayto" - } - }.assertBadRequest() - // Unknown creditor - client.postA("/accounts/merchant/transactions") { - json(valid_req) { - "payto_uri" to "$unknownPayto?message=payout" - } - }.assertConflict(TalerErrorCode.BANK_UNKNOWN_CREDITOR) - // Transaction to self - client.postA("/accounts/merchant/transactions") { - json(valid_req) { - "payto_uri" to "$merchantPayto?message=payout" - } - }.assertConflict(TalerErrorCode.BANK_SAME_ACCOUNT) - // Transaction to admin - val adminPayto = client.getA("/accounts/admin") - .assertOkJson<AccountData>().payto_uri - client.postA("/accounts/merchant/transactions") { - json(valid_req) { - "payto_uri" to "$adminPayto&message=payout" - } - }.assertConflict(TalerErrorCode.BANK_ADMIN_CREDITOR) - - // Init state - assertBalance("merchant", "+KUDOS:0") - assertBalance("customer", "+KUDOS:0") - // Send 2 times 3 - repeat(2) { - tx("merchant", "KUDOS:3", "customer") - } - client.postA("/accounts/merchant/transactions") { - json { - "payto_uri" to "$customerPayto?message=payout2&amount=KUDOS:5" - } - }.assertConflict(TalerErrorCode.BANK_UNALLOWED_DEBIT) - assertBalance("merchant", "-KUDOS:6") - assertBalance("customer", "+KUDOS:6") - // Send through debt - tx("customer", "KUDOS:10", "merchant") - assertBalance("merchant", "+KUDOS:4") - assertBalance("customer", "-KUDOS:4") - tx("merchant", "KUDOS:4", "customer") - - // Check bounce - assertBalance("merchant", "+KUDOS:0") - assertBalance("exchange", "+KUDOS:0") - tx("merchant", "KUDOS:1", "exchange", "") // Bounce common to transaction - tx("merchant", "KUDOS:1", "exchange", "Malformed") // Bounce malformed transaction - tx("merchant", "KUDOS:1", "exchange", "ADMIN BALANCE ADJUST") // Bounce admin balance adjust - val reservePub = EddsaPublicKey.randEdsaKey() - tx("merchant", "KUDOS:1", "exchange", fmtIncomingSubject(IncomingType.reserve, reservePub)) // Accept incoming - tx("merchant", "KUDOS:1", "exchange", fmtIncomingSubject(IncomingType.reserve, reservePub)) // Bounce reserve_pub reuse - assertBalance("merchant", "-KUDOS:1") - assertBalance("exchange", "+KUDOS:1") - - // Check warn - assertBalance("merchant", "-KUDOS:1") - assertBalance("exchange", "+KUDOS:1") - tx("exchange", "KUDOS:1", "merchant", "") // Warn common to transaction - tx("exchange", "KUDOS:1", "merchant", "Malformed") // Warn malformed transaction - val wtid = ShortHashCode.rand() - val exchange = BaseURL.parse("http://exchange.example.com/") - tx("exchange", "KUDOS:1", "merchant", fmtOutgoingSubject(wtid, exchange)) // Accept outgoing - tx("exchange", "KUDOS:1", "merchant", fmtOutgoingSubject(wtid, exchange)) // Warn wtid reuse - assertBalance("merchant", "+KUDOS:3") - assertBalance("exchange", "-KUDOS:3") - - // Check 2fa - fillTanInfo("merchant") - assertBalance("merchant", "+KUDOS:3") - assertBalance("customer", "+KUDOS:0") - client.postA("/accounts/merchant/transactions") { - json { - "payto_uri" to "$customerPayto?message=tan+check&amount=KUDOS:1" - } - }.assertChallenge { - assertBalance("merchant", "+KUDOS:3") - assertBalance("customer", "+KUDOS:0") - }.assertOkJson <TransactionCreateResponse> { - assertBalance("merchant", "+KUDOS:2") - assertBalance("customer", "+KUDOS:1") - } - - // Check 2fa idempotency - val req = obj { - "payto_uri" to "$customerPayto?message=tan+check&amount=KUDOS:1" - "request_uid" to ShortHashCode.rand() - } - val id = client.postA("/accounts/merchant/transactions") { - json(req) - }.assertChallenge { - assertBalance("merchant", "+KUDOS:2") - assertBalance("customer", "+KUDOS:1") - }.assertOkJson <TransactionCreateResponse> { - assertBalance("merchant", "+KUDOS:1") - assertBalance("customer", "+KUDOS:2") - }.row_id - client.postA("/accounts/merchant/transactions") { - json(req) - }.assertOkJson<TransactionCreateResponse> { - assertEquals(id, it.row_id) - } - client.postA("/accounts/merchant/transactions") { - json(req) { - "payto_uri" to "$customerPayto?message=tan+chec2k&amount=KUDOS:1" - } - }.assertConflict(TalerErrorCode.BANK_TRANSFER_REQUEST_UID_REUSED) - } - - @Test - fun createWithFee() = bankSetup(conf = "test_with_fees.conf") { - // Init state - assertBalance("merchant", "+KUDOS:0") - assertBalance("customer", "+KUDOS:0") - assertBalance("admin", "+KUDOS:0") - - // Check fee are sent to admin - tx("merchant", "KUDOS:3", "customer") - assertBalance("merchant", "-KUDOS:3.1") - assertBalance("customer", "+KUDOS:3") - assertBalance("admin", "+KUDOS:0.1") - - // Check amount with fee and min & max are checked - for (amount in listOf("KUDOS:7", "KUDOS:6.9", "KUDOS:0", "KUDOS:150")) { - client.postA("/accounts/merchant/transactions") { - json { - "payto_uri" to "$customerPayto?message=payout2&amount=$amount" - } - }.assertConflict(TalerErrorCode.BANK_UNALLOWED_DEBIT) - } - // Check empty account - tx("merchant", "KUDOS:6.8", "customer") - assertBalance("merchant", "-KUDOS:10") - assertBalance("customer", "+KUDOS:9.8") - assertBalance("admin", "+KUDOS:0.2") - - // Admin check no fee - tx("admin", "KUDOS:0.35", "merchant") - assertBalance("merchant", "-KUDOS:9.65") - assertBalance("admin", "-KUDOS:0.15") - - // Admin recover from debt - tx("customer", "KUDOS:1", "merchant") - assertBalance("admin", "-KUDOS:0.05") - tx("customer", "KUDOS:1", "merchant") - assertBalance("merchant", "-KUDOS:7.65") - assertBalance("customer", "+KUDOS:7.6") - assertBalance("admin", "+KUDOS:0.05") - } -} - -class CoreBankWithdrawalApiTest { - // POST /accounts/USERNAME/withdrawals - @Test - fun create() = bankSetup { - authRoutine(HttpMethod.Post, "/accounts/merchant/withdrawals") - - // Check OK - for (valid in listOf( - obj {}, - obj { "amount" to "KUDOS:1.0" }, - obj { "suggested_amount" to "KUDOS:2.0" }, - obj { - "amount" to "KUDOS:3.0" - "suggested_amount" to "KUDOS:4.0" - } - )) { - // Check OK - client.postA("/accounts/merchant/withdrawals") { - json(valid) - }.assertOkJson<BankAccountCreateWithdrawalResponse> { - assertEquals("taler+http://withdraw/localhost:8080/taler-integration/${it.withdrawal_id}", it.taler_withdraw_uri) - } - } - - // Check exchange account - client.postA("/accounts/exchange/withdrawals") { - json { "amount" to "KUDOS:9.0" } - }.assertConflict(TalerErrorCode.BANK_ACCOUNT_IS_EXCHANGE) - - // Check insufficient fund - client.postA("/accounts/merchant/withdrawals") { - json { "amount" to "KUDOS:90" } - }.assertConflict(TalerErrorCode.BANK_UNALLOWED_DEBIT) - client.postA("/accounts/merchant/withdrawals") { - json { "suggested_amount" to "KUDOS:90" } - }.assertConflict(TalerErrorCode.BANK_UNALLOWED_DEBIT) - - // Check wrong currency - client.postA("/accounts/merchant/withdrawals") { - json { "amount" to "EUR:90" } - }.assertBadRequest(TalerErrorCode.GENERIC_CURRENCY_MISMATCH) - client.postA("/accounts/merchant/withdrawals") { - json { "suggested_amount" to "EUR:90" } - }.assertBadRequest(TalerErrorCode.GENERIC_CURRENCY_MISMATCH) - } - - @Test - fun createWithFee() = bankSetup(conf = "test_with_fees.conf") { - // Check insufficient fund - for (amount in listOf("KUDOS:11", "KUDOS:10", "KUDOS:0", "KUDOS:150")) { - for (name in listOf("amount", "suggested_amount")) { - client.postA("/accounts/merchant/withdrawals") { - json { name to amount } - }.assertConflict(TalerErrorCode.BANK_UNALLOWED_DEBIT) - } - } - - // Check OK - for (name in listOf("amount", "suggested_amount")) { - client.postA("/accounts/merchant/withdrawals") { - json { name to "KUDOS:9.9" } - }.assertOk() - } - } - - // GET /withdrawals/withdrawal_id - @Test - fun get() = bankSetup { - // Check OK - for (valid in listOf( - Pair(null, null), - Pair("KUDOS:1.0", null), - Pair(null, "KUDOS:2.0") , - Pair("KUDOS:3.0", "KUDOS:4.0") - )) { - val amount = valid.first?.run(::TalerAmount) - val suggested = valid.second?.run(::TalerAmount) - client.postA("/accounts/merchant/withdrawals") { - json { - "amount" to amount - "suggested_amount" to suggested - } - }.assertOkJson<BankAccountCreateWithdrawalResponse> { - client.get("/withdrawals/${it.withdrawal_id}") - .assertOkJson<WithdrawalPublicInfo> { - assertEquals(amount, it.amount) - assertEquals(suggested, it.suggested_amount) - } - } - } - - // Check polling - statusRoutine<WithdrawalPublicInfo>("/withdrawals") { it.status } - - // Check bad UUID - client.get("/withdrawals/chocolate").assertBadRequest(TalerErrorCode.GENERIC_PARAMETER_MALFORMED) - - // Check unknown - client.get("/withdrawals/${UUID.randomUUID()}") - .assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) - } - - // POST /accounts/USERNAME/withdrawals/withdrawal_id/abort - @Test - fun abort() = bankSetup { - authRoutine(HttpMethod.Post, "/accounts/merchant/withdrawals/42/abort") - - // Check abort created - client.postA("/accounts/merchant/withdrawals") { - json { "amount" to "KUDOS:1" } - }.assertOkJson<BankAccountCreateWithdrawalResponse> { - val uuid = it.withdrawal_id - - // Check OK - client.postA("/accounts/merchant/withdrawals/$uuid/abort").assertNoContent() - // Check idempotence - client.postA("/accounts/merchant/withdrawals/$uuid/abort").assertNoContent() - } - - // Check abort selected - client.postA("/accounts/merchant/withdrawals") { - json { "amount" to "KUDOS:1" } - }.assertOkJson<BankAccountCreateWithdrawalResponse> { - val uuid = it.withdrawal_id - withdrawalSelect(uuid) - - // Check OK - client.postA("/accounts/merchant/withdrawals/$uuid/abort").assertNoContent() - // Check idempotence - client.postA("/accounts/merchant/withdrawals/$uuid/abort").assertNoContent() - } - - // Check abort confirmed - client.postA("/accounts/merchant/withdrawals") { - json { "amount" to "KUDOS:1" } - }.assertOkJson<BankAccountCreateWithdrawalResponse> { - val uuid = it.withdrawal_id - withdrawalSelect(uuid) - client.postA("/accounts/merchant/withdrawals/$uuid/confirm").assertNoContent() - - // Check error - client.postA("/accounts/merchant/withdrawals/$uuid/abort") - .assertConflict(TalerErrorCode.BANK_ABORT_CONFIRM_CONFLICT) - } - - // Check bad UUID - client.postA("/accounts/merchant/withdrawals/chocolate/abort") - .assertBadRequest(TalerErrorCode.GENERIC_PARAMETER_MALFORMED) - - // Check unknown - client.postA("/accounts/merchant/withdrawals/${UUID.randomUUID()}/abort") - .assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) - } - - // POST /accounts/USERNAME/withdrawals/withdrawal_id/confirm - @Test - fun confirm() = bankSetup { - authRoutine(HttpMethod.Post, "/accounts/merchant/withdrawals/42/confirm") - // Check confirm created - client.postA("/accounts/merchant/withdrawals") { - json { "amount" to "KUDOS:1" } - }.assertOkJson<BankAccountCreateWithdrawalResponse> { - val uuid = it.withdrawal_id - - // Check err - client.postA("/accounts/merchant/withdrawals/$uuid/confirm") - .assertConflict(TalerErrorCode.BANK_CONFIRM_INCOMPLETE) - } - - // Check confirm selected - client.postA("/accounts/merchant/withdrawals") { - json { "amount" to "KUDOS:1" } - }.assertOkJson<BankAccountCreateWithdrawalResponse> { - val uuid = it.withdrawal_id - withdrawalSelect(uuid) - - // Check amount differs - client.postA("/accounts/merchant/withdrawals/$uuid/confirm") { - json { "amount" to "KUDOS:2" } - }.assertConflict(TalerErrorCode.BANK_AMOUNT_DIFFERS) - - // Check OK - client.postA("/accounts/merchant/withdrawals/$uuid/confirm").assertNoContent() - // Check idempotence - client.postA("/accounts/merchant/withdrawals/$uuid/confirm").assertNoContent() - - // Check amount differs - client.postA("/accounts/merchant/withdrawals/$uuid/confirm") { - json { "amount" to "KUDOS:2" } - }.assertConflict(TalerErrorCode.BANK_AMOUNT_DIFFERS) - } - - // A selected operation is bound to the account that created it. - client.postA("/accounts/merchant/withdrawals") { - json { "amount" to "KUDOS:1" } - }.assertOkJson<BankAccountCreateWithdrawalResponse> { - val uuid = it.withdrawal_id - withdrawalSelect(uuid) - client.postA("/accounts/customer/withdrawals/$uuid/confirm") - .assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) - client.postA("/accounts/merchant/withdrawals/$uuid/abort") - .assertNoContent() - } - - // Check confirm with amount - client.postA("/accounts/merchant/withdrawals") { - json {} - }.assertOkJson<BankAccountCreateWithdrawalResponse> { - val uuid = it.withdrawal_id - withdrawalSelect(uuid) - - // Check missing amount - client.postA("/accounts/merchant/withdrawals/$uuid/confirm") - .assertConflict(TalerErrorCode.BANK_AMOUNT_REQUIRED) - - // Check OK - client.postA("/accounts/merchant/withdrawals/$uuid/confirm") { - json { "amount" to "KUDOS:1" } - }.assertNoContent() - // Check idempotence - client.postA("/accounts/merchant/withdrawals/$uuid/confirm") { - json { "amount" to "KUDOS:1" } - }.assertNoContent() - - // Check amount differs - client.postA("/accounts/merchant/withdrawals/$uuid/confirm") { - json { "amount" to "KUDOS:2" } - }.assertConflict(TalerErrorCode.BANK_AMOUNT_DIFFERS) - } - - // Check confirm aborted - client.postA("/accounts/merchant/withdrawals") { - json { "amount" to "KUDOS:1" } - }.assertOkJson<BankAccountCreateWithdrawalResponse> { - val uuid = it.withdrawal_id - withdrawalSelect(uuid) - client.postA("/accounts/merchant/withdrawals/$uuid/abort").assertNoContent() - - // Check error - client.postA("/accounts/merchant/withdrawals/$uuid/confirm") - .assertConflict(TalerErrorCode.BANK_CONFIRM_ABORT_CONFLICT) - } - - // Check reserve pub reuse - client.postA("/accounts/merchant/withdrawals") { - json { "amount" to "KUDOS:5" } - }.assertOkJson<BankAccountCreateWithdrawalResponse> { - val uuid = it.withdrawal_id - val reservePub = withdrawalSelect(uuid) - - tx("customer", "KUDOS:5", "exchange", "Taler $reservePub") - client.postA("/accounts/merchant/withdrawals/$uuid/confirm") - .assertConflict(TalerErrorCode.BANK_DUPLICATE_RESERVE_PUB_SUBJECT) - } - - // Check balance insufficient - client.postA("/accounts/merchant/withdrawals") { - json { "amount" to "KUDOS:5" } - }.assertOkJson<BankAccountCreateWithdrawalResponse> { - val uuid = it.withdrawal_id - withdrawalSelect(uuid) - - // Send too much money - tx("merchant", "KUDOS:5", "customer") - client.postA("/accounts/merchant/withdrawals/$uuid/confirm") - .assertConflict(TalerErrorCode.BANK_UNALLOWED_DEBIT) - - // Check can abort because not confirmed - client.postA("/accounts/merchant/withdrawals/$uuid/abort").assertNoContent() - } - - // Check bad UUID - client.postA("/accounts/merchant/withdrawals/chocolate/confirm") - .assertBadRequest(TalerErrorCode.GENERIC_PARAMETER_MALFORMED) - - // Check unknown - client.postA("/accounts/merchant/withdrawals/${UUID.randomUUID()}/confirm") - .assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) - - // Check 2fa without body - fillTanInfo("merchant") - assertBalance("merchant", "-KUDOS:7") - client.postA("/accounts/merchant/withdrawals") { - json { "amount" to "KUDOS:1" } - }.assertOkJson<BankAccountCreateWithdrawalResponse> { - val uuid = it.withdrawal_id - withdrawalSelect(uuid) - - client.postA("/accounts/merchant/withdrawals/$uuid/confirm") - .assertChallenge { - assertBalance("merchant", "-KUDOS:7") - }.assertNoContent() - } - - // Check 2fa with body - fillTanInfo("merchant") - assertBalance("merchant", "-KUDOS:8") - client.postA("/accounts/merchant/withdrawals") { - json {} - }.assertOkJson<BankAccountCreateWithdrawalResponse> { - val uuid = it.withdrawal_id - withdrawalSelect(uuid) - - client.postA("/accounts/merchant/withdrawals/$uuid/confirm") { - json { "amount" to "KUDOS:1" } - } - .assertChallenge { - assertBalance("merchant", "-KUDOS:8") - }.assertNoContent() - } - assertBalance("merchant", "-KUDOS:9") - } - - @Test - fun confirmWithFee() = bankSetup(conf = "test_with_fees.conf") { db -> - suspend fun run(amount: TalerAmount): HttpResponse { - val uuid = UUID.randomUUID() - // Create a selected withdrawal directly in the database to bypass checks - db.serializable(""" - INSERT INTO taler_withdrawal_operations(withdrawal_uuid,amount,exchange_bank_account,selection_done,wallet_bank_account,creation_date) - VALUES (?, (?, ?)::taler_amount, 2, true, 3, 0) - """) { - bind(uuid) - bind(amount) - executeUpdate() - } - - return client.postA("/accounts/customer/withdrawals/$uuid/confirm") - } - - // Check insufficient fund - for (amount in listOf("KUDOS:11", "KUDOS:10", "KUDOS:0", "KUDOS:150")) { - run(TalerAmount(amount)).assertConflict(TalerErrorCode.BANK_UNALLOWED_DEBIT) - } - - // Check OK - run(TalerAmount("KUDOS:9.9")) - } -} - -class CoreBankCashoutApiTest { - // POST /accounts/{USERNAME}/cashouts - @Test - fun create() = bankSetup { db -> - authRoutine(HttpMethod.Post, "/accounts/merchant/cashouts") - - val req = obj { - "request_uid" to ShortHashCode.rand() - "amount_debit" to "KUDOS:1" - "amount_credit" to convert("KUDOS:1") - } - - // Missing info - client.postA("/accounts/customer/cashouts") { - json(req) - }.assertConflict(TalerErrorCode.BANK_CONFIRM_INCOMPLETE) - - fillCashoutInfo("customer") - - // Check OK - val id = client.postA("/accounts/customer/cashouts") { - json(req) - }.assertOkJson<CashoutResponse>().cashout_id - - // Check idempotent - client.postA("/accounts/customer/cashouts") { - json(req) - }.assertOkJson<CashoutResponse> { - assertEquals(id, it.cashout_id) - } - - // Trigger conflict due to reused request_uid - client.postA("/accounts/customer/cashouts") { - json(req) { - "amount_debit" to "KUDOS:2" - "amount_credit" to convert("KUDOS:2") - } - }.assertConflict(TalerErrorCode.BANK_TRANSFER_REQUEST_UID_REUSED) - - // Every monetary field belongs to the idempotency identity. Simulate - // an inconsistent stored credit amount to isolate this comparison - // from conversion validation. - db.serializable( - "UPDATE cashout_operations SET amount_credit=(999,0)::taler_amount WHERE cashout_id=?" - ) { - bind(id) - executeUpdate() - } - client.postA("/accounts/customer/cashouts") { - json(req) - }.assertConflict(TalerErrorCode.BANK_TRANSFER_REQUEST_UID_REUSED) - - // Check exchange account - client.postA("/accounts/exchange/cashouts") { - json(req) - }.assertConflict(TalerErrorCode.BANK_ACCOUNT_IS_EXCHANGE) - - // Check insufficient fund - client.postA("/accounts/customer/cashouts") { - json(req) { - "request_uid" to ShortHashCode.rand() - "amount_debit" to "KUDOS:75" - "amount_credit" to convert("KUDOS:75") - } - }.assertConflict(TalerErrorCode.BANK_UNALLOWED_DEBIT) - - // Check wrong conversion - client.postA("/accounts/customer/cashouts") { - json(req) { - "amount_credit" to convert("KUDOS:2") - } - }.assertConflict(TalerErrorCode.BANK_BAD_CONVERSION) - - // Check min amount - client.postA("/accounts/customer/cashouts") { - json(req) { - "amount_debit" to "KUDOS:0.09" - } - }.assertConflict(TalerErrorCode.BANK_CONVERSION_AMOUNT_TO_SMALL) - - // Check custom min account - createConversionRateClass(cashout_min_amount = TalerAmount("KUDOS:10")) - client.patchAdmin("/accounts/customer") { - json { - "conversion_rate_class_id" to 1 - } - }.assertNoContent() - client.postA("/accounts/customer/cashouts") { - json(req) { - "amount_debit" to "KUDOS:5" - "amount_credit" to convert("KUDOS:5") - } - }.assertConflict(TalerErrorCode.BANK_CONVERSION_AMOUNT_TO_SMALL) - client.patchAdmin("/accounts/customer") { - json { - "conversion_rate_class_id" to (null as Long?) - } - }.assertNoContent() - - // Check wrong currency - client.postA("/accounts/customer/cashouts") { - json(req) { - "amount_debit" to "EUR:1" - } - }.assertBadRequest(TalerErrorCode.GENERIC_CURRENCY_MISMATCH) - client.postA("/accounts/customer/cashouts") { - json(req) { - "amount_credit" to "KUDOS:1" - } - }.assertBadRequest(TalerErrorCode.GENERIC_CURRENCY_MISMATCH) - - // Check 2fa - fillTanInfo("customer") - assertBalance("customer", "-KUDOS:1") - client.postA("/accounts/customer/cashouts") { - json(req) { - "request_uid" to ShortHashCode.rand() - } - }.assertChallenge { - assertBalance("customer", "-KUDOS:1") - }.assertOkJson<CashoutResponse> { - assertBalance("customer", "-KUDOS:2") - } - } - - // GET /accounts/{USERNAME}/cashouts/{CASHOUT_ID} - @Test - fun get() = bankSetup { - authRoutine(HttpMethod.Get, "/accounts/merchant/cashouts/42", allowAdmin = true) - fillCashoutInfo("customer") - - val amountDebit = TalerAmount("KUDOS:1.5") - val amountCredit = convert("KUDOS:1.5") - val req = obj { - "amount_debit" to amountDebit - "amount_credit" to amountCredit - } - - // Check confirm - client.postA("/accounts/customer/cashouts") { - json(req) { "request_uid" to ShortHashCode.rand() } - }.assertOkJson<CashoutResponse> { - val id = it.cashout_id - client.getA("/accounts/customer/cashouts/$id") - .assertOkJson<CashoutStatusResponse> { - assertEquals(amountDebit, it.amount_debit) - assertEquals(amountCredit, it.amount_credit) - } - } - - // Check bad UUID - client.getA("/accounts/customer/cashouts/chocolate") - .assertBadRequest(TalerErrorCode.GENERIC_PARAMETER_MALFORMED) - - // Check unknown - client.getA("/accounts/customer/cashouts/42") - .assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) - - // Check get another user's operation - client.postA("/accounts/customer/cashouts") { - json(req) { "request_uid" to ShortHashCode.rand() } - }.assertOkJson<CashoutResponse> { - val id = it.cashout_id - - // Check error - client.getA("/accounts/merchant/cashouts/$id") - .assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) - } - } - - // GET /accounts/{USERNAME}/cashouts - @Test - fun history() = bankSetup { - authRoutine(HttpMethod.Get, "/accounts/merchant/cashouts", allowAdmin = true) - historyRoutine<Cashouts>( - url = "/accounts/customer/cashouts", - ids = { it.cashouts.map { it.cashout_id } }, - registered = listOf { cashout("KUDOS:0.1") }, - polling = false - ) - } - - // GET /cashouts - @Test - fun globalHistory() = bankSetup { - authRoutine(HttpMethod.Get, "/cashouts", requireAdmin = true) - historyRoutine<GlobalCashouts>( - url = "/cashouts", - ids = { it.cashouts.map { it.cashout_id } }, - registered = listOf { cashout("KUDOS:0.1") }, - polling = false, - auth = "admin" - ) - } - - @Test - fun notImplemented() = bankSetup("test_no_conversion.conf") { - client.get("/accounts/customer/cashouts") - .assertNotImplemented() - } -} - -class CoreBankTanApiTest { - // POST /accounts/{USERNAME}/challenge/{challenge_id} - @Test - fun send() = bankSetup { - suspend fun HttpResponse.expectMfa(vararg tans: Pair<TanChannel, String>): HttpResponse { - return assertChallenge { res -> - assertEquals(setOf(*tans), res.challenges.map { it.tan_channel to it.tan_info }.toSet()) - assertFalse(res.combi_and) - } - } - suspend fun HttpResponse.expectValidation(vararg tans: Pair<TanChannel, String>): HttpResponse { - return assertChallenge { res -> - assertEquals(setOf(*tans), res.challenges.map { it.tan_channel to it.tan_info }.toSet()) - assertTrue(res.combi_and) - } - } - - // Set up 2fa - client.patchA("/accounts/merchant") { - json { - "contact_data" to obj { - "phone" to "+99" - "email" to "email@example.com" - } - "tan_channel" to "sms" - } - }.expectValidation(TanChannel.sms to "+99") - .assertNoContent() - - // Update 2fa settings - first 2FA challenge then new tan channel check - client.patchA("/accounts/merchant") { - json { // Info change - "contact_data" to obj { "phone" to "+98" } - } - }.expectValidation(TanChannel.sms to "+99", TanChannel.sms to "+98") - .assertNoContent() - client.patchA("/accounts/merchant") { - json { // Channel change - "tan_channel" to "email" - } - }.expectValidation(TanChannel.sms to "+98", TanChannel.email to "email@example.com") - .assertNoContent() - client.patchA("/accounts/merchant") { - json { // Both change - "contact_data" to obj { "phone" to "+97" } - "tan_channel" to "sms" - } - }.expectValidation(TanChannel.email to "email@example.com", TanChannel.sms to "+97") - .assertNoContent() - - // Disable 2fa - client.patchA("/accounts/merchant") { - json { "tan_channel" to null as String? } - }.expectValidation(TanChannel.sms to "+97") - .assertNoContent() - - // Update mfa settings - first mfa challenge then new tan channel check - client.patchA("/accounts/merchant") { - json { // All channels - "tan_channels" to setOf("sms", "email") - } - }.expectValidation(TanChannel.sms to "+97", TanChannel.email to "email@example.com") - .assertNoContent() - client.patchA("/accounts/merchant") { - json { // All info changes - "contact_data" to obj { - "phone" to "+99" - "email" to "email2@example.com" - } - } - }.expectMfa(TanChannel.sms to "+97", TanChannel.email to "email@example.com") - .expectValidation(TanChannel.sms to "+99", TanChannel.email to "email2@example.com") - .assertNoContent() - - // Disable mfa - client.patchA("/accounts/merchant") { - json { "tan_channels" to emptySet<String>() } - }.expectMfa(TanChannel.sms to "+99", TanChannel.email to "email2@example.com") - .assertNoContent() - - - // Admin has no 2FA - client.patchAdmin("/accounts/merchant") { - json { - "contact_data" to obj { "phone" to "+99" } - "tan_channel" to "sms" - } - }.assertNoContent() - client.patchAdmin("/accounts/merchant") { - json { "tan_channel" to "email" } - }.assertNoContent() - client.patchAdmin("/accounts/merchant") { - json { "tan_channel" to null as String? } - }.assertNoContent() - - // Check retry and invalidate - client.patchA("/accounts/merchant") { - json { - "contact_data" to obj { "phone" to "+88" } - "tan_channel" to "sms" - } - }.assertChallenge().assertNoContent() - client.patchA("/accounts/merchant") { - json { "is_public" to false } - }.assertAcceptedJson<ChallengeResponse> { - val challenge = it.challenges[0] - // Check ok - client.postA("/accounts/merchant/challenge/${challenge.challenge_id}") - .assertOk() - val code = tanCode("+88") - assertNotNull(code) - // Check retry - client.postA("/accounts/merchant/challenge/${challenge.challenge_id}") - .assertOk() - assertNull(tanCode("+88")) - // Idempotent patch does nothing - client.patchA("/accounts/merchant") { - json { - "contact_data" to obj { "phone" to "+88" } - "tan_channel" to "sms" - } - } - client.postA("/accounts/merchant/challenge/${challenge.challenge_id}") - .assertOk() - assertNull(tanCode("+88")) - - // Change 2fa settings - client.patchA("/accounts/merchant") { - json { - "tan_channel" to "email" - } - }.expectValidation(TanChannel.sms to "+88", TanChannel.email to "email2@example.com") - .assertNoContent() - - // Check invalidated - client.postA("/accounts/merchant/challenge/${challenge.challenge_id}/confirm") { - json { "tan" to code } - }.assertNotFound(TalerErrorCode.BANK_TAN_CHALLENGE_EXPIRED) - client.patchA("/accounts/merchant") { - headers[TALER_CHALLENGE_IDS] = "${challenge.challenge_id}" - json { "is_public" to false } - }.expectValidation(TanChannel.email to "email2@example.com") - .assertNoContent() - } - - // Unknown challenge - client.postA("/accounts/merchant/challenge/${UUID.randomUUID()}") - .assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) - } - - @Test - fun sendRateLimited() = bankSetup { - fillTanInfo("merchant") - - suspend fun ApplicationTestBuilder.txChallenge() - = client.postA("/accounts/merchant/transactions") { - json { - "payto_uri" to "$customerPayto?message=tx&amount=KUDOS:0.1" - } - }.assertAcceptedJson<ChallengeResponse>().challenges[0] - suspend fun ApplicationTestBuilder.submit(challenge: Challenge) - = client.postA("/accounts/merchant/challenge/${challenge.challenge_id}") - .assertOkJson<ChallengeRequestResponse>() - - - // Start a legitimate challenge and submit it - val oldChallenge = txChallenge() - submit(oldChallenge) - val tanCode = tanCode(oldChallenge.tan_info) - - // Challenge creation is not rate limited - repeat(MAX_ACTIVE_CHALLENGES*2) { - txChallenge() - } - - // Challenge submission is rate limited - repeat(MAX_ACTIVE_CHALLENGES-1) { - submit(txChallenge()) - } - val challenge = txChallenge() - client.postA("/accounts/merchant/challenge/${challenge.challenge_id}") - .assertTooManyRequests(TalerErrorCode.BANK_TAN_RATE_LIMITED) - - // Old already submitted challenge still works - val transmission = submit(oldChallenge) - client.postA("/accounts/merchant/challenge/${oldChallenge.challenge_id}/confirm") { - json { "tan" to tanCode } - }.assertNoContent() - - // Now an active challenge slot have been freed - submit(challenge) - - // We are rate limited again - val newChallenge = txChallenge() - client.postA("/accounts/merchant/challenge/${newChallenge.challenge_id}") - .assertTooManyRequests(TalerErrorCode.BANK_TAN_RATE_LIMITED) - } - - // POST /accounts/{USERNAME}/challenge/{challenge_id} - @Test - fun sendTanErr() = bankSetup("test_tan_err.conf") { - // Check fail - fillTanInfo("merchant") - client.patchA("/accounts/merchant") { - json { "is_public" to false } - }.assertAcceptedJson<ChallengeResponse> { - val challenge = it.challenges[0] - client.postA("/accounts/merchant/challenge/${challenge.challenge_id}") - .assertStatus(HttpStatusCode.BadGateway, TalerErrorCode.BANK_TAN_CHANNEL_SCRIPT_FAILED) - } - } - - // POST /accounts/{USERNAME}/challenge/{challenge_id}/confirm - @Test - fun confirm() = bankSetup { - fillTanInfo("merchant") - - // Check simple case - client.patchA("/accounts/merchant") { - json { "is_public" to false } - }.assertAcceptedJson<ChallengeResponse> { - val challenge = it.challenges[0] - val id = challenge.challenge_id - client.postA("/accounts/merchant/challenge/$id") - .assertOkJson<ChallengeRequestResponse>() - val code = tanCode(challenge.tan_info) - - // Check bad TAN code - client.postA("/accounts/merchant/challenge/$id/confirm") { - json { "tan" to "nice-try" } - }.assertConflict(TalerErrorCode.BANK_TAN_CHALLENGE_FAILED) - - // Check wrong account - client.postA("/accounts/customer/challenge/$id/confirm") { - json { "tan" to "nice-try" } - }.assertConflict(TalerErrorCode.BANK_TAN_CHALLENGE_FAILED) - - // Check OK - client.postA("/accounts/merchant/challenge/$id/confirm") { - json { "tan" to code } - }.assertNoContent() - // Check idempotence - client.postA("/accounts/merchant/challenge/$id/confirm") { - json { "tan" to code } - }.assertNoContent() - - // Unknown challenge - client.postA("/accounts/merchant/challenge/${UUID.randomUUID()}/confirm") { - json { "tan" to code } - }.assertNotFound(TalerErrorCode.BANK_CHALLENGE_NOT_FOUND) - } - - // Check invalidation - client.patchA("/accounts/merchant") { - json { "is_public" to true } - }.assertAcceptedJson<ChallengeResponse> { - val challenge = it.challenges[0] - val id = challenge.challenge_id - client.postA("/accounts/merchant/challenge/$id") - .assertOkJson<ChallengeRequestResponse>() - - // Check invalidated - fillTanInfo("merchant") - client.postA("/accounts/merchant/challenge/$id/confirm") { - json { "tan" to tanCode(challenge.tan_info) } - }.assertNotFound(TalerErrorCode.BANK_TAN_CHALLENGE_EXPIRED) - - client.postA("/accounts/merchant/challenge/$id") - .assertNotFound(TalerErrorCode.BANK_TAN_CHALLENGE_EXPIRED) - } - } -} - -class CoreBankConversionApiTest { - // POST /conversion-rate-classes - // GET /conversion-rate-classes - // GET /conversion-rate-classes/{CLASS_ID} - @Test - fun classes() = bankSetup() { - authRoutine(HttpMethod.Post, "/conversion-rate-classes", requireAdmin = true) - authRoutine(HttpMethod.Get, "/conversion-rate-classes", requireAdmin = true) - authRoutine(HttpMethod.Get, "/conversion-rate-classes/1", requireAdmin = true) - - val fullInput = obj { - "description" to "A nice little class" - "cashin_ratio" to "0.1" - "cashin_fee" to "KUDOS:0.2" - "cashin_tiny_amount" to "KUDOS:0.3" - "cashin_rounding_mode" to "nearest" - "cashin_min_amount" to "EUR:0" - "cashout_ratio" to "0.4" - "cashout_fee" to "EUR:0.5" - "cashout_tiny_amount" to "EUR:0.6" - "cashout_rounding_mode" to "zero" - "cashout_min_amount" to "KUDOS:0.7" - } - - // Check no classes - client.getAdmin("/conversion-rate-classes").assertNoContent() - client.getAdmin("/conversion-rate-classes/1").assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) - client.patchAdmin("/conversion-rate-classes/1") { - json(fullInput) { - "name" to "Class" - } - }.assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) - client.deleteAdmin("/conversion-rate-classes/1").assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) - - // Create full - val full = client.postAdmin("/conversion-rate-classes") { - json(fullInput) { - "name" to "Class n°1" - } - }.assertOkJson<ConversionRateClassResponse> { - assertEquals(it.conversion_rate_class_id, 1) - val rate = client.getAdmin("/conversion-rate-classes/${it.conversion_rate_class_id}").assertOkJson<ConversionRateClass>() - client.patchAdmin("/conversion-rate-classes/${it.conversion_rate_class_id}") { - json { - "name" to "Class n°1" - } - }.assertNoContent() - it.conversion_rate_class_id - } - // Create empty - val empty = client.postAdmin("/conversion-rate-classes") { - json { - "name" to "Class n°2" - } - }.assertOkJson<ConversionRateClassResponse> { - assertEquals(it.conversion_rate_class_id, 2) - val rate = client.getAdmin("/conversion-rate-classes/${it.conversion_rate_class_id}").assertOkJson<ConversionRateClass>() - client.patchAdmin("/conversion-rate-classes/${it.conversion_rate_class_id}") { - json(fullInput) { - "name" to "Class n°2" - } - }.assertNoContent() - it.conversion_rate_class_id - } - - // Bad currency - client.postAdmin("/conversion-rate-classes") { - json(fullInput) { - "name" to "Bad currency" - "cashout_fee" to "CHF:0.003" - } - }.assertBadRequest(TalerErrorCode.GENERIC_CURRENCY_MISMATCH) - - // Name reuse currency - client.postAdmin("/conversion-rate-classes") { - json(fullInput) { - "name" to "Class n°1" - } - }.assertConflict(TalerErrorCode.BANK_NAME_REUSE) - client.patchAdmin("/conversion-rate-classes/2") { - json(fullInput) { - "name" to "Class n°1" - } - }.assertConflict(TalerErrorCode.BANK_NAME_REUSE) - client.patchAdmin("/conversion-rate-classes/1") { - json(fullInput) { - "name" to "Class n°1" - } - }.assertNoContent() - - // Page - client.getAdmin("/conversion-rate-classes").assertOkJson<ConversionRateClasses> { - assertEquals(it.classes.size, 2) - } - val generated = (0 until 5).map { createConversionRateClass() } - client.getAdmin("/conversion-rate-classes").assertOkJson<ConversionRateClasses> { - assertEquals(it.classes.size, 7) - } - client.getAdmin("/conversion-rate-classes?filter_name=Gen").assertOkJson<ConversionRateClasses> { - assertEquals(it.classes.size, 5) - } - - // Delete all - for (id in listOf(full.conversion_rate_class_id, empty.conversion_rate_class_id) + generated) { - client.deleteAdmin("/conversion-rate-classes/$id").assertNoContent() - client.deleteAdmin("/conversion-rate-classes/$id").assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) - } - client.getAdmin("/conversion-rate-classes").assertNoContent() - } - - @Test - fun notImplemented() = bankSetup("test_no_conversion.conf") { - client.getAdmin("conversion-rate-classes/1").assertNotImplemented() - client.getAdmin("conversion-rate-classes").assertNotImplemented() - } -} diff --git a/libeufin-bank/src/test/kotlin/DatabaseTest.kt b/libeufin-bank/src/test/kotlin/DatabaseTest.kt @@ -1,187 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2023-2025 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/> - */ - -import io.ktor.http.* -import kotlinx.coroutines.* -import org.junit.Test -import org.slf4j.LoggerFactory -import tech.libeufin.bank.* -import tech.libeufin.bank.db.AccountDAO.AccountCreationResult -import tech.libeufin.bank.db.TanDAO.* -import tech.libeufin.common.* -import tech.libeufin.common.assertOk -import tech.libeufin.common.db.* -import tech.libeufin.common.json -import tech.libeufin.common.test.* -import java.time.Duration -import java.time.Instant -import java.time.temporal.ChronoUnit -import java.util.UUID -import java.util.concurrent.TimeUnit -import kotlin.test.assertEquals -import kotlin.test.assertIs -import kotlin.test.assertNull -import kotlin.test.assertNotEquals - -class DatabaseTest { - - @Test - fun notificationConnectionsClose() = setup { db, cfg -> - val source = pgDataSource(cfg.dbCfg.dbConnStr) - val applicationName = "notification-test-${UUID.randomUUID()}" - source.applicationName = applicationName - val firstNotification = CompletableDeferred<Unit>() - val secondNotification = CompletableDeferred<Unit>() - var reconnect = true - - db.conn { observer -> - fun watcherPids(): List<Int> = observer.prepareStatement( - "SELECT pid FROM pg_stat_activity WHERE application_name = ? AND query = 'LISTEN watcher_test' AND state = 'idle'" - ).use { stmt -> - stmt.setString(1, applicationName) - stmt.executeQuery().use { rows -> - buildList { while (rows.next()) add(rows.getInt(1)) } - } - } - - val watcher = watchNotifications( - source, "libeufin_bank", LoggerFactory.getLogger("notification-test"), - mapOf("watcher_test" to { - if (reconnect) { - reconnect = false - firstNotification.complete(Unit) - // Exercise cleanup on the retry path as well as on close(). - throw IllegalStateException("test notification reconnect") - } - secondNotification.complete(Unit) - }) - ) - watcher.use { - val firstPid = withTimeout(5000) { - while (watcherPids().isEmpty()) delay(10) - watcherPids().single() - } - observer.execSQLUpdate("NOTIFY watcher_test, 'first'") - withTimeout(5000) { firstNotification.await() } - val secondPid = withTimeout(5000) { - while (true) { - val pids = watcherPids() - if (pids.size == 1 && pids.single() != firstPid) break - delay(10) - } - watcherPids().single() - } - assertNotEquals(firstPid, secondPid) - observer.execSQLUpdate("NOTIFY watcher_test, 'second'") - withTimeout(5000) { secondNotification.await() } - } - withTimeout(5000) { - while (watcherPids().isNotEmpty()) delay(10) - } - } - } - - // Testing the helper that creates the admin account. - @Test - fun createAdmin() = setup { db, ctx -> - // Create admin account - assertIs<AccountCreationResult.Success>(createAdminAccount(db, ctx)) - // Checking idempotency - assertEquals(AccountCreationResult.UsernameReuse, createAdminAccount(db, ctx)) - } - - @Test - fun tanChallenge() = bankSetup { db -> db.conn { conn -> - val validityPeriod = Duration.ofHours(1) - val retransmissionPeriod: Duration = Duration.ofMinutes(1) - val retryCounter = 3 - - suspend fun create(code: String, timestamp: Instant): UUID { - return db.tan.new( - hbody = Base32Crockford64B.rand(), - salt = Base32Crockford16B.rand(), - username = "customer", - op = Operation.withdrawal, - code = code, - timestamp = timestamp, - retryCounter = retryCounter, - validityPeriod = validityPeriod, - tanChannel = TanChannel.sms, - tanInfo = "+88" - ) - } - - suspend fun markSent(id: UUID, timestamp: Instant) { - db.tan.markSent(id, timestamp + retransmissionPeriod) - } - - suspend fun send(id: UUID, code: String, timestamp: Instant): String? { - return (db.tan.send( - id, - timestamp, - 10 - ) as? TanSendResult.Send)?.tanCode - } - - val now = Instant.now() - val expired = now + validityPeriod - val retransmit = now + retransmissionPeriod - - // Check basic - create("good-code", now).run { - // Bad code - assertEquals(TanSolveResult.BadCode, db.tan.solve(this, "bad-code", now)) - // Good code - assertIs<TanSolveResult.Success>(db.tan.solve(this, "good-code", now)) - // Never resend a confirmed challenge - assertEquals(TanSendResult.Solved, db.tan.send(this, now, 10)) - // Confirmed challenge always ok - assertIs<TanSolveResult.Success>(db.tan.solve(this, "good-code", now)) - } - - // Check retry - create("good-code", now).run { - markSent(this, now) - // Bad code - repeat(retryCounter-1) { - assertEquals(TanSolveResult.BadCode, db.tan.solve(this, "bad-code", now)) - } - assertEquals(TanSolveResult.NoRetry, db.tan.solve(this, "bad-code", now)) - // Good code fail - assertEquals(TanSolveResult.NoRetry, db.tan.solve(this, "good-code", now)) - // Exhausted challenge IDs are terminal and cannot be resent. - assertEquals(TanSendResult.Expired, db.tan.send(this, now, 10)) - } - - // Check retransmission - create("good-code", now).run { - // Failed to send retransmit - assertIs<TanSendResult.Send>(db.tan.send(this, now, 10)) - // Code successfully sent and still valid - markSent(this, now) - assertIs<TanSendResult.Success>(db.tan.send(this, now, 10)) - // Code is still valid but should be resent - assertIs<TanSendResult.Send>(db.tan.send(this, retransmit, 10)) - // Good code fail because expired - assertEquals(TanSolveResult.Expired, db.tan.solve(this, "good-code", expired)) - // Expired challenge IDs are terminal and cannot be resent. - assertEquals(TanSendResult.Expired, db.tan.send(this, expired, 10)) - } - }} -} diff --git a/libeufin-bank/src/test/kotlin/GcTest.kt b/libeufin-bank/src/test/kotlin/GcTest.kt @@ -1,169 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2024-2025 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/> - */ - -import io.ktor.client.request.* -import org.junit.Test -import tech.libeufin.bank.* -import tech.libeufin.bank.db.CashoutDAO.CashoutCreationResult -import tech.libeufin.bank.db.ExchangeDAO.TransferResult -import tech.libeufin.bank.db.TransactionDAO.BankTransactionResult -import tech.libeufin.bank.db.WithdrawalDAO.* -import tech.libeufin.common.* -import tech.libeufin.common.test.* -import tech.libeufin.common.db.* -import java.time.Duration -import java.time.Instant -import java.util.* -import kotlin.test.assertEquals -import kotlin.test.assertIs - -class GcTest { - @Test - fun gc() = bankSetup { db -> db.conn { conn -> - fun assertNb(nb: Int, stmt: String) { - assertEquals(nb, conn.talerStatement(stmt).one { it.getInt(1) }) - } - fun assertNbAccount(nb: Int) = assertNb(nb, "SELECT count(*) from bank_accounts") - fun assertNbTokens(nb: Int) = assertNb(nb, "SELECT count(*) from bearer_tokens") - fun assertNbTan(nb: Int) = assertNb(nb, "SELECT count(*) from tan_challenges") - fun assertNbCashout(nb: Int) = assertNb(nb, "SELECT count(*) from cashout_operations") - fun assertNbWithdrawal(nb: Int) = assertNb(nb, "SELECT count(*) from taler_withdrawal_operations") - fun assertNbBankTx(nb: Int) = assertNb(nb, "SELECT count(*) from bank_transaction_operations") - fun assertNbTx(nb: Int) = assertNb(nb, "SELECT count(*) from bank_account_transactions") - fun assertNbIncoming(nb: Int) = assertNb(nb, "SELECT count(*) from taler_exchange_incoming") - fun assertNbOutgoing(nb: Int) = assertNb(nb, "SELECT count(*) from taler_exchange_outgoing") - - val ZERO = TalerAmount.zero("KUDOS") - val MAX = TalerAmount.max("KUDOS") - - // Time calculation - val abortAfter = Duration.ofMinutes(15) - val cleanAfter = Duration.ofDays(14) - val deleteAfter = Duration.ofDays(350) - val now = Instant.now() - val abort = now.minus(abortAfter) - val clean = now.minus(cleanAfter) - val delete = now.minus(deleteAfter) - - // Create test accounts - val payto = IbanPayto.rand() - client.post("/accounts") { - json { - "username" to "old_account" - "password" to "old_account-password" - "name" to "Old Account" - "cashout_payto_uri" to payto - } - }.assertOkJson<RegisterAccountResponse>().internal_payto_uri - client.post("/accounts") { - json { - "username" to "recent_account" - "password" to "recent_account-password" - "name" to "Recent Account" - "cashout_payto_uri" to payto - } - - }.assertOkJson<RegisterAccountResponse>().internal_payto_uri - assertNbAccount(6) - - // Create test tokens - for (time in listOf(now, clean)) { - for (account in listOf("old_account", "recent_account")) { - db.token.create(account, ByteArray(32).rand(), time, time, TokenScope.readonly, false, null, true) - db.tan.new(account, Operation.cashout, Base32Crockford64B.rand(), Base32Crockford16B.rand(), "", time, 0, Duration.ZERO, TanChannel.sms, "") - } - } - assertNbTokens(5) - assertNbTan(4) - - // Create test operations - val from = TalerAmount("KUDOS:1") - val to = convert("KUDOS:1") - for ((account, times) in listOf( - Pair("old_account", listOf(delete)), - Pair("recent_account", listOf(now, abort, clean, delete)) - )) { - for (time in times) { - val uuid = UUID.randomUUID() - assertEquals( - db.withdrawal.create(account, uuid, from, null, false, time, ZERO, ZERO, MAX), - WithdrawalCreationResult.Success - ) - assertIs<WithdrawalSelectionResult.Success>( - db.withdrawal.setDetails(uuid, exchangePayto, EddsaPublicKey.randEdsaKey(), null, ZERO, ZERO, MAX) - ) - assertEquals( - db.withdrawal.confirm(account, uuid, time, null, false, ZERO, ZERO, MAX), - WithdrawalConfirmationResult.Success - ) - assertIs<CashoutCreationResult.Success>( - db.cashout.create(account, ShortHashCode.rand(), from, to, "", time, false), - ) - assertIs<BankTransactionResult.Success>( - db.transaction.create(customerPayto, account, "", from, time, false, ShortHashCode.rand(), ZERO, ZERO, MAX), - ) - } - for (time in listOf(now, abort, clean, delete)) { - assertEquals( - db.withdrawal.create(account, UUID.randomUUID(), from, null, false, time, ZERO, ZERO, MAX), - WithdrawalCreationResult.Success - ) - } - } - for (time in listOf(now, abort, clean, delete)) { - assertIs<TransferResult.Success>( - db.exchange.transfer( - TransferRequest(HashCode.rand(), from, BaseURL.parse("http://localhost/"), ShortHashCode.rand(), customerPayto), - "exchange", time, false - ) - ) - } - - assertNbTx(38) - assertNbCashout(5) - assertNbBankTx(5) - assertNbWithdrawal(13) - assertNbIncoming(5) - assertNbOutgoing(4) - - // Check soft delete - conn.execSQLUpdate("UPDATE bank_accounts SET balance = (0, 0)::taler_amount") - for (account in listOf("old_account", "recent_account")) { - client.deleteA("/accounts/$account").assertNoContent() - } - assertNbAccount(6) - - db.gc.collect( - Instant.now(), - abortAfter, - cleanAfter, - deleteAfter - ) - // Check hard delete - assertNbAccount(5) - assertNbTokens(3) - assertNbTan(1) - assertNbTx(24) - assertNbCashout(3) - assertNbBankTx(3) - assertNbWithdrawal(4) - assertNbIncoming(3) - assertNbOutgoing(3) - }} -} -\ No newline at end of file diff --git a/libeufin-bank/src/test/kotlin/JsonTest.kt b/libeufin-bank/src/test/kotlin/JsonTest.kt @@ -1,84 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2023 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/> - */ - -import kotlinx.serialization.Serializable -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json -import org.junit.Test -import tech.libeufin.bank.CreditDebitInfo -import tech.libeufin.common.RelativeTime -import tech.libeufin.common.TalerAmount -import tech.libeufin.common.TalerTimestamp -import java.time.Duration -import java.time.Instant -import java.time.temporal.ChronoUnit - -@Serializable -data class MyJsonType( - val content: String, - val n: Int -) - -// Running (de)serialization, only checking that no exceptions are raised. -class JsonTest { - @Test - fun serializationTest() { - Json.encodeToString(MyJsonType("Lorem Ipsum", 3)) - } - @Test - fun deserializationTest() { - val serialized = """ - {"content": "Lorem Ipsum", "n": 3} - """.trimIndent() - Json.decodeFromString<MyJsonType>(serialized) - } - - /** - * Testing the custom absolute and relative time serializers. - */ - @Test - fun timeSerializers() { - // from JSON to time types - assert(Json.decodeFromString<RelativeTime>("{\"d_us\": 3}").duration.toNanos() == 3000L) - assert(Json.decodeFromString<RelativeTime>("{\"d_us\": \"forever\"}").duration == ChronoUnit.FOREVER.duration) - assert(Json.decodeFromString<TalerTimestamp>("{\"t_s\": 3}").instant == Instant.ofEpochSecond(3)) - assert(Json.decodeFromString<TalerTimestamp>("{\"t_s\": \"never\"}").instant == Instant.MAX) - - // from time types to JSON - val oneDay = RelativeTime(Duration.of(1, ChronoUnit.DAYS)) - val oneDaySerial = Json.encodeToString(oneDay) - assert(Json.decodeFromString<RelativeTime>(oneDaySerial).duration == oneDay.duration) - val forever = RelativeTime(ChronoUnit.FOREVER.duration) - val foreverSerial = Json.encodeToString(forever) - assert(Json.decodeFromString<RelativeTime>(foreverSerial).duration == forever.duration) - } - - @Test - fun enumSerializer() { - assert("\"credit\"" == Json.encodeToString(CreditDebitInfo.credit)) - assert("\"debit\"" == Json.encodeToString(CreditDebitInfo.debit)) - } - - // Testing JSON <--> TalerAmount - @Test - fun amountSerializer() { - val amt = Json.decodeFromString<TalerAmount>("\"KUDOS:4.4\"") - assert(Json.encodeToString(amt) == "\"KUDOS:4.4\"") - } -} -\ No newline at end of file diff --git a/libeufin-bank/src/test/kotlin/ObservabilityTest.kt b/libeufin-bank/src/test/kotlin/ObservabilityTest.kt @@ -1,54 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2025 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/> - */ - -import io.ktor.http.* -import io.ktor.client.request.* -import org.junit.Test -import tech.libeufin.bank.* -import tech.libeufin.common.* -import tech.libeufin.common.test.* - -class ObservabilityApiTest { - // GET /taler-observability/config - @Test - fun config() = bankSetup { - client.get("/taler-observability/config").assertOkJson<TalerObservabilityConfig>() - } - - // GET /taler-observability/metrics - @Test - fun metrics() = bankSetup { db -> - authRoutine(HttpMethod.Get, "/taler-observability/metrics", requireAdmin = true) - client.getAdmin("/taler-observability/metrics").assertOk() - - // Check observability token - val response = client.post("/accounts/admin/token") { - pwAuth() - json { - "scope" to "observability" - "duration" to obj { - "d_us" to "forever" - } - } - }.assertOkJson<TokenSuccessResponse>() - client.get("/taler-observability/metrics") { - headers[HttpHeaders.Authorization] = "Bearer ${response.access_token}" - }.assertOk() - } -} -\ No newline at end of file diff --git a/libeufin-bank/src/test/kotlin/OpenApiTest.kt b/libeufin-bank/src/test/kotlin/OpenApiTest.kt @@ -1,59 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2026 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/> - */ - -import io.ktor.client.request.* -import io.ktor.client.statement.* -import io.ktor.http.* -import io.ktor.server.testing.* -import org.junit.Test -import sun.misc.Unsafe -import tech.libeufin.bank.bankConfig -import tech.libeufin.bank.corebankWebApp -import tech.libeufin.bank.db.Database -import tech.libeufin.common.* -import java.io.File -import kotlin.io.path.Path -import kotlin.test.* - -class OpenApiTest { - private fun fakeDatabase(): Database { - val field = Unsafe::class.java.getDeclaredField("theUnsafe") - field.isAccessible = true - val unsafe = field.get(null) as Unsafe - return unsafe.allocateInstance(Database::class.java) as Database - } - - @Test - fun generateSpec() { - val cfg = bankConfig(Path("conf/test.conf")) - testApplication { - application { - corebankWebApp(fakeDatabase(), cfg, serveSpec = true) - } - val resp = client.get("/openapi.yaml") - resp.assertOk() - val spec = resp.bodyAsText() - assertTrue(spec.contains("openapi: 3.1.0"), "Response should be a valid OpenAPI spec") - assertTrue(spec.contains("LibEuFin Bank API"), "Spec should contain the API title") - val outFile = File("build/openapi.yaml") - outFile.writeText(spec) - println("OpenAPI spec written to ${outFile.absolutePath}") - } - } -} diff --git a/libeufin-bank/src/test/kotlin/PaytoTest.kt b/libeufin-bank/src/test/kotlin/PaytoTest.kt @@ -1,89 +0,0 @@ -/* - * 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/> - */ - -import io.ktor.client.request.* -import org.junit.Test -import tech.libeufin.bank.BankAccountTransactionInfo -import tech.libeufin.bank.RegisterAccountResponse -import tech.libeufin.bank.TransactionCreateResponse -import tech.libeufin.common.* -import tech.libeufin.common.test.* -import kotlin.test.assertEquals - -class PaytoTest { - // x-taler-bank - @Test - fun xTalerBank() = bankSetup("test_x_taler_bank.conf") { - // Check Ok - client.post("/accounts") { - json { - "username" to "john" - "password" to "john-password" - "name" to "John" - } - }.assertOkJson<RegisterAccountResponse> { - assertEquals("payto://x-taler-bank/localhost/john?receiver-name=John", it.internal_payto_uri) - } - - // Bad IBAN payto - client.post("/accounts") { - json { - "username" to "foo" - "password" to "foo-password" - "name" to "Jane" - "payto_uri" to IbanPayto.rand() - } - }.assertBadRequest() - // Bad payto username - client.post("/accounts") { - json { - "username" to "foo" - "password" to "foo-password" - "name" to "Jane" - "payto_uri" to "payto://x-taler-bank/localhost/not-foo" - } - }.assertBadRequest() - // Check Ok - client.post("/accounts") { - json { - "username" to "foo" - "password" to "foo-password" - "name" to "Jane" - "payto_uri" to "payto://x-taler-bank/localhost/foo" - } - }.assertOkJson<RegisterAccountResponse> { - assertEquals("payto://x-taler-bank/localhost/foo?receiver-name=Jane", it.internal_payto_uri) - } - - // Check payto canonicalisation - client.postA("/accounts/john/transactions") { - json { - "payto_uri" to "payto://x-taler-bank/ignored/foo?message=payout&amount=KUDOS:0.3" - } - }.assertOkJson<TransactionCreateResponse> { - client.getA("/accounts/john/transactions/${it.row_id}") - .assertOkJson<BankAccountTransactionInfo> { tx -> - assertEquals("payout", tx.subject) - assertEquals("payto://x-taler-bank/localhost/foo?receiver-name=Jane", tx.creditor_payto_uri) - assertEquals("payto://x-taler-bank/localhost/john?receiver-name=John", tx.debtor_payto_uri) - assertEquals(TalerAmount("KUDOS:0.3"), tx.amount) - } - } - } -} -\ No newline at end of file diff --git a/libeufin-bank/src/test/kotlin/PreparedTransferApiTest.kt b/libeufin-bank/src/test/kotlin/PreparedTransferApiTest.kt @@ -1,288 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2026 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/> - */ - -import io.ktor.client.request.* -import org.junit.Test -import tech.libeufin.common.* -import tech.libeufin.common.crypto.CryptoUtil -import tech.libeufin.common.test.* -import java.time.Instant -import kotlin.test.* - -class PreparedTransferApiTest { - // GET /taler-prepared-transfer/config - @Test - fun config() = bankSetup { - client.get("/taler-prepared-transfer/config").assertOkJson<PreparedTransferConfig>() - } - - // POST /taler-prepared-transfer/registration - @Test - fun registration() = bankSetup { - val (priv, pub) = EddsaPublicKey.randEdsaKeyPair() - val amount = TalerAmount("KUDOS:1") - val valid_req = SubjectRequest( - exchangePayto, - TransferType.reserve, - false, - amount, - PublicKeyAlg.EdDSA, - pub, - pub, - EddsaSignature.rand() - ) - - val simpleSubject = TransferSubject.Simple("Taler MAP:$pub", amount) - - // Valid - val subjects = client.post("/taler-prepared-transfer/registration") { - json(valid_req.sign(priv)) - }.assertOkJson<SubjectResult> { - assertEquals(it.subjects[1], simpleSubject) - assertIs<TransferSubject.Uri>(it.subjects[0]) - }.subjects - - // Idempotent - client.post("/taler-prepared-transfer/registration") { - json(valid_req.sign(priv)) - }.assertOkJson<SubjectResult> { - assertEquals(it.subjects, subjects) - } - - // KYC has a different withdrawal uri - client.post("/taler-prepared-transfer/registration") { - json(valid_req.copy(type = TransferType.kyc).sign(priv)) - }.assertOkJson<SubjectResult> { - assertEquals(it.subjects[1], simpleSubject) - val uriSubject = assertIs<TransferSubject.Uri>(it.subjects[0]) - assertNotEquals(subjects[0], uriSubject) - } - - // Recurrent only has simple subject - client.post("/taler-prepared-transfer/registration") { - json(valid_req.copy(recurrent = true).sign(priv)) - }.assertOkJson<SubjectResult> { - assertEquals(it.subjects, listOf(simpleSubject)) - } - - // Bad signature - client.post("/taler-prepared-transfer/registration") { - json(valid_req) - }.assertForbidden(TalerErrorCode.BANK_BAD_SIGNATURE) - - // Not exchange - client.post("/taler-prepared-transfer/registration") { - json(valid_req.copy(credit_account = merchantPayto).sign(priv)) - }.assertConflict(TalerErrorCode.BANK_ACCOUNT_IS_NOT_EXCHANGE) - - // Unknown account - client.post("/taler-prepared-transfer/registration") { - json(valid_req.copy(credit_account = unknownPayto).sign(priv)) - }.assertConflict(TalerErrorCode.BANK_UNKNOWN_CREDITOR) - - assertBalance("customer", "+KUDOS:0") - assertBalance("exchange", "+KUDOS:0") - - // Non recurrent accept on then bounce - client.post("/taler-prepared-transfer/registration") { - json(valid_req.sign(priv)) - }.assertOkJson<SubjectResult> { - val uuid = (it.subjects[0] as? TransferSubject.Uri)!!.uri.substringAfterLast('/') - client.postA("/accounts/customer/withdrawals/$uuid/confirm").assertNoContent() // reserve - client.postA("/accounts/merchant/withdrawals/$uuid/confirm") - .assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) - tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // bounce - tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // bounce - assertBalance("customer", "-KUDOS:1") - assertBalance("exchange", "+KUDOS:1") - } - - // Withdrawal is aborted on completion - client.post("/taler-prepared-transfer/registration") { - json(valid_req.copy(type = TransferType.kyc).sign(priv)) - }.assertOkJson<SubjectResult> { - val uuid = (it.subjects[0] as? TransferSubject.Uri)!!.uri.substringAfterLast('/') - println("UUID $uuid") - tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // kyc - tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // bounce - client.postA("/accounts/customer/withdrawals/$uuid/confirm") - .assertConflict(TalerErrorCode.BANK_CONFIRM_ABORT_CONFLICT) // aborted - assertBalance("customer", "-KUDOS:2") - assertBalance("exchange", "+KUDOS:2") - } - - // Recurrent accept one and delay others - val newKey = EddsaPublicKey.randEdsaKey() - client.post("/taler-prepared-transfer/registration") { - json(valid_req.copy(account_pub = newKey, recurrent = true).sign(priv)) - } - tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // reserve - tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // pending - tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // pending - tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // pending - tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // pending - assertBalance("customer", "-KUDOS:7") - assertBalance("exchange", "+KUDOS:7") - - // Complete pending on recurrent update - val kycKey = EddsaPublicKey.randEdsaKey() - client.post("/taler-prepared-transfer/registration") { - json(valid_req.copy(type= TransferType.kyc, account_pub = kycKey, recurrent = true).sign(priv)) - }.assertOkJson<SubjectResult>() - client.post("/taler-prepared-transfer/registration") { - json(valid_req.copy(account_pub = kycKey, recurrent = true).sign(priv)) - }.assertOkJson<SubjectResult>() - assertBalance("customer", "-KUDOS:7") - assertBalance("exchange", "+KUDOS:7") - - // Kyc key reuse keep pending ones - tx("customer", "KUDOS:1", "exchange", "Taler KYC:$kycKey") - assertBalance("customer", "-KUDOS:8") - assertBalance("exchange", "+KUDOS:8") - - // Switching to non recurrent cancel pending - client.post("/taler-prepared-transfer/registration") { - json(valid_req.copy(type= TransferType.kyc, account_pub = kycKey).sign(priv)) - }.assertOkJson<SubjectResult>() - assertBalance("customer", "-KUDOS:6") - assertBalance("exchange", "+KUDOS:6") - - // Check authorization field in incoming history - val (testPriv, testAuth) = EddsaPublicKey.randEdsaKeyPair() - val testKey = EddsaPublicKey.randEdsaKey() - val qr = subjectFmtQrBill(testAuth) - client.post("/taler-prepared-transfer/registration") { - json(valid_req.copy(account_pub=testKey, authorization_pub=testAuth, recurrent=true).sign(testPriv)) - }.assertOkJson<SubjectResult>() - tx("customer", "KUDOS:0.1", "exchange", "Taler MAP:$testAuth") - tx("customer", "KUDOS:0.1", "exchange", "Taler MAP:$testAuth") - tx("customer", "KUDOS:0.1", "exchange", "Taler MAP:$testAuth") - client.post("/taler-prepared-transfer/registration") { - json(valid_req.copy(type=TransferType.kyc, account_pub=testKey, authorization_pub=testAuth, recurrent=true).sign(testPriv)) - }.assertOkJson<SubjectResult>() - val otherPub = EddsaPublicKey.randEdsaKey() - client.post("/taler-prepared-transfer/registration") { - json(valid_req.copy(account_pub=otherPub, authorization_pub=testAuth, recurrent=true).sign(testPriv)) - }.assertOkJson<SubjectResult>() - val lastPub = EddsaPublicKey.randEdsaKey() - tx("customer", "KUDOS:0.1", "exchange", "Taler $lastPub") - tx("customer", "KUDOS:0.1", "exchange", "Taler KYC:$lastPub") - val history = client.getA("/accounts/exchange/taler-wire-gateway/history/incoming?limit=-5") - .assertOkJson<IncomingHistory>().incoming_transactions.map { - when (it) { - is IncomingKycAuthTransaction -> Pair(it.account_pub, it.authorization_pub) - is IncomingReserveTransaction -> Pair(it.reserve_pub, it.authorization_pub) - else -> throw UnsupportedOperationException() - } - } - assertContentEquals(history, listOf( - Pair(lastPub, null), - Pair(lastPub, null), - Pair(otherPub, testAuth), - Pair(testKey, testAuth), - Pair(testKey, testAuth) - )) - } - - // DELETE /taler-prepared-transfer/registration - @Test - fun unregistration() = bankSetup { - val (priv, pub) = EddsaPublicKey.randEdsaKeyPair() - val valid_req = SubjectRequest( - exchangePayto, - TransferType.reserve, - false, - TalerAmount("KUDOS:1"), - PublicKeyAlg.EdDSA, - pub, - pub, - EddsaSignature.rand() - ).sign(priv) - - val now = TalerTimestamp(Instant.now()) - val req = Unregistration( - now, - pub, - EddsaSignature.rand() - ).sign(priv) - - // Unknown - client.post("/taler-prepared-transfer/unregistration") { - json(req) - }.assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) - - // Know - client.post("/taler-prepared-transfer/registration") { - json(valid_req) - }.assertOkJson<SubjectResult>() - client.post("/taler-prepared-transfer/unregistration") { - json(req) - }.assertNoContent() - - // Idempotent - client.post("/taler-prepared-transfer/unregistration") { - json(req) - }.assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) - - // Bad signature - client.post("/taler-prepared-transfer/unregistration") { - json( - Unregistration( - now, - pub, - EddsaSignature.rand() - ) - ) - }.assertForbidden(TalerErrorCode.BANK_BAD_SIGNATURE) - - // Old timestamp - client.post("/taler-prepared-transfer/unregistration") { - json( - Unregistration( - TalerTimestamp(Instant.now().minusSeconds(1000000)), - pub, - EddsaSignature.rand() - ).sign(priv) - ) - }.assertConflict(TalerErrorCode.BANK_OLD_TIMESTAMP) - - // Unknown bounce - assertBalance("customer", "+KUDOS:0") - assertBalance("exchange", "+KUDOS:0") - tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // bounce - assertBalance("customer", "+KUDOS:0") - assertBalance("exchange", "+KUDOS:0") - - // Pending bounced after deletion - val newKey = EddsaPublicKey.randEdsaKey() - client.post("/taler-prepared-transfer/registration") { - json(valid_req.copy(account_pub=newKey, recurrent=true).sign(priv)) - }.assertOkJson<SubjectResult>() - tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // reserve - tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // pending - tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // pending - assertBalance("customer", "-KUDOS:3") - assertBalance("exchange", "+KUDOS:3") - client.post("/taler-prepared-transfer/unregistration") { - json(req) - }.assertNoContent() - assertBalance("customer", "-KUDOS:1") - assertBalance("exchange", "+KUDOS:1") - } -} diff --git a/libeufin-bank/src/test/kotlin/RevenueApiTest.kt b/libeufin-bank/src/test/kotlin/RevenueApiTest.kt @@ -1,60 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2023 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/> - */ - -import io.ktor.http.* -import io.ktor.client.request.* -import org.junit.Test -import tech.libeufin.common.RevenueIncomingHistory -import tech.libeufin.common.assertOk -import tech.libeufin.common.test.* - -class RevenueApiTest { - // GET /accounts/{USERNAME}/taler-revenue/config - @Test - fun config() = bankSetup { - client.get("/accounts/merchant/taler-revenue/config").assertOk() - } - - // GET /accounts/{USERNAME}/taler-revenue/history - @Test - fun history() = bankSetup { - setMaxDebt("exchange", "KUDOS:1000000") - authRoutine(HttpMethod.Get, "/accounts/merchant/taler-revenue/history") - historyRoutine<RevenueIncomingHistory>( - url = "/accounts/merchant/taler-revenue/history", - ids = { it.incoming_transactions.map { it.row_id } }, - registered = listOf( - { - // Transactions using clean transfer logic - transfer("KUDOS:10") - }, - { - // Common credit transactions - tx("exchange", "KUDOS:10", "merchant", "ignored") - } - ), - ignored = listOf( - { - // Ignore debit transactions - tx("merchant", "KUDOS:10", "customer") - } - ) - ) - } -} -\ No newline at end of file diff --git a/libeufin-bank/src/test/kotlin/SecurityTest.kt b/libeufin-bank/src/test/kotlin/SecurityTest.kt @@ -1,94 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2023-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/> - */ - -import io.ktor.client.request.* -import io.ktor.http.* -import io.ktor.http.content.* -import kotlinx.serialization.json.Json -import org.junit.Test -import tech.libeufin.common.* -import tech.libeufin.common.test.* - -inline fun <reified B> HttpRequestBuilder.jsonDeflate(b: B) { - val json = Json.encodeToString(kotlinx.serialization.serializer<B>(), b) - contentType(ContentType.Application.Json) - headers[HttpHeaders.ContentEncoding] = "deflate" - setBody(json.toByteArray().inputStream().deflate().readBytes()) -} - -inline fun <reified B> HttpRequestBuilder.jsonStreamDeflate(b: B) { - val json = Json.encodeToString(kotlinx.serialization.serializer<B>(), b) - headers[HttpHeaders.ContentEncoding] = "deflate" - setBody(OutputStreamContent({ - write(json.toByteArray().inputStream().deflate().readBytes()) - }, ContentType.Application.Json)) -} - -inline fun <reified B> HttpRequestBuilder.jsonStream(b: B) { - val json = Json.encodeToString(kotlinx.serialization.serializer<B>(), b) - setBody(OutputStreamContent({ - write(json.toByteArray()) - }, ContentType.Application.Json)) -} - -class SecurityTest { - @Test - fun bodySizeLimit() = bankSetup { - val valid_req = obj { - "payto_uri" to "$exchangePayto?message=payout" - "amount" to "KUDOS:0.3" - } - val too_big = obj(valid_req) { - "payto_uri" to "$exchangePayto?message=payout${"A".repeat(MAX_BODY_LENGTH+1)}" - } - - client.postA("/accounts/merchant/transactions") { - json(valid_req) - }.assertOk() - - // Check body too big - client.postA("/accounts/merchant/transactions") { - json(too_big) - }.assertPayloadTooLarge() - - // Check body too big even after compression - client.postA("/accounts/merchant/transactions") { - jsonDeflate(too_big) - }.assertPayloadTooLarge() - - // Check streaming body too big - client.postA("/accounts/merchant/transactions") { - jsonStream(too_big) - }.assertPayloadTooLarge() - - // Check streaming body too big even after compression - client.postA("/accounts/merchant/transactions") { - jsonStreamDeflate(too_big) - }.assertPayloadTooLarge() - - // Check unknown encoding - client.postA("/accounts/merchant/transactions") { - headers[HttpHeaders.ContentEncoding] = "unknown" - json(valid_req) - }.assertStatus(HttpStatusCode.UnsupportedMediaType, TalerErrorCode.GENERIC_COMPRESSION_INVALID) - } -} - - - diff --git a/libeufin-bank/src/test/kotlin/StatsTest.kt b/libeufin-bank/src/test/kotlin/StatsTest.kt @@ -1,216 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2024-2025 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/> - */ - -import io.ktor.client.request.* -import org.junit.Test -import tech.libeufin.bank.MonitorParams -import tech.libeufin.bank.MonitorResponse -import tech.libeufin.bank.MonitorWithConversion -import tech.libeufin.bank.Timeframe -import tech.libeufin.common.ShortHashCode -import tech.libeufin.common.TalerAmount -import tech.libeufin.common.assertOkJson -import tech.libeufin.common.db.* -import tech.libeufin.common.micros -import tech.libeufin.common.test.* -import java.time.Instant -import java.time.LocalDateTime -import java.time.ZoneOffset -import kotlin.test.assertEquals - -class StatsTest { - @Test - fun register() = bankSetup { db -> - setMaxDebt("merchant", "KUDOS:1000") - setMaxDebt("exchange", "KUDOS:1000") - setMaxDebt("customer", "KUDOS:1000") - - suspend fun cashin(amount: String) { - db.conn { conn -> - val stmt = conn.talerStatement("SELECT 0 FROM cashin(?, ?, (?, ?)::taler_amount, ?)") - stmt.bind(Instant.now()) - stmt.bind(ShortHashCode.rand()) - val amount = TalerAmount(amount) - stmt.bind(amount) - stmt.bind("") - stmt.executeQueryCheck() - } - } - - suspend fun monitor( - dbCount: (MonitorWithConversion) -> Long, - count: Long, - regionalVolume: (MonitorWithConversion) -> TalerAmount, - regionalAmount: String, - fiatVolume: ((MonitorWithConversion) -> TalerAmount)? = null, - fiatAmount: String? = null - ) { - Timeframe.entries.forEach { timeframe -> - client.getAdmin("/monitor?timestamp=${timeframe.name}").assertOkJson<MonitorResponse> { - val resp = it as MonitorWithConversion - assertEquals(count, dbCount(resp)) - assertEquals(TalerAmount(regionalAmount), regionalVolume(resp)) - fiatVolume?.run { assertEquals(TalerAmount(fiatAmount!!), this(resp)) } - } - } - } - - suspend fun monitorTalerIn(count: Long, amount: String) = - monitor({it.talerInCount}, count, {it.talerInVolume}, amount) - suspend fun monitorTalerOut(count: Long, amount: String) = - monitor({it.talerOutCount}, count, {it.talerOutVolume}, amount) - suspend fun monitorCashin(count: Long, regionalAmount: String, fiatAmount: String) = - monitor({it.cashinCount}, count, {it.cashinRegionalVolume}, regionalAmount, {it.cashinFiatVolume}, fiatAmount) - suspend fun monitorCashout(count: Long, regionalAmount: String, fiatAmount: String) = - monitor({it.cashoutCount}, count, {it.cashoutRegionalVolume}, regionalAmount, {it.cashoutFiatVolume}, fiatAmount) - - monitorTalerIn(0, "KUDOS:0") - monitorTalerOut(0, "KUDOS:0") - monitorCashin(0, "KUDOS:0", "EUR:0") - monitorCashout(0, "KUDOS:0", "EUR:0") - - addIncoming("KUDOS:3") - monitorTalerIn(1, "KUDOS:3") - addIncoming("KUDOS:7.6") - monitorTalerIn(2, "KUDOS:10.6") - addIncoming("KUDOS:12.3") - monitorTalerIn(3, "KUDOS:22.9") - - // KYC are ignored - addKyc("KUDOS:3") - monitorTalerIn(3, "KUDOS:22.9") - - transfer("KUDOS:10.0") - monitorTalerOut(1, "KUDOS:10.0") - transfer("KUDOS:30.5") - monitorTalerOut(2, "KUDOS:40.5") - transfer("KUDOS:42") - monitorTalerOut(3, "KUDOS:82.5") - - cashin("EUR:10") - monitorCashin(1, "KUDOS:7.98", "EUR:10") - monitorTalerIn(4, "KUDOS:30.88") - cashin("EUR:20") - monitorCashin(2, "KUDOS:23.96", "EUR:30") - monitorTalerIn(5, "KUDOS:46.86") - cashin("EUR:40") - monitorCashin(3, "KUDOS:55.94", "EUR:70") - monitorTalerIn(6, "KUDOS:78.84") - - cashout("KUDOS:3") - monitorCashout(1, "KUDOS:3", "EUR:3.77") - cashout("KUDOS:7.6") - monitorCashout(2, "KUDOS:10.6", "EUR:13.34") - cashout("KUDOS:12.3") - monitorCashout(3, "KUDOS:22.9", "EUR:28.83") - - monitorTalerIn(6, "KUDOS:78.84") - monitorTalerOut(3, "KUDOS:82.5") - monitorCashin(3, "KUDOS:55.94", "EUR:70") - monitorCashout(3, "KUDOS:22.9", "EUR:28.83") - } - - @Test - fun timeframe() = bankSetup { db -> - db.conn { conn -> - fun register(timestamp: LocalDateTime, amount: TalerAmount) { - val stmt = conn.talerStatement( - "CALL stats_register_payment('taler_out', ?::timestamp, (?, ?)::taler_amount, null)" - ) - stmt.bind(timestamp) - stmt.bind(amount) - stmt.executeUpdate() - } - - suspend fun check( - params: MonitorParams, - count: Long, - amount: TalerAmount - ) { - val res = db.monitor(params) - assertEquals(count, res.talerOutCount, "taler count") - assertEquals(amount, res.talerOutVolume, "taler volume") - } - - suspend fun checkSimple( - timestamp: LocalDateTime, - timeframe: Timeframe, - count: Long, - amount: TalerAmount - ) = check(MonitorParams(timeframe, timestamp), count, amount) - suspend fun checkWhich( - timestamp: LocalDateTime, - timeframe: Timeframe, - which: Int, - count: Long, - amount: TalerAmount - ) = check(MonitorParams(timeframe, timestamp, which), count, amount) - suspend fun checkDate( - secs: Long, - timeframe: Timeframe, - count: Long, - amount: TalerAmount - ) = check(MonitorParams(timeframe, secs), count, amount) - - val now = LocalDateTime.now(ZoneOffset.UTC) - val otherHour = now.withHour((now.hour + 1) % 24) - val otherDay = now.withDayOfMonth((now.dayOfMonth) % 28 + 1) - val otherMonth = now.withMonth((now.monthValue) % 12 + 1) - val otherYear = now.minusYears(1) - - register(now, TalerAmount("KUDOS:10.0")) - register(otherHour, TalerAmount("KUDOS:20.0")) - register(otherDay, TalerAmount("KUDOS:35.0")) - register(otherMonth, TalerAmount("KUDOS:40.0")) - register(otherYear, TalerAmount("KUDOS:50.0")) - - // Check with timestamp and truncating - checkSimple(now, Timeframe.hour, 1, TalerAmount("KUDOS:10.0")) - checkSimple(otherHour, Timeframe.hour, 1, TalerAmount("KUDOS:20.0")) - checkSimple(otherDay, Timeframe.day, 1, TalerAmount("KUDOS:35.0")) - checkSimple(otherMonth, Timeframe.month, 1, TalerAmount("KUDOS:40.0")) - checkSimple(otherYear, Timeframe.year, 1, TalerAmount("KUDOS:50.0")) - - // Check with timestamp and intervals - checkWhich(now, Timeframe.hour, now.hour, 1, TalerAmount("KUDOS:10.0")) - checkWhich(now, Timeframe.hour, otherHour.hour, 1, TalerAmount("KUDOS:20.0")) - checkWhich(now, Timeframe.day, otherDay.dayOfMonth, 1, TalerAmount("KUDOS:35.0")) - checkWhich(now, Timeframe.month, otherMonth.monthValue, 1, TalerAmount("KUDOS:40.0")) - checkWhich(now, Timeframe.year, otherYear.year, 1, TalerAmount("KUDOS:50.0")) - - // Check with date seconds - checkDate(now.toEpochSecond(ZoneOffset.UTC), Timeframe.hour, 1, TalerAmount("KUDOS:10.0")) - checkDate(otherHour.toEpochSecond(ZoneOffset.UTC), Timeframe.hour, 1, TalerAmount("KUDOS:20.0")) - checkDate(otherDay.toEpochSecond(ZoneOffset.UTC), Timeframe.day, 1, TalerAmount("KUDOS:35.0")) - checkDate(otherMonth.toEpochSecond(ZoneOffset.UTC), Timeframe.month, 1, TalerAmount("KUDOS:40.0")) - checkDate(otherYear.toEpochSecond(ZoneOffset.UTC), Timeframe.year, 1, TalerAmount("KUDOS:50.0")) - - // Check timestamp aggregation - checkSimple(now, Timeframe.day, 2, TalerAmount("KUDOS:30.0")) - checkSimple(now, Timeframe.month, 3, TalerAmount("KUDOS:65.0")) - checkSimple(now, Timeframe.year, 4, TalerAmount("KUDOS:105.0")) - checkWhich(now, Timeframe.day, now.dayOfMonth, 2, TalerAmount("KUDOS:30.0")) - checkWhich(now, Timeframe.month, now.monthValue, 3, TalerAmount("KUDOS:65.0")) - checkWhich(now, Timeframe.year, now.year, 4, TalerAmount("KUDOS:105.0")) - checkDate(now.toEpochSecond(ZoneOffset.UTC), Timeframe.day, 2, TalerAmount("KUDOS:30.0")) - checkDate(now.toEpochSecond(ZoneOffset.UTC), Timeframe.month, 3, TalerAmount("KUDOS:65.0")) - checkDate(now.toEpochSecond(ZoneOffset.UTC), Timeframe.year, 4, TalerAmount("KUDOS:105.0")) - } - } -} diff --git a/libeufin-bank/src/test/kotlin/WireGatewayApiTest.kt b/libeufin-bank/src/test/kotlin/WireGatewayApiTest.kt @@ -1,537 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2023, 2024, 2025, 2026 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/> - */ - -import io.ktor.http.* -import io.ktor.server.testing.* -import io.ktor.client.request.* -import org.junit.Test -import tech.libeufin.common.* -import tech.libeufin.common.crypto.CryptoUtil -import tech.libeufin.common.test.* -import kotlin.test.* - -class WireGatewayApiTest { - // GET /accounts/{USERNAME}/taler-wire-gateway/config - @Test - fun config() = bankSetup { - client.get("/accounts/merchant/taler-wire-gateway/config").assertOk() - } - - // POST /accounts/{USERNAME}/taler-wire-gateway/transfer - @Test - fun transfer() = bankSetup { - val valid_req = obj { - "request_uid" to HashCode.rand() - "amount" to "KUDOS:55" - "exchange_base_url" to "http://exchange.example.com/" - "wtid" to ShortHashCode.rand() - "credit_account" to merchantPayto.canonical - } - - authRoutine(HttpMethod.Post, "/accounts/merchant/taler-wire-gateway/transfer", valid_req) - - // Checking exchange debt constraint. - client.postA("/accounts/exchange/taler-wire-gateway/transfer") { - json(valid_req) - }.assertConflict(TalerErrorCode.BANK_UNALLOWED_DEBIT) - - // Giving debt allowance and checking the OK case. - setMaxDebt("exchange", "KUDOS:1000") - client.postA("/accounts/exchange/taler-wire-gateway/transfer") { - json(valid_req) - }.assertOk() - - // check idempotency - client.postA("/accounts/exchange/taler-wire-gateway/transfer") { - json(valid_req) - }.assertOk() - - val with_metadata = obj(valid_req) { - "request_uid" to HashCode.rand() - "metadata" to "ID" - "wtid" to ShortHashCode.rand() - } - client.postA("/accounts/exchange/taler-wire-gateway/transfer") { - json(with_metadata) - }.assertOk() - client.postA("/accounts/exchange/taler-wire-gateway/transfer") { - json(with_metadata) - }.assertOk() - - // Malformed metadata - listOf("bad_id", "bad id", "bad@id.com", "A".repeat(41)).forEach { - client.postA("/accounts/exchange/taler-wire-gateway/transfer") { - json(valid_req) { - "metadata" to it - } - }.assertBadRequest() - } - - val new_req = obj(valid_req) { - "request_uid" to HashCode.rand() - "wtid" to ShortHashCode.rand() - "credit_account" to adminPayto - } - - // Check conversion bounce - client.postA("/accounts/exchange/taler-wire-gateway/transfer") { - json(new_req) - }.assertOk() - - // check idempotency - client.postA("/accounts/exchange/taler-wire-gateway/transfer") { - json(new_req) - }.assertOk() - - - // Trigger conflict due to reused request_uid - client.postA("/accounts/exchange/taler-wire-gateway/transfer") { - json(valid_req) { - "wtid" to ShortHashCode.rand() - "exchange_base_url" to "http://different-exchange.example.com/" - } - }.assertConflict(TalerErrorCode.BANK_TRANSFER_REQUEST_UID_REUSED) - - // Trigger conflict due to reused wtid - client.postA("/accounts/exchange/taler-wire-gateway/transfer") { - json(valid_req) { - "request_uid" to HashCode.rand() - } - }.assertConflict(TalerErrorCode.BANK_TRANSFER_WTID_REUSED) - - // Currency mismatch - client.postA("/accounts/exchange/taler-wire-gateway/transfer") { - json(valid_req) { - "amount" to "EUR:33" - } - }.assertBadRequest(TalerErrorCode.GENERIC_CURRENCY_MISMATCH) - - // Same account - client.postA("/accounts/exchange/taler-wire-gateway/transfer") { - json(valid_req) { - "request_uid" to HashCode.rand() - "wtid" to ShortHashCode.rand() - "credit_account" to exchangePayto - } - }.assertConflict(TalerErrorCode.BANK_ACCOUNT_IS_EXCHANGE) - - // Bad BASE32 wtid - client.postA("/accounts/exchange/taler-wire-gateway/transfer") { - json(valid_req) { - "wtid" to "I love chocolate" - } - }.assertBadRequest() - - // Bad BASE32 len wtid - client.postA("/accounts/exchange/taler-wire-gateway/transfer") { - json(valid_req) { - "wtid" to randBase32Crockford(31) - } - }.assertBadRequest() - - // Bad BASE32 request_uid - client.postA("/accounts/exchange/taler-wire-gateway/transfer") { - json(valid_req) { - "request_uid" to "I love chocolate" - } - }.assertBadRequest() - - // Bad BASE32 len wtid - client.postA("/accounts/exchange/taler-wire-gateway/transfer") { - json(valid_req) { - "request_uid" to randBase32Crockford(65) - } - }.assertBadRequest() - - // Bad baseURL - for (bad in sequenceOf("not-a-url", "file://not.http.com/", "no.transport.com/", "https://not.a/base/url")) { - client.postA("/accounts/exchange/taler-wire-gateway/transfer") { - json(valid_req) { - "exchange_base_url" to bad - } - }.assertBadRequest() - } - } - - @Test - fun transferNoConversion() = bankSetup("test_no_conversion.conf") { - val valid_req = obj { - "request_uid" to HashCode.rand() - "amount" to "KUDOS:55" - "exchange_base_url" to "http://exchange.example.com/" - "wtid" to ShortHashCode.rand() - "credit_account" to merchantPayto.canonical - } - setMaxDebt("exchange", "KUDOS:1000") - - // Transfer works for common accounts - client.postA("/accounts/exchange/taler-wire-gateway/transfer") { - json(valid_req) - }.assertOk() - - // But fails to admin accounts - client.postA("/accounts/exchange/taler-wire-gateway/transfer") { - json(valid_req) { - "credit_account" to adminPayto - "request_uid" to HashCode.rand() - "wtid" to ShortHashCode.rand() - } - }.assertConflict(TalerErrorCode.BANK_ADMIN_CREDITOR) - } - - // GET /accounts/{USERNAME}/taler-wire-gateway/transfers/{ROW_ID} - @Test - fun transferById() = bankSetup { - var wtid = ShortHashCode.rand() - val valid_req = obj { - "request_uid" to HashCode.rand() - "amount" to "KUDOS:0.12" - "exchange_base_url" to "http://exchange.example.com/" - "wtid" to wtid - "credit_account" to merchantPayto.canonical - } - - authRoutine(HttpMethod.Get, "/accounts/merchant/taler-wire-gateway/transfers/1", requireExchange = true) - - val resp = client.postA("/accounts/exchange/taler-wire-gateway/transfer") { - json(valid_req) - }.assertOkJson<TransferResponse>() - - // Check OK - client.getA("/accounts/exchange/taler-wire-gateway/transfers/${resp.row_id}") - .assertOkJson<TransferStatus> { tx -> - assertEquals(TransferStatusState.success, tx.status) - assertEquals(TalerAmount("KUDOS:0.12"), tx.amount) - assertEquals("http://exchange.example.com/", tx.origin_exchange_url) - assertNull(tx.metadata) - assertEquals(wtid, tx.wtid) - assertEquals(resp.timestamp, tx.timestamp) - } - - client.postA("/accounts/exchange/taler-wire-gateway/transfer") { - json(valid_req) { - "request_uid" to HashCode.rand() - "metadata" to "ID" - "wtid" to ShortHashCode.rand() - } - }.assertOkJson<TransferResponse> { - client.getA("/accounts/exchange/taler-wire-gateway/transfers/${it.row_id}") - .assertOkJson<TransferStatus> { tx -> - assertEquals(tx.metadata, "ID") - } - } - - // Unknown account - wtid = ShortHashCode.rand() - client.postA("/accounts/exchange/taler-wire-gateway/transfer") { - json(valid_req) { - "request_uid" to HashCode.rand() - "wtid" to wtid - "credit_account" to unknownPayto - } - }.assertOkJson<TransferResponse> { resp -> - client.getA("/accounts/exchange/taler-wire-gateway/transfers/${resp.row_id}") - .assertOkJson<TransferStatus> { tx -> - assertEquals(TransferStatusState.permanent_failure, tx.status) - assertEquals(TalerAmount("KUDOS:0.12"), tx.amount) - assertEquals("http://exchange.example.com/", tx.origin_exchange_url) - assertEquals(wtid, tx.wtid) - assertEquals(resp.timestamp, tx.timestamp) - } - } - // Check unknown transaction - client.getA("/accounts/exchange/taler-wire-gateway/transfers/42") - .assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) - // Check another user's transaction - client.getA("/accounts/merchant/taler-wire-gateway/transfers/${resp.row_id}") - .assertConflict(TalerErrorCode.BANK_ACCOUNT_IS_NOT_EXCHANGE) - } - - // GET /accounts/{USERNAME}/taler-wire-gateway/transfers - @Test - fun transferPage() = bankSetup { - authRoutine(HttpMethod.Get, "/accounts/merchant/taler-wire-gateway/transfers", requireExchange = true) - - client.getA("/accounts/exchange/taler-wire-gateway/transfers").assertNoContent() - - repeat(5) { - client.postA("/accounts/exchange/taler-wire-gateway/transfer") { - json { - "request_uid" to HashCode.rand() - "amount" to "KUDOS:0.12" - "exchange_base_url" to "http://exchange.example.com/" - "wtid" to ShortHashCode.rand() - "credit_account" to merchantPayto - } - }.assertOkJson<TransferResponse>() - } - client.getA("/accounts/exchange/taler-wire-gateway/transfers") - .assertOkJson<TransferList> { - assertEquals(5, it.transfers.size) - assertEquals( - it, - client.getA("/accounts/exchange/taler-wire-gateway/transfers?status=success").assertOkJson<TransferList>() - ) - } - client.getA("/accounts/exchange/taler-wire-gateway/transfers?status=pending").assertNoContent() - client.getA("/accounts/exchange/taler-wire-gateway/transfers?status=permanent_failure").assertNoContent() - - client.postA("/accounts/exchange/taler-wire-gateway/transfer") { - json { - "request_uid" to HashCode.rand() - "amount" to "KUDOS:0.12" - "exchange_base_url" to "http://exchange.example.com/" - "wtid" to ShortHashCode.rand() - "credit_account" to unknownPayto - } - }.assertOkJson<TransferResponse>() - client.getA("/accounts/exchange/taler-wire-gateway/transfers").assertOkJson<TransferList> { - assertEquals(6, it.transfers.size) - } - client.getA("/accounts/exchange/taler-wire-gateway/transfers?status=success").assertOkJson<TransferList> { - assertEquals(5, it.transfers.size) - } - client.getA("/accounts/exchange/taler-wire-gateway/transfers?status=permanent_failure").assertOkJson<TransferList> { - assertEquals(1, it.transfers.size) - } - } - - // GET /accounts/{USERNAME}/taler-wire-gateway/history/incoming - @Test - fun historyIncoming() = bankSetup { - // Give Foo reasonable debt allowance: - setMaxDebt("merchant", "KUDOS:1000") - authRoutine(HttpMethod.Get, "/accounts/merchant/taler-wire-gateway/history/incoming", requireExchange = true) - historyRoutine<IncomingHistory>( - url = "/accounts/exchange/taler-wire-gateway/history/incoming", - ids = { it.incoming_transactions.map { it.row_id } }, - registered = listOf( - // Reserve transactions using clean add incoming logic - { addIncoming("KUDOS:10") }, - - // Reserve transactions using raw bank transaction logic - { tx("merchant", "KUDOS:10", "exchange", "history test with ${EddsaPublicKey.randEdsaKey()} reserve pub") }, - - // Reserve transactions using withdraw logic - { withdrawal("KUDOS:9") }, - - // KYC transaction using clean add incoming logic - { addKyc("KUDOS:2") }, - - // KYC transactions using raw bank transaction logic - { tx("merchant", "KUDOS:2", "exchange", "history test with KYC:${EddsaPublicKey.randEdsaKey()} account pub") }, - ), - ignored = listOf( - // Ignore malformed incoming transaction - { tx("merchant", "KUDOS:10", "exchange", "ignored") }, - - // Ignore malformed outgoing transaction - { tx("exchange", "KUDOS:10", "merchant", "ignored") }, - ) - ) - } - - // GET /accounts/{USERNAME}/taler-wire-gateway/history/outgoing - @Test - fun historyOutgoing() = bankSetup { - setMaxDebt("exchange", "KUDOS:1000000") - authRoutine(HttpMethod.Get, "/accounts/merchant/taler-wire-gateway/history/outgoing", requireExchange = true) - historyRoutine<OutgoingHistory>( - url = "/accounts/exchange/taler-wire-gateway/history/outgoing", - ids = { it.outgoing_transactions.map { it.row_id } }, - registered = listOf( - // Transactions using clean add incoming logic - { transfer("KUDOS:10") }, - - // And with metadata - { transfer("KUDOS:12", metadata = "CON:ID") } - ), - ignored = listOf( - // Failed transfer - { transfer("KUDOS:10", unknownPayto) }, - - // Ignore manual outgoing transaction - { tx("exchange", "KUDOS:10", "merchant", "${ShortHashCode.rand()} http://exchange.example.com/") }, - - // Ignore malformed incoming transaction - { tx("merchant", "KUDOS:10", "exchange", "ignored") }, - - // Ignore malformed outgoing transaction - { tx("exchange", "KUDOS:10", "merchant", "ignored") }, - ) - ) - assertContentEquals( - client.getA("/accounts/exchange/taler-wire-gateway/history/outgoing?limit=2") - .assertOkJson<OutgoingHistory>() - .outgoing_transactions - .map { it.amount.toString() to it.metadata } - ,listOf( - "KUDOS:10" to null, - "KUDOS:12" to "CON:ID", - ) - ) - } - - suspend fun ApplicationTestBuilder.talerAddIncomingRoutine(type: IncomingType) { - val (path, key) = when (type) { - IncomingType.reserve -> Pair("add-incoming", "reserve_pub") - IncomingType.kyc -> Pair("add-kycauth", "account_pub") - IncomingType.map -> Pair("add-mapped", "authorization_pub") - } - - val (priv, pub) = EddsaPublicKey.randEdsaKeyPair() - client.post("/taler-prepared-transfer/registration") { - json(SubjectRequest( - exchangePayto, - TransferType.reserve, - false, - TalerAmount("KUDOS:44"), - PublicKeyAlg.EdDSA, - pub, - pub, - EddsaSignature.rand() - ).sign(priv)) - }.assertOkJson<SubjectResult>() - val valid_req = obj { - "amount" to "KUDOS:44" - key to pub - "debit_account" to merchantPayto.canonical - } - - authRoutine(HttpMethod.Post, "/accounts/merchant/taler-wire-gateway/admin/$path", valid_req, requireAdmin = true) - - // Checking exchange debt constraint. - client.postA("/accounts/exchange/taler-wire-gateway/admin/$path") { - json(valid_req) - }.assertConflict(TalerErrorCode.BANK_UNALLOWED_DEBIT) - - // Giving debt allowance and checking the OK case. - setMaxDebt("merchant", "KUDOS:1000") - client.postA("/accounts/exchange/taler-wire-gateway/admin/$path") { - json(valid_req) - }.assertOk() - - when (type) { - IncomingType.reserve -> { - // Trigger conflict due to reused reserve_pub - client.postA("/accounts/exchange/taler-wire-gateway/admin/$path") { - json(valid_req) - }.assertConflict(TalerErrorCode.BANK_DUPLICATE_RESERVE_PUB_SUBJECT) - } - IncomingType.kyc -> { - // Non conflict on reuse - client.postA("/accounts/exchange/taler-wire-gateway/admin/$path") { - json(valid_req) - }.assertOk() - } - IncomingType.map -> { - // Trigger conflict due to reused authorization_pub - client.postA("/accounts/exchange/taler-wire-gateway/admin/$path") { - json(valid_req) - }.assertConflict(TalerErrorCode.BANK_TRANSFER_MAPPING_REUSED) - // Trigger conflict due to unknown authorization_pub - client.postA("/accounts/exchange/taler-wire-gateway/admin/$path") { - json(valid_req) { - key to EddsaPublicKey.randEdsaKey() - } - }.assertConflict(TalerErrorCode.BANK_TRANSFER_MAPPING_UNKNOWN) - } - } - - // Currency mismatch - client.postA("/accounts/exchange/taler-wire-gateway/admin/$path") { - json(valid_req) { "amount" to "EUR:33" } - }.assertBadRequest(TalerErrorCode.GENERIC_CURRENCY_MISMATCH) - - // Unknown account - client.postA("/accounts/exchange/taler-wire-gateway/admin/$path") { - json(valid_req) { - key to EddsaPublicKey.randEdsaKey() - "debit_account" to unknownPayto - } - }.assertConflict(TalerErrorCode.BANK_UNKNOWN_DEBTOR) - - // Same account - client.postA("/accounts/exchange/taler-wire-gateway/admin/$path") { - json(valid_req) { - key to EddsaPublicKey.randEdsaKey() - "debit_account" to exchangePayto - } - }.assertConflict(TalerErrorCode.BANK_ACCOUNT_IS_EXCHANGE) - - // Bad BASE32 reserve_pub - client.postA("/accounts/exchange/taler-wire-gateway/admin/$path") { - json(valid_req) { - key to "I love chocolate" - } - }.assertBadRequest() - - // Bad BASE32 len reserve_pub - client.postA("/accounts/exchange/taler-wire-gateway/admin/$path") { - json(valid_req) { - key to randBase32Crockford(31) - } - }.assertBadRequest() - } - - // POST /accounts/{USERNAME}/taler-wire-gateway/admin/add-incoming - @Test - fun addIncoming() = bankSetup { - talerAddIncomingRoutine(IncomingType.reserve) - } - - // POST /accounts/{USERNAME}/taler-wire-gateway/admin/add-kycauth - @Test - fun addKycAuth() = bankSetup { - talerAddIncomingRoutine(IncomingType.kyc) - } - - // POST /accounts/{USERNAME}/taler-wire-gateway/admin/add-mapped - @Test - fun addMapped() = bankSetup { - talerAddIncomingRoutine(IncomingType.map) - } - - @Test - fun addIncomingMix() = bankSetup { - addIncoming("KUDOS:1") - addKyc("KUDOS:2") - tx("merchant", "KUDOS:3", "exchange", "test with ${EddsaPublicKey.randEdsaKey()} reserve pub") - tx("merchant", "KUDOS:4", "exchange", "test with KYC:${EddsaPublicKey.randEdsaKey()} account pub") - client.getA("/accounts/exchange/taler-wire-gateway/history/incoming?limit=25").assertOkJson<IncomingHistory> { - assertEquals(4, it.incoming_transactions.size) - it.incoming_transactions.forEachIndexed { i, tx -> - assertEquals(TalerAmount("KUDOS:${i+1}"), tx.amount) - if (i % 2 == 1) { - assertIs<IncomingKycAuthTransaction>(tx) - } else { - assertIs<IncomingReserveTransaction>(tx) - } - } - } - } - - // POST /taler-wire-gateway/account/check - @Test - fun accountCheck() = bankSetup { - client.getA("/accounts/exchange/taler-wire-gateway/account/check").assertBadRequest(TalerErrorCode.GENERIC_PARAMETER_MISSING) - client.getA("/accounts/exchange/taler-wire-gateway/account/check?account=$unknownPayto").assertNotFound(TalerErrorCode.BANK_UNKNOWN_ACCOUNT) - client.getA("/accounts/exchange/taler-wire-gateway/account/check?account=$merchantPayto").assertOkJson<AccountInfo>() - } -} -\ No newline at end of file diff --git a/libeufin-bank/src/test/kotlin/bench.kt b/libeufin-bank/src/test/kotlin/bench.kt @@ -1,439 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2024, 2025, 2026 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/> - */ - -import io.ktor.client.request.* -import io.ktor.http.* -import org.junit.Test -import org.postgresql.jdbc.PgConnection -import tech.libeufin.bank.* -import tech.libeufin.common.* -import tech.libeufin.common.crypto.PwCrypto -import tech.libeufin.common.crypto.CryptoUtil -import tech.libeufin.common.test.* -import java.time.Instant -import java.time.LocalDateTime -import java.time.ZoneId -import java.util.* -import kotlin.math.max - -class Bench { - - /** Generate [amount] rows to fill the database */ - fun genData(conn: PgConnection, amount: Int) { - val amount = max(amount, 10) - - // Skip 4 accounts created by bankSetup - val skipAccount = 4 - // Customer account will be used in tests so we want to generate more data for him - val customerAccount = 3 - val exchangeAccount = 2 - // In general half of the data is for generated account and half is for customer - val mid = amount / 2 - - val password = PwCrypto.Bcrypt(cost = 4).hashpw("password") - - val token16 = ByteArray(16) - val token32 = ByteArray(32) - val token64 = ByteArray(64) - - val accountPubs = List(amount*2) { EddsaPublicKey.randEdsaKey() } - - conn.genData(amount, sequenceOf( - "customers(username, name, password_hash, cashout_payto)" to { - "account_$it\t$password\tMr n°$it\t$unknownPayto\n" - }, - "conversion_rate_classes(name)" to { - "Class n0$it\n" - }, - "bank_accounts(internal_payto, owning_customer_id, is_public,conversion_rate_class_id)" to { - val conversionId = when (it%5) { - 0 -> "\\N" - 1, 2 -> "1" - 3 -> "2" - else -> it%10 - } - "payto://x-taler-bank/localhost/account_$it\t${it+skipAccount}\t${it%3==0}\t$conversionId\n" - }, - "bearer_tokens(content, creation_time, expiration_time, scope, is_refreshable, bank_customer, description, last_access)" to { - val account = if (it > mid) customerAccount else it+4 - val hex = token32.rand().encodeHex() - "\\\\x$hex\t0\t0\treadonly\tfalse\t$account\t\\N\t0\n" - }, - "bank_account_transactions(creditor_payto, creditor_name, debtor_payto, debtor_name, subject, amount, transaction_date, direction, bank_account_id)" to { - val account = if (it > mid) customerAccount else it+4 - "$unknownPayto\tcreditor_name\t$unknownPayto\tdebtor_name\tsubject\t(42,0)\t0\tcredit\t$exchangeAccount\n" + - "$unknownPayto\tcreditor_name\t$unknownPayto\tdebtor_name\tsubject\t(42,0)\t0\tdebit\t$account\n" - }, - "bank_transaction_operations" to { - val hex = token32.rand().encodeHex() - "\\\\x$hex\t$it\n" - }, - "tan_challenges(uuid, hbody, salt, op, code, creation_date, expiration_date, retry_counter, customer, tan_channel, tan_info)" to { - val account = if (it > mid) customerAccount else it+4 - val uuid = UUID.randomUUID() - val hex16 = token16.rand().encodeHex() - val hex64 = token64.rand().encodeHex() - "$uuid\t\\\\x$hex64\t\\\\x$hex16\taccount_reconfig\tcode\t0\t0\t0\t$account\tsms\tinfo\n" - }, - "taler_withdrawal_operations(withdrawal_uuid, wallet_bank_account, reserve_pub, creation_date)" to { - val account = if (it > mid) customerAccount else it+4 - val hex = token32.rand().encodeHex() - val uuid = UUID.randomUUID() - "$uuid\t$account\t\\\\x$hex\t0\n" - }, - "prepared_transfers(type, account_pub, authorization_pub, authorization_sig, recurrent, registered_at, bank_transaction_id)" to { - val type = if (it % 2 == 0) "reserve" else "kyc" - val recurrent = if (it % 3 == 0) "true" else "false" - val incoming_transaction_id = if (it % 5 == 0) "\\N" else "${it*2}" - val hex = accountPubs[it].raw.encodeHex() - val hex64 = token64.rand().encodeHex() - "$type\t\\\\x$hex\t\\\\x$hex\t\\\\x$hex64\t$recurrent\t0\t$incoming_transaction_id\n" - }, - "pending_recurrent_incoming_transactions(bank_transaction_id, debtor_account_id, authorization_pub)" to { - val hex = accountPubs[it].raw.encodeHex() - "${it*2}\t${it}\t\\\\x$hex\n" - }, - "taler_exchange_outgoing(bank_transaction)" to { - "${it*2-1}\n" - }, - "transfer_operations(wtid, request_uid, amount, exchange_base_url, exchange_outgoing_id, exchange_id, transfer_date, creditor_payto, status, status_msg)" to { - val hex32 = token32.rand().encodeHex() - val hex64 = token64.rand().encodeHex() - if (it % 2 == 0) { - "\\\\x$hex32\t\\\\x$hex64\t(42, 0)\turl\t$it\t$it\t0\tpayto://x-taler-bank/localhost/10\tsuccess\t\\N\n" - } else { - "\\\\x$hex32\t\\\\x$hex64\t(42, 0)\turl\t\\N\t$it\t0\tpayto://x-taler-bank/localhost/10\tpermanent_failure\tfailure\n" - } - }, - "taler_exchange_incoming(type, metadata, bank_transaction)" to { - val hex = token32.rand().encodeHex() - if (it % 2 == 0) { - "reserve\t\\\\x$hex\t${it*2}\n" - } else { - "kyc\t\\\\x$hex\t${it*2}\n" - } - }, - "bank_stats(timeframe, start_time)" to { - val instant = Instant.ofEpochSecond(it.toLong()) - val date = LocalDateTime.ofInstant(instant, ZoneId.of("UTC")) - "day\t$date\n" - }, - "cashout_operations(request_uid,amount_debit,amount_credit,subject,creation_time,bank_account,local_transaction)" to { - val account = if (it > mid) customerAccount else it+4 - val hex = token32.rand().encodeHex() - "\\\\x$hex\t(0,0)\t(0,0)\tsubject\t0\t$account\t$it\n" - } - )) - } - - @Test - fun benchDb() = bench { AMOUNT -> bankSetup { db -> - // Prepare customer accounts - fillCashoutInfo("customer") - setMaxDebt("customer", "KUDOS:1000000") - - // Generate data - db.conn { genData(it, AMOUNT) } - - val accountPubs = List(AMOUNT) { EddsaPublicKey.randEdsaKeyPair() } - - // Warm HTTP client - client.get("/config").assertOk() - - // Accounts - measureAction("account_create") { - client.post("/accounts") { - json { - "username" to "account_bench_$it" - "password" to "account_bench_$it-password" - "name" to "Bench Account $it" - } - }.assertOkJson<RegisterAccountResponse>().internal_payto_uri - } - measureAction("account_reconfig") { - client.patchA("/accounts/account_bench_$it") { - json { - "name" to "New Bench Account $it" - } - }.assertNoContent() - } - measureAction("account_reconfig_auth") { - client.patchA("/accounts/account_bench_$it/auth") { - json { - "old_password" to "account_bench_$it-password" - "new_password" to "account_bench_$it-password" - } - }.assertNoContent() - } - measureAction("account_list") { - client.getAdmin("/accounts").assertOk() - } - measureAction("account_list_class") { - client.getAdmin("/accounts?conversion_rate_class=${it%10}").assertOk() - } - measureAction("account_list_name") { - client.getAdmin("/accounts?name=Mr").assertOk() - } - measureAction("account_list_public") { - client.get("/public-accounts").assertOk() - } - measureAction("account_get") { - client.getA("/accounts/account_bench_$it").assertOk() - } - - // Tokens - val tokens = measureAction("token_create") { - client.postPw("/accounts/customer/token") { - json { - "scope" to "readonly" - "refreshable" to true - } - }.assertOkJson<TokenSuccessResponse>().access_token - } - measureAction("token_refresh") { - client.post("/accounts/customer/token") { - headers[HttpHeaders.Authorization] = "Bearer ${tokens[it]}" - json { "scope" to "readonly" } - }.assertOk() - } - measureAction("token_list") { - client.getA("/accounts/customer/tokens").assertOk() - } - measureAction("token_delete") { - client.delete("/accounts/customer/token") { - headers[HttpHeaders.Authorization] = "Bearer ${tokens[it]}" - }.assertNoContent() - } - - // Conversion rate classes - val classes = measureAction("class_create") { - client.postAdmin("/conversion-rate-classes") { - json { - "name" to "Gen class $it" - } - }.assertOkJson<ConversionRateClassResponse>().conversion_rate_class_id - } - measureAction("class_patch") { - client.patchAdmin("/conversion-rate-classes/${classes[it]}") { - json { - "name" to "Gen class $it" - "description" to "test $it" - } - }.assertNoContent() - } - measureAction("class_get") { - client.getAdmin("/conversion-rate-classes/${classes[it]}").assertOk() - } - measureAction("class_list") { - client.getAdmin("/conversion-rate-classes").assertOk() - } - measureAction("class_delete") { - client.deleteAdmin("/conversion-rate-classes/${classes[it]}").assertNoContent() - } - - // Transaction - val transactions = measureAction("transaction_create") { - client.postA("/accounts/customer/transactions") { - json { - "payto_uri" to "$merchantPayto?receiver-name=Test&message=payout" - "amount" to "KUDOS:0.0001" - } - }.assertOkJson<TransactionCreateResponse>().row_id - } - measureAction("transaction_get") { - client.getA("/accounts/customer/transactions/${transactions[it]}").assertOk() - } - measureAction("transaction_history") { - client.getA("/accounts/customer/transactions").assertOk() - } - measureAction("transaction_revenue") { - client.getA("/accounts/merchant/taler-revenue/history").assertOk() - } - - // Withdrawal - val withdrawals = measureAction("withdrawal_create") { - client.postA("/accounts/customer/withdrawals") { - json { - "amount" to "KUDOS:0.0001" - } - }.assertOkJson<BankAccountCreateWithdrawalResponse>().withdrawal_id - } - measureAction("withdrawal_get") { - client.get("/withdrawals/${withdrawals[it]}").assertOk() - } - measureAction("withdrawal_status") { - client.get("/taler-integration/withdrawal-operation/${withdrawals[it]}").assertOk() - } - measureAction("withdrawal_select") { - client.post("/taler-integration/withdrawal-operation/${withdrawals[it]}") { - json { - "reserve_pub" to EddsaPublicKey.randEdsaKey() - "selected_exchange" to exchangePayto - } - }.assertOk() - } - measureAction("withdrawal_confirm") { - client.postA("/accounts/customer/withdrawals/${withdrawals[it]}/confirm") - .assertNoContent() - } - measureAction("withdrawal_abort") { - val uuid = client.postA("/accounts/customer/withdrawals") { - json { - "amount" to "KUDOS:0.0001" - } - }.assertOkJson<BankAccountCreateWithdrawalResponse>().withdrawal_id - client.postA("/accounts/customer/withdrawals/$uuid/abort") - .assertNoContent() - } - - // Cashout - convert("KUDOS:0.1") - val cashouts = measureAction("cashout_create") { - client.postA("/accounts/customer/cashouts") { - json { - "request_uid" to ShortHashCode.rand() - "amount_debit" to "KUDOS:0.1" - "amount_credit" to convert("KUDOS:0.1") - } - }.assertOkJson<CashoutResponse>().cashout_id - } - measureAction("cashout_get") { - client.getA("/accounts/customer/cashouts/${cashouts[it]}").assertOk() - } - measureAction("cashout_history") { - client.getA("/accounts/customer/cashouts").assertOk() - } - measureAction("cashout_history_admin") { - client.getAdmin("/cashouts").assertOk() - } - - // Wire gateway - val transfers = measureAction("wg_transfer") { - client.postA("/accounts/exchange/taler-wire-gateway/transfer") { - json { - "request_uid" to HashCode.rand() - "amount" to "KUDOS:0.0001" - "exchange_base_url" to "http://exchange.example.com/" - "wtid" to ShortHashCode.rand() - "credit_account" to customerPayto.canonical - } - }.assertOkJson<TransferResponse>().row_id - } - measureAction("wg_transfer_get") { - client.getA("/accounts/exchange/taler-wire-gateway/transfers/${transfers[it]}").assertOk() - } - measureAction("wg_transfer_page") { - client.getA("/accounts/exchange/taler-wire-gateway/transfers").assertOk() - } - measureAction("wg_transfer_page_filter") { - client.getA("/accounts/exchange/taler-wire-gateway/transfers?status=success").assertOk() - } - measureAction("wg_add") { - client.postA("/accounts/exchange/taler-wire-gateway/admin/add-incoming") { - json { - "amount" to "KUDOS:0.0001" - "reserve_pub" to EddsaPublicKey.randEdsaKey() - "debit_account" to customerPayto.canonical - } - }.assertOk() - } - measureAction("wg_incoming") { - client.getA("/accounts/exchange/taler-wire-gateway/history/incoming") - .assertOk() - } - measureAction("wg_outgoing") { - client.getA("/accounts/exchange/taler-wire-gateway/history/outgoing") - .assertOk() - } - - // TAN challenges - val challenges = measureAction("tan_send") { - val res = client.patchA("/accounts/account_bench_$it") { - json { - "contact_data" to obj { - "phone" to "+99" - "email" to "email@example.com" - } - "tan_channel" to "sms" - } - }.assertAcceptedJson<ChallengeResponse>() - val challenge = res.challenges[0] - client.postA("/accounts/account_bench_$it/challenge/${challenge.challenge_id}").assertOk() - val code = tanCode(challenge.tan_info) - Pair(challenge.challenge_id, code) - } - measureAction("tan_confirm") { - val (id, code) = challenges[it] - client.postA("/accounts/account_bench_$it/challenge/$id/confirm") { - json { "tan" to code } - }.assertNoContent() - } - - // Wire transfer - /*measureAction("wt_register") { - val (priv, pub) = accountPubs[it] - val valid_req = obj { - "credit_account" to exchangePayto - "credit_amount" to "KUDOS:55" - "type" to "reserve" - "alg" to "EdDSA" - "account_pub" to pub - "authorization_pub" to pub - "authorization_sig" to CryptoUtil.eddsaSign(pub.raw, priv) - "recurrent" to false - } - client.post("/taler-prepared-transfer/registration") { - json(valid_req) - }.assertOkJson<SubjectResult>() - client.post("/taler-prepared-transfer/registration") { - json(valid_req) - }.assertOkJson<SubjectResult>() - } - measureAction("wt_unregister") { - val (priv, pub) = accountPubs[it] - val now = Instant.now().toString() - val valid_req = obj { - "timestamp" to now - "authorization_pub" to pub - "authorization_sig" to CryptoUtil.eddsaSign(now.toByteArray(), priv) - } - client.post("/taler-prepared-transfer/unregistration") { - json(valid_req) - }.assertNoContent() - client.post("/taler-prepared-transfer/unregistration") { - json(valid_req) - }.assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) - }*/ - - // Delete accounts - - measureAction("account_delete") { - client.deleteA("/accounts/account_bench_$it").assertNoContent() - } - - // Other - measureAction("monitor") { - client.getAdmin("/monitor").assertOk() - } - db.gc.collect(Instant.now(), java.time.Duration.ZERO, java.time.Duration.ZERO, java.time.Duration.ZERO) - measureAction("gc") { - db.gc.collect(Instant.now(), java.time.Duration.ZERO, java.time.Duration.ZERO, java.time.Duration.ZERO) - } - } } -} -\ No newline at end of file diff --git a/libeufin-bank/src/test/kotlin/helpers.kt b/libeufin-bank/src/test/kotlin/helpers.kt @@ -1,413 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2023, 2024, 2025, 2026 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/> - */ - -import io.ktor.client.request.* -import io.ktor.client.statement.* -import io.ktor.http.* -import io.ktor.server.testing.* -import kotlinx.coroutines.runBlocking -import tech.libeufin.bank.* -import tech.libeufin.bank.db.AccountDAO.AccountCreationResult -import tech.libeufin.bank.db.Database -import tech.libeufin.common.* -import tech.libeufin.common.test.* -import tech.libeufin.common.db.dbInit -import tech.libeufin.common.db.pgDataSource -import java.nio.file.NoSuchFileException -import kotlin.io.path.Path -import kotlin.io.path.deleteExisting -import kotlin.io.path.readText -import kotlin.random.Random -import kotlin.test.assertEquals -import kotlin.test.assertIs -import kotlin.test.assertNotNull -import java.time.Duration -import java.time.Instant - -/* ----- Setup ----- */ - -val merchantPayto = IbanPayto.rand() -val exchangePayto = IbanPayto.rand() -val customerPayto = IbanPayto.rand() -val unknownPayto = IbanPayto.rand() -var tmpPayTo = IbanPayto.rand() -lateinit var adminPayto: String -val paytos = mapOf( - "merchant" to merchantPayto, - "exchange" to exchangePayto, - "customer" to customerPayto -) - -fun genTmpPayTo(): IbanPayto { - tmpPayTo = IbanPayto.rand() - return tmpPayTo -} - -fun setup( - conf: String = "test.conf", - lambda: suspend (Database, BankConfig) -> Unit -) = runBlocking { - globalTestTokens.clear() - val cfg = bankConfig(Path("conf/$conf")) - pgDataSource(cfg.dbCfg.dbConnStr).run { - dbInit(cfg.dbCfg, "libeufin-bank", true) - dbInit(cfg.dbCfg, "libeufin-nexus", true) - } - cfg.withDb { db, cfg -> - db.conn { conn -> - val sqlProcedures = Path("${cfg.dbCfg.sqlDir}/libeufin-conversion-setup.sql") - conn.execSQLUpdate(sqlProcedures.readText()) - } - lambda(db, cfg) - } -} - -fun bankSetup( - conf: String = "test.conf", - lambda: suspend ApplicationTestBuilder.(Database) -> Unit -) = setup(conf) { db, cfg -> - // Creating the exchange and merchant accounts first. - val bonus = TalerAmount.zero("KUDOS") - assertIs<AccountCreationResult.Success>(db.account.create( - username = "merchant", - password = "merchant-password", - name = "Merchant", - internalPayto = merchantPayto, - maxDebt = TalerAmount("KUDOS:10"), - isTalerExchange = false, - isPublic = false, - bonus = bonus, - checkPaytoIdempotent = false, - email = null, - phone = null, - cashoutPayto = null, - tanChannels = emptySet(), - conversionRateClassId = null, - pwCrypto = cfg.pwCrypto - )) - assertIs<AccountCreationResult.Success>(db.account.create( - username = "exchange", - password = "exchange-password", - name = "Exchange", - internalPayto = exchangePayto, - maxDebt = TalerAmount("KUDOS:10"), - isTalerExchange = true, - isPublic = false, - bonus = bonus, - checkPaytoIdempotent = false, - email = null, - phone = null, - cashoutPayto = null, - tanChannels = emptySet(), - conversionRateClassId = null, - pwCrypto = cfg.pwCrypto - )) - assertIs<AccountCreationResult.Success>(db.account.create( - username = "customer", - password = "customer-password", - name = "Customer", - internalPayto = customerPayto, - maxDebt = TalerAmount("KUDOS:10"), - isTalerExchange = false, - isPublic = false, - bonus = bonus, - checkPaytoIdempotent = false, - email = null, - phone = null, - cashoutPayto = null, - tanChannels = emptySet(), - conversionRateClassId = null, - pwCrypto = cfg.pwCrypto - )) - // Create admin account - val result = assertIs<AccountCreationResult.Success>(createAdminAccount(db, cfg, "admin-password")) - adminPayto = result.payto - testApplication { - application { - corebankWebApp(db, cfg) - } - if (cfg.allowConversion) { - // Set conversion rates - client.postAdmin("/conversion-info/conversion-rate") { - json { - "cashin_ratio" to "0.8" - "cashin_fee" to "KUDOS:0.02" - "cashin_tiny_amount" to "KUDOS:0.01" - "cashin_rounding_mode" to "nearest" - "cashin_min_amount" to "EUR:0" - "cashout_ratio" to "1.26" - "cashout_fee" to "EUR:0.003" - "cashout_tiny_amount" to "EUR:0.01" - "cashout_rounding_mode" to "zero" - "cashout_min_amount" to "KUDOS:0.1" - } - }.assertNoContent() - } - lambda(db) - // GC everything - db.gc.collect(Instant.now(), Duration.ZERO, Duration.ZERO, Duration.ZERO) - } -} - -fun dbSetup(lambda: suspend (Database) -> Unit) = - setup { db, _ -> lambda(db) } - -/* ----- Common actions ----- */ - -/** Set [account] debit threshold to [maxDebt] amount */ -suspend fun ApplicationTestBuilder.setMaxDebt(account: String, maxDebt: String) { - client.patchAdmin("/accounts/$account") { - json { "debit_threshold" to maxDebt } - }.assertNoContent() -} - -/** Check [account] balance is [amount], [amount] is prefixed with + for credit and - for debit */ -suspend fun ApplicationTestBuilder.assertBalance(account: String, amount: String) { - client.getAdmin("/accounts/$account").assertOkJson<AccountData> { - val balance = it.balance - val fmt = "${if (balance.credit_debit_indicator == CreditDebitInfo.debit) '-' else '+'}${balance.amount}" - assertEquals(amount, fmt, "For $account") - } -} - -/** Check [account] tan channel and info */ -suspend fun ApplicationTestBuilder.tanInfo(account: String): Pair<TanChannel?, String?> { - val res = client.getA("/accounts/$account").assertOkJson<AccountData>() - val channel: TanChannel? = res.tan_channel - return Pair(channel, when (channel) { - TanChannel.sms -> res.contact_data!!.phone.get() - TanChannel.email -> res.contact_data!!.email.get() - null -> null - }) -} - -/** Perform a bank transaction of [amount] [from] account [to] account with [subject} */ -suspend fun ApplicationTestBuilder.tx(from: String, amount: String, to: String, subject: String = "payout"): Long { - return client.postA("/accounts/$from/transactions") { - json { - "payto_uri" to "${paytos[to] ?: tmpPayTo}?message=${subject.encodeURLParameter()}&amount=$amount" - } - }.maybeChallenge().assertOkJson<TransactionCreateResponse>().row_id -} - -/** Perform a taler outgoing transaction of [amount] from exchange to merchant */ -suspend fun ApplicationTestBuilder.transfer(amount: String, payto: IbanPayto = merchantPayto, metadata: String? = null) { - client.postA("/accounts/exchange/taler-wire-gateway/transfer") { - json { - "request_uid" to HashCode.rand() - "amount" to TalerAmount(amount) - "exchange_base_url" to "http://exchange.example.com/" - "wtid" to ShortHashCode.rand() - "credit_account" to payto - "metadata" to metadata - } - }.assertOk() -} - -/** Perform a taler incoming transaction of [amount] from merchant to exchange */ -suspend fun ApplicationTestBuilder.addIncoming(amount: String) { - client.postA("/accounts/exchange/taler-wire-gateway/admin/add-incoming") { - json { - "amount" to TalerAmount(amount) - "reserve_pub" to EddsaPublicKey.randEdsaKey() - "debit_account" to merchantPayto - } - }.assertOk() -} - -/** Perform a taler kyc transaction of [amount] from merchant to exchange */ -suspend fun ApplicationTestBuilder.addKyc(amount: String) { - client.postA("/accounts/exchange/taler-wire-gateway/admin/add-kycauth") { - json { - "amount" to TalerAmount(amount) - "account_pub" to EddsaPublicKey.randEdsaKey() - "debit_account" to merchantPayto - } - }.assertOk() -} - -/** Perform a cashout operation of [amount] from customer */ -suspend fun ApplicationTestBuilder.cashout(amount: String) { - val res = client.postA("/accounts/customer/cashouts") { - json { - "request_uid" to ShortHashCode.rand() - "amount_debit" to amount - "amount_credit" to convert(amount) - } - } - if (res.status == HttpStatusCode.Conflict) { - // Retry with cashout info - fillCashoutInfo("customer") - client.postA("/accounts/customer/cashouts") { - json { - "request_uid" to ShortHashCode.rand() - "amount_debit" to amount - "amount_credit" to convert(amount) - } - } - } else { - res - }.assertOk() -} - -/** Perform a whithrawal operation of [amount] from customer */ -suspend fun ApplicationTestBuilder.withdrawal(amount: String) { - client.postA("/accounts/merchant/withdrawals") { - json { "amount" to amount } - }.assertOkJson<BankAccountCreateWithdrawalResponse> { - val uuid = it.taler_withdraw_uri.split("/").last() - withdrawalSelect(uuid) - client.postA("/accounts/merchant/withdrawals/${uuid}/confirm") - .assertNoContent() - } -} - -suspend fun ApplicationTestBuilder.fillCashoutInfo(account: String) { - client.patchAdmin("/accounts/$account") { - json { - "cashout_payto_uri" to unknownPayto - "contact_data" to obj { - "phone" to "+99" - } - } - }.assertNoContent() -} - -suspend fun ApplicationTestBuilder.fillTanInfo(username: String) { - // Create a token before we require 2fa for it - client.cachedToken(username) - client.patchAdmin("/accounts/$username") { - json { - "contact_data" to obj { - "phone" to "+${Random.nextInt(0, 10000)}" - } - "tan_channel" to "sms" - } - }.assertNoContent() -} - -suspend fun ApplicationTestBuilder.withdrawalSelect(uuid: String): EddsaPublicKey { - val reservePub = EddsaPublicKey.randEdsaKey() - client.post("/taler-integration/withdrawal-operation/$uuid") { - json { - "reserve_pub" to reservePub - "selected_exchange" to exchangePayto - } - }.assertOk() - return reservePub -} - -private var nbClass = 0; - -suspend fun ApplicationTestBuilder.createConversionRateClass( - cashout_min_amount: TalerAmount? = null -): Long { - nbClass += 1 - return client.postAdmin("/conversion-rate-classes") { - json { - "name" to "Gen class $nbClass" - "cashout_min_amount" to cashout_min_amount - } - }.assertOkJson<ConversionRateClassResponse>().conversion_rate_class_id -} - - -suspend fun ApplicationTestBuilder.convert(amount: String): TalerAmount { - return client.get("/conversion-info/cashout-rate?amount_debit=$amount") - .assertOkJson<ConversionResponse>().amount_credit -} - -fun tanCode(info: String): String? { - try { - val file = Path("/tmp/tan-$info.txt") - val code = file.readText().split(" ", limit=2).first() - file.deleteExisting() - return code - } catch (e: Exception) { - if (e is NoSuchFileException) return null - throw e - } -} - - -/* ----- Assert ----- */ - -suspend fun HttpResponse.maybeChallenge(): HttpResponse { - return if (this.status == HttpStatusCode.Accepted) { - this.assertChallenge() - } else { - this - } -} - -suspend fun HttpResponse.assertChallenge( - check: suspend (ChallengeResponse) -> Unit = {} -): HttpResponse { - val res = assertAcceptedJson<ChallengeResponse>() - val username = call.request.url.segments[1] - - val challenge = res.challenges.random() - - if (res.combi_and) { - for (challenge in res.challenges) { - call.client.postA("/accounts/$username/challenge/${challenge.challenge_id}").assertOk() - } - } else { - call.client.postA("/accounts/$username/challenge/${challenge.challenge_id}").assertOk() - } - - check(res) - - if (res.combi_and) { - for (challenge in res.challenges) { - val code = assertNotNull(tanCode(challenge.tan_info)) - call.client.postA("/accounts/$username/challenge/${challenge.challenge_id}/confirm") { - json { "tan" to code } - }.assertNoContent() - } - } else { - val code = assertNotNull(tanCode(challenge.tan_info)) - call.client.postA("/accounts/$username/challenge/${challenge.challenge_id}/confirm") { - json { "tan" to code } - }.assertNoContent() - } - - // Recover body from request - val requestBody = this.request.content - val ids = res.challenges.map { it.challenge_id }.joinToString(", ") - return call.client.request(this.call.request.url) { - tokenAuth(call.client, username) - method = call.request.method - headers[TALER_CHALLENGE_IDS] = ids - setBody(requestBody) - } -} - -fun assertException(msg: String, lambda: () -> Unit) { - try { - lambda() - throw Exception("Expected failure") - } catch (e: Exception) { - assert(e.message!!.startsWith(msg)) { "${e.message}" } - } -} - -/* ----- Random data generation ----- */ - -fun randBase32Crockford(length: Int) = Base32Crockford.encode(ByteArray(length).rand()) diff --git a/libeufin-bank/src/test/kotlin/routines.kt b/libeufin-bank/src/test/kotlin/routines.kt @@ -1,176 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2023, 2024, 2026 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/> - */ - -import io.ktor.client.request.* -import io.ktor.http.* -import io.ktor.server.testing.* -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.serialization.json.JsonObject -import tech.libeufin.bank.BankAccountCreateWithdrawalResponse -import tech.libeufin.bank.WithdrawalStatus -import tech.libeufin.common.* -import tech.libeufin.common.test.* -import kotlin.test.assertEquals - -// Test endpoint is correctly authenticated -suspend fun ApplicationTestBuilder.authRoutine( - method: HttpMethod, - path: String, - body: JsonObject? = null, - requireExchange: Boolean = false, - requireAdmin: Boolean = false, - allowAdmin: Boolean = false, - optional: Boolean = false -) { - // No body when authentication must happen before parsing the body - - if (!optional) { - // No header - client.request(path) { - this.method = method - if (body != null) json(body) - }.assertUnauthorized(TalerErrorCode.GENERIC_PARAMETER_MISSING) - } - - // Bad header - client.request(path) { - this.method = method - if (body != null) json(body) - headers[HttpHeaders.Authorization] = "WTF" - }.assertBadRequest(TalerErrorCode.GENERIC_HTTP_HEADERS_MALFORMED) - - if (requireAdmin) { - // Not an admin account - client.request(path) { - this.method = method - if (body != null) json(body) - tokenAuth(client, "merchant") - }.assertForbidden() - } else if (!allowAdmin) { - // Check no admin - client.request(path) { - this.method = method - if (body != null) json(body) - tokenAuth(client, "admin") - }.assertStatus(HttpStatusCode.Forbidden, null) - } - - if (requireExchange) { - // Not exchange account - client.request(path) { - this.method = method - if (body != null) json(body) - tokenAuth(client, if (requireAdmin) "admin" else "merchant") - }.assertConflict(TalerErrorCode.BANK_ACCOUNT_IS_NOT_EXCHANGE) - } -} - -suspend inline fun <reified B> ApplicationTestBuilder.historyRoutine( - url: String, - crossinline ids: (B) -> List<Long>, - registered: List<suspend () -> Unit>, - ignored: List<suspend () -> Unit> = listOf(), - polling: Boolean = true, - auth: String? = null -) { - abstractHistoryRoutine(ids, registered, ignored, polling) { params: String -> - client.getA("$url?$params") { - tokenAuth(client, auth) - } - } -} - -suspend inline fun <reified B> ApplicationTestBuilder.statusRoutine( - url: String, - crossinline status: (B) -> WithdrawalStatus -) { - val amount = TalerAmount("KUDOS:9.0") - client.postA("/accounts/customer/withdrawals") { - json { "amount" to amount } - }.assertOkJson<BankAccountCreateWithdrawalResponse> { resp -> - val aborted_uuid = resp.taler_withdraw_uri.split("/").last() - val confirmed_uuid = client.postA("/accounts/customer/withdrawals") { - json { "amount" to amount } - }.assertOkJson<BankAccountCreateWithdrawalResponse>() - .taler_withdraw_uri.split("/").last() - - // Check no useless polling - assertTime(0, 500) { - client.get("$url/$confirmed_uuid?timeout_ms=1000&old_state=selected") - .assertOkJson<B> { assertEquals(WithdrawalStatus.pending, status(it)) } - } - - // Polling selected - coroutineScope { - launch { // Check polling succeed - assertTime(100, 500) { - client.get("$url/$confirmed_uuid?timeout_ms=1000") - .assertOkJson<B> { assertEquals(WithdrawalStatus.selected, status(it)) } - } - } - launch { // Check polling succeed - assertTime(100, 500) { - client.get("$url/$aborted_uuid?timeout_ms=1000") - .assertOkJson<B> { assertEquals(WithdrawalStatus.selected, status(it)) } - } - } - delay(100) - withdrawalSelect(confirmed_uuid) - withdrawalSelect(aborted_uuid) - } - - // Polling confirmed - coroutineScope { - launch { // Check polling succeed - assertTime(100, 500) { - client.get("$url/$confirmed_uuid?timeout_ms=1000&old_state=selected") - .assertOkJson<B> { assertEquals(WithdrawalStatus.confirmed, status(it))} - } - } - launch { // Check polling timeout - assertTime(200, 500) { - client.get("$url/$aborted_uuid?timeout_ms=200&old_state=selected") - .assertOkJson<B> { assertEquals(WithdrawalStatus.selected, status(it)) } - } - } - delay(100) - client.postA("/accounts/customer/withdrawals/$confirmed_uuid/confirm").assertNoContent() - } - - // Polling abort - coroutineScope { - launch { - assertTime(200, 500) { - client.get("$url/$confirmed_uuid?timeout_ms=200&old_state=confirmed") - .assertOkJson<B> { assertEquals(WithdrawalStatus.confirmed, status(it))} - } - } - launch { - assertTime(100, 500) { - client.get("$url/$aborted_uuid?timeout_ms=1000&old_state=selected") - .assertOkJson<B> { assertEquals(WithdrawalStatus.aborted, status(it)) } - } - } - delay(100) - client.post("/taler-integration/withdrawal-operation/$aborted_uuid/abort").assertNoContent() - } - } -} -\ No newline at end of file diff --git a/libeufin-common/build.gradle b/libeufin-common/build.gradle @@ -1,84 +0,0 @@ -plugins { - id("kotlin") - id("org.jetbrains.kotlin.plugin.serialization") version "$kotlin_version" -} - -version = rootProject.version - -java { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 -} - -compileKotlin.kotlinOptions.jvmTarget = "17" -compileTestKotlin.kotlinOptions.jvmTarget = "17" - -task versionConstant { - def outputDir = file("$buildDir/generated/constants") - def outputFile = new File(outputDir, "CompileConstants.kt") - def programVersion = rootProject.version.toString() - - inputs.property 'programVersion', programVersion - outputs.dir outputDir - - doLast { - // Ensure output directory exists - outputDir.mkdirs() - - // Generate the Kotlin constants file - def versionLiteral = groovy.json.JsonOutput.toJson(programVersion).replace('$', '\\$') - outputFile.setText(""" - package tech.libeufin.common - - val VERSION: String = ${versionLiteral} - """.stripIndent(), 'UTF-8') - } -} - -dokkaGenerateModuleHtml { - dependsOn versionConstant -} - -clean { - delete "$buildDir/generated" -} - - -sourceSets.main.java.srcDirs = ["src/main/kotlin", "$buildDir/generated/constants"] - - -compileKotlin { - dependsOn versionConstant -} - -dependencies { - implementation("org.slf4j:slf4j-api:2.0.18") - // Crypto - implementation("org.bouncycastle:bcprov-jdk18on:1.84") - implementation("org.bouncycastle:bcpkix-jdk18on:1.84") - // Database helper - implementation("org.postgresql:postgresql:$postgres_version") - implementation("com.zaxxer:HikariCP:7.0.2") - - implementation("io.ktor:ktor-server-core:$ktor_version") - implementation("io.ktor:ktor-server-call-logging:$ktor_version") - implementation("io.ktor:ktor-server-content-negotiation:$ktor_version") - implementation("io.ktor:ktor-server-status-pages:$ktor_version") - implementation("io.ktor:ktor-server-cio:$ktor_version") - implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor_version") - implementation("io.ktor:ktor-server-forwarded-header:$ktor_version") - implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor_version") - implementation("io.ktor:ktor-server-test-host:$ktor_version") - implementation("io.ktor:ktor-server-call-id:$ktor_version") - - // OpenAPI spec generation - implementation("io.github.smiley4:ktor-openapi:5.6.0") - implementation("io.github.smiley4:schema-kenerator-core:2.6.0") - implementation("io.github.smiley4:schema-kenerator-serialization:2.6.0") - implementation("io.github.smiley4:schema-kenerator-swagger:2.6.0") - - implementation("com.github.ajalt.clikt:clikt:$clikt_version") - - implementation("org.jetbrains.kotlin:kotlin-test:$kotlin_version") - testImplementation("uk.org.webcompere:system-stubs-core:2.1.8") -} diff --git a/libeufin-common/src/main/kotlin/AnsiColor.kt b/libeufin-common/src/main/kotlin/AnsiColor.kt @@ -1,75 +0,0 @@ -/* - * 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.common - -object ANSI { - private val ANSI_PATTERN = Regex("\\u001B\\[[;\\d]*m") - - enum class Color(val code: Int) { - BLACK(0), - RED(1), - GREEN(2), - YELLOW(3), - BLUE(4), - MAGENTA(5), - CYAN(6), - WHITE(7) - } - - /** Compute [msg] length without ANSI escape sequences */ - fun CharSequence.displayLength(): Int = - splitToSequence(ANSI_PATTERN).sumOf { it.length } - - /** Format a [msg] using optionals [fg] and [bg] colors and optionally make the text [bold] */ - fun fmt(msg: String, fg: Color? = null, bg: Color? = null, bold: Boolean = false): String { - if (fg == null && bg == null && !bold) return msg - return buildString { - fun next() { - if (last() != '[') { - append(';') - } - } - - append("\u001b[") - if (bold) { - append('1') - } - if (fg != null) { - next() - append('3') - append(fg.code.toString()) - } - if (bg != null) { - next() - append('4') - append(bg.code.toString()) - } - append('m') - append(msg) - append("\u001b[0m") - } - } - - fun red(msg: String) = fmt(msg, Color.RED) - fun green(msg: String) = fmt(msg, Color.GREEN) - fun yellow(msg: String) = fmt(msg, Color.YELLOW) - fun magenta(msg: String) = fmt(msg, Color.MAGENTA) - fun bold(msg: String) = fmt(msg, bold = true) -} -\ No newline at end of file diff --git a/libeufin-common/src/main/kotlin/ApiError.kt b/libeufin-common/src/main/kotlin/ApiError.kt @@ -1,161 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2024-2025 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.common - -import io.ktor.http.* -import io.ktor.server.application.* -import io.ktor.server.response.* -import io.ktor.util.* -import kotlinx.serialization.Serializable - -/** - * Convenience type to throw errors along the API activity - * and that is meant to be caught by Ktor and responded to the - * client. - */ -class ApiException( - // Status code that Ktor will set for the response. - val httpStatus: HttpStatusCode, - // Error detail object, after Taler API. - val talerError: TalerError -) : Exception(talerError.hint) - -/** - * Error object to respond to the client. The - * 'code' field takes values from the GANA gnu-taler-error-code - * specification. 'hint' is a human-readable description - * of the error. - */ -@Serializable -data class TalerError( - @kotlinx.serialization.Transient val err: TalerErrorCode = TalerErrorCode.END, - val code: Int, - val hint: String? = null, - val detail: String? = null -) - -private val LOG_MSG = AttributeKey<String>("log_msg") - -fun ApplicationCall.logMsg(): String? = attributes.getOrNull(LOG_MSG) - -suspend fun ApplicationCall.err( - status: HttpStatusCode, - hint: String?, - error: TalerErrorCode, - cause: Exception? -) { - err( - ApiException( - httpStatus = status, talerError = TalerError( - code = error.code, err = error, hint = hint - ) - ), - cause - ) -} - -suspend fun ApplicationCall.err( - err: ApiException, - cause: Exception? -) { - val fmt = buildString { - append(err.talerError.err.name) - append(" ") - append(err.talerError.hint) - val msg = cause?.message - if (msg != null) { - append("- ") - append(msg) - } - } - attributes.put(LOG_MSG, fmt) - respond( - status = err.httpStatus, - message = err.talerError - ) -} - - -fun apiError( - status: HttpStatusCode, - hint: String?, - error: TalerErrorCode, - detail: String? = null -): ApiException = ApiException( - httpStatus = status, talerError = TalerError( - code = error.code, err = error, hint = hint, detail = detail - ) -) - -/* ----- HTTP error ----- */ - -fun forbidden( - hint: String, - error: TalerErrorCode = TalerErrorCode.GENERIC_FORBIDDEN -): ApiException = apiError(HttpStatusCode.Forbidden, hint, error) - -fun unauthorized( - hint: String, - error: TalerErrorCode = TalerErrorCode.GENERIC_UNAUTHORIZED -): ApiException = apiError(HttpStatusCode.Unauthorized, hint, error) - -fun internalServerError(hint: String?): ApiException - = apiError(HttpStatusCode.InternalServerError, hint, TalerErrorCode.GENERIC_INTERNAL_INVARIANT_FAILURE) - -fun paramsMalformed(hint: String): ApiException - = badRequest(hint, TalerErrorCode.GENERIC_PARAMETER_MALFORMED) - -fun notFound( - hint: String, - error: TalerErrorCode -): ApiException = apiError(HttpStatusCode.NotFound, hint, error) - -fun conflict( - hint: String, error: TalerErrorCode -): ApiException = apiError(HttpStatusCode.Conflict, hint, error) - -fun tooManyRequests( - hint: String, error: TalerErrorCode -): ApiException = apiError(HttpStatusCode.TooManyRequests, hint, error) - -fun badRequest( - hint: String? = null, - error: TalerErrorCode = TalerErrorCode.GENERIC_JSON_INVALID, - detail: String? = null -): ApiException = apiError(HttpStatusCode.BadRequest, hint, error, detail) - -fun unsupportedMediaType( - hint: String, - error: TalerErrorCode = TalerErrorCode.END, -): ApiException = apiError(HttpStatusCode.UnsupportedMediaType, hint, error) - -fun notImplemented( - hint: String = "API not implemented", - error: TalerErrorCode = TalerErrorCode.END, -): ApiException = apiError(HttpStatusCode.NotImplemented, hint, error) - -fun bodyOverflow( - hint: String, - error: TalerErrorCode = TalerErrorCode.GENERIC_UPLOAD_EXCEEDS_LIMIT, -): ApiException = apiError(HttpStatusCode.PayloadTooLarge, hint, error) - -fun badGateway( - hint: String, - error: TalerErrorCode = TalerErrorCode.GENERIC_UPLOAD_EXCEEDS_LIMIT, -): ApiException = apiError(HttpStatusCode.PayloadTooLarge, hint, error) -\ No newline at end of file diff --git a/libeufin-common/src/main/kotlin/Backoff.kt b/libeufin-common/src/main/kotlin/Backoff.kt @@ -1,41 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2023-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.common - -import kotlin.random.Random - -/** Infinite exponential backoff with decorrelated jitter */ -class ExpoBackoffDecorr( - private val base: Long = 100, // 0.1 second - private val max: Long = 60000, // 60 seconds - private val factor: Double = 2.0, -) { - private var sleep: Long = base - - fun next() : Long { - sleep = Random.nextDouble(base.toDouble(), sleep.toDouble() * factor) - .toLong().coerceAtMost(max) - return sleep - } - - fun reset() { - sleep = base - } -} -\ No newline at end of file diff --git a/libeufin-common/src/main/kotlin/Cli.kt b/libeufin-common/src/main/kotlin/Cli.kt @@ -1,124 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2023-2025 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.common - -import com.github.ajalt.clikt.core.CliktCommand -import com.github.ajalt.clikt.core.Context -import com.github.ajalt.clikt.core.ProgramResult -import com.github.ajalt.clikt.core.subcommands -import com.github.ajalt.clikt.parameters.arguments.argument -import com.github.ajalt.clikt.parameters.groups.OptionGroup -import com.github.ajalt.clikt.parameters.groups.provideDelegate -import com.github.ajalt.clikt.parameters.options.default -import com.github.ajalt.clikt.parameters.options.flag -import com.github.ajalt.clikt.parameters.options.option -import com.github.ajalt.clikt.parameters.types.enum -import com.github.ajalt.clikt.parameters.types.path -import kotlinx.coroutines.runBlocking -import org.slf4j.Logger -import org.slf4j.LoggerFactory -import org.slf4j.event.Level - -private val logger: Logger = LoggerFactory.getLogger("libeufin-config") - -abstract class TalerCmd(name: String? = null): CliktCommand(name) { - val config by option( - "--config", "-c", - help = "Specifies the configuration file", - metavar = "config_file" - ).path() - val log by option( - "--log", "-L", - help = "Configure logging to use LOGLEVEL" - ).enum<Level>().default(Level.INFO) - - fun cliCmd(logger: Logger, lambda: suspend () -> Unit) { - // Set log level - TalerServiceProvider.currentLevel = log - // Run cli command catching all errors - try { - runBlocking { - lambda() - } - } catch (e: ProgramResult) { - throw e - } catch (e: TalerConfigError) { - e.fmtLog(logger) - throw ProgramResult(6) - } catch (e: Throwable) { - e.fmtLog(logger) - throw ProgramResult(1) - } - } -} - -class CliConfigCmd(configSource: ConfigSource) : TalerCmd("config") { - init { - subcommands(CliConfigGet(configSource), CliConfigDump(configSource), CliConfigPathsub(configSource)) - } - - override fun help(context: Context) = "Inspect or change the configuration" - - override fun run() = Unit -} - -private class CliConfigGet(private val configSource: ConfigSource) : TalerCmd("get") { - override fun help(context: Context) = "Lookup config value" - - private val isPath by option( - "--filename", "-f", - help = "Interpret value as path with dollar-expansion" - ).flag() - private val section by argument() - private val option by argument() - - override fun run() = cliCmd(logger) { - val config = configSource.fromFile(config) - val sect = config.section(section) - if (isPath) { - println(sect.path(option).require()) - } else { - println(sect.string(option).require()) - } - } -} - - - -private class CliConfigPathsub(private val configSource: ConfigSource) : TalerCmd("pathsub") { - override fun help(context: Context) = "Substitute variables in a path" - - private val pathExpr by argument() - - override fun run() = cliCmd(logger) { - val config = configSource.fromFile(config) - println(config.pathsub(pathExpr)) - } -} - -private class CliConfigDump(private val configSource: ConfigSource) : TalerCmd("dump") { - override fun help(context: Context) = "Dump the configuration" - - override fun run() = cliCmd(logger) { - val config = configSource.fromFile(config) - println("# install path: ${configSource.installPath()}") - println(config.stringify()) - } -} diff --git a/libeufin-common/src/main/kotlin/Constants.kt b/libeufin-common/src/main/kotlin/Constants.kt @@ -1,41 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2024, 2025, 2026 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.common - -// DB -const val MIN_VERSION: Int = 14 -const val SERIALIZATION_RETRY: Int = 30 - -// Security -const val MAX_BODY_LENGTH: Int = 4 * 1024 // 4kB - -// API version -const val WIRE_GATEWAY_API_VERSION: String = "5:0:0" -const val WIRE_TRANSFER_API_VERSION: String = "0:0:0" -const val REVENUE_API_VERSION: String = "1:1:1" -const val OBSERVABILITY_API_VERSION: String = "0:0:0" - -// HTTP headers -const val TALER_CHALLENGE_IDS: String = "Taler-Challenge-Ids" -const val X_FORWARD_PREFIX: String = "X-Forward-Prefix" - -// Params -const val MAX_PAGE_SIZE: Int = 1024 -const val MAX_TIMEOUT_MS: Long = 60 * 60 * 1000 // 1h -// TODO make MAX_TIMEOUT_MS configurable -\ No newline at end of file diff --git a/libeufin-common/src/main/kotlin/Encoding.kt b/libeufin-common/src/main/kotlin/Encoding.kt @@ -1,105 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2024-2025 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.common - -/** Crockford's Base32 implementation */ -object Base32Crockford { - /** Crockford's Base32 alphabet */ - const val ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ" - /** Base32 mark to extract 5 bits chunks */ - private const val MASK = 0b11111 - /** Crockford's Base32 inversed alphabet */ - private val INV = intArrayOf( - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1, -1, 10, 11, 12, - 13, 14, 15, 16, 17, 1, 18, 19, 1, 20, 21, 0, 22, 23, 24, 25, 26, 27, 27, 28, - 29, 30, 31, -1, -1, -1, -1, -1, -1, 10, 11, 12, 13, 14, 15, 16, 17, 1, 18, 19, - 1, 20, 21, 0, 22, 23, 24, 25, 26, 27, 27, 28, 29, 30, 31, - ) - - fun encode(data: ByteArray): String = buildString(encodedSize(data.size)) { - var buffer = 0 - var bitsLeft = 0 - - for (byte in data) { - // Read input - buffer = (buffer shl 8) or (byte.toInt() and 0xFF) - bitsLeft += 8 - // Write symbols - while (bitsLeft >= 5) { - append(ALPHABET[(buffer shr (bitsLeft - 5)) and MASK]) - bitsLeft -= 5 - } - } - - if (bitsLeft > 0) { - // Write remaining bits - append(ALPHABET[(buffer shl (5 - bitsLeft)) and MASK]) - } - } - - fun decode(encoded: String): ByteArray { - val out = ByteArray(decodedSize(encoded.length)) - - var bitsLeft = 0 - var buffer = 0 - var cursor = 0 - - for (char in encoded) { - // Read input - val index = char - '0' - require(index in 0..INV.size) { "invalid Base32 character: $char" } - val decoded = INV[index] - require(decoded != -1) { "invalid Base32 character: $char" } - buffer = (buffer shl 5) or decoded - bitsLeft += 5 - // Write bytes - if (bitsLeft >= 8) { - out[cursor++] = (buffer shr (bitsLeft - 8)).toByte() - bitsLeft -= 8 // decrease of written bits. - } - } - - return out - } - - /** - * Compute the length of the resulting string when encoding data of the given size - * in bytes. - * - * @param dataSize size of the data to encode in bytes - * @return size of the string that would result from encoding - */ - fun encodedSize(dataSize: Int): Int { - return (dataSize * 8 + 4) / 5 - } - - /** - * Compute the length of the resulting data in bytes when decoding a (valid) string of the - * given size. - * - * @param stringSize size of the string to decode - * @return size of the resulting data in bytes - */ - fun decodedSize(stringSize: Int): Int { - return (stringSize * 5) / 8 - } -} - - -\ No newline at end of file diff --git a/libeufin-common/src/main/kotlin/Subject.kt b/libeufin-common/src/main/kotlin/Subject.kt @@ -1,270 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2024, 2025, 2026 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.common - -import org.bouncycastle.math.ec.rfc8032.Ed25519 -import java.math.BigInteger -import java.security.MessageDigest - -sealed interface IncomingSubject { - data class Reserve(val reserve_pub: EddsaPublicKey): IncomingSubject - data class Kyc(val account_pub: EddsaPublicKey): IncomingSubject - data class Map(val auth_pub: EddsaPublicKey): IncomingSubject - data object AdminBalanceAdjust: IncomingSubject - - val type: IncomingType get() = when (this) { - is Reserve -> IncomingType.reserve - is Kyc -> IncomingType.kyc - is Map -> IncomingType.map - AdminBalanceAdjust -> throw IllegalStateException("Admin balance adjust") - } - val key: EddsaPublicKey get() = when (this) { - is Reserve -> this.reserve_pub - is Kyc -> this.account_pub - is Map -> this.auth_pub - AdminBalanceAdjust -> throw IllegalStateException("Admin balance adjust") - } -} - -/** Base32 quality by proximity to spec and error probability */ -private enum class Base32Quality { - /// Both mixed casing and mixed characters, that's weird - Mixed, - /// Standard but use lowercase, maybe the client shown lowercase in the UI - Standard, - /// Uppercase but mixed characters, its common when making typos - Upper, - /// Both uppercase and use the standard alphabet as it should - UpperStandard; - - companion object { - fun measure(s: String): Base32Quality { - var uppercase = true; - var standard = true; - for (char in s) { - uppercase = uppercase && char.isUpperCase() - standard = standard && Base32Crockford.ALPHABET.contains(char) - } - return if (uppercase && standard) { - Base32Quality.UpperStandard - } else if (uppercase && !standard) { - Base32Quality.Upper - } else if (!uppercase && standard) { - Base32Quality.Standard - } else { - Base32Quality.Mixed - } - } - } -} - -private data class Candidate(val subject: IncomingSubject, val quality: Base32Quality) - -private const val ADMIN_BALANCE_ADJUST = "ADMINBALANCEADJUST" -private const val KEY_SIZE = 52; -private const val PREFIX_SIZE = KEY_SIZE + 3; -private val ALPHA_NUMBERIC_PATTERN = Regex("[0-9a-zA-Z]*") - -/** - * Extract the public key from an unstructured incoming transfer subject. - * - * When a user enters the transfer object in an unstructured way, for ex in - * their banking UI, they may mistakenly enter separators such as ' \n-+' and - * make typos. - * To parse them while ignoring user errors, we reconstruct valid keys from key - * parts, resolving ambiguities where possible. - **/ -fun parseIncomingSubject(subject: String?): IncomingSubject { - if (subject == null || subject.isEmpty()) { - throw Exception("missing subject") - } - - /** Parse an incoming subject */ - fun parseSingle(str: String): Candidate? { - // Check key type - val (type, raw) = when (str.length) { - ADMIN_BALANCE_ADJUST.length -> if (str.equals(ADMIN_BALANCE_ADJUST, ignoreCase = true)) { - return Candidate(IncomingSubject.AdminBalanceAdjust, Base32Quality.UpperStandard) - } else { - return null - } - KEY_SIZE -> Pair(IncomingType.reserve, str) - PREFIX_SIZE -> if (str.startsWith("KYC")) { - Pair(IncomingType.kyc, str.substring(3)) - } else if (str.startsWith("MAP")) { - Pair(IncomingType.map, str.substring(3)) - } else { - return null - } - else -> return null - } - - // Check key validity - val key = try { - EddsaPublicKey(raw) - } catch (e: Exception) { - return null - } - if (!Ed25519.validatePublicKeyFull(key.raw, 0)) { - return null - } - - val quality = Base32Quality.measure(raw); - - val subject = when (type) { - IncomingType.map -> IncomingSubject.Map(key) - IncomingType.kyc -> IncomingSubject.Kyc(key) - IncomingType.reserve -> IncomingSubject.Reserve(key) - } - return Candidate(subject, quality) - } - - // Find and concatenate valid parts of a keys - val parts = mutableListOf(0) - val concatenated = StringBuilder() - for (match in ALPHA_NUMBERIC_PATTERN.findAll(subject.replace("%20", " "))) { - concatenated.append(match.value); - parts.add(concatenated.length); - } - - // Find best candidates - var best: Candidate? = null - // For each part as a starting point - for ((i, start) in parts.withIndex()) { - // Use progressively longer concatenation - for (end in parts.subList(i, parts.size)) { - val range = start until end - // Until they are to long to be a key - if (range.count() > PREFIX_SIZE) { - break; - } - // Parse the concatenated parts - val slice = concatenated.substring(range) - parseSingle(slice)?.let { other -> - if (best != null) { - if (best.subject is IncomingSubject.AdminBalanceAdjust) { - if (other.subject !is IncomingSubject.AdminBalanceAdjust) { - throw Exception("found multiple subject kind") - } - } else if (other.quality > best.quality // We prefer high quality keys - || ( // We prefer prefixed keys over reserve keys - best.subject.type == IncomingType.reserve && - (other.subject.type == IncomingType.kyc || other.subject.type == IncomingType.map) - )) - { - best = other - } else if (best.subject.key != other.subject.key // If keys are different - && best.quality == other.quality // Of same quality - && !( // And prefixing is different - (best.subject.type == IncomingType.kyc || best.subject.type == IncomingType.map) && - other.subject.type == IncomingType.reserve - )) - { - throw Exception("found multiple reserve public key") - } - } else { - best = other - } - } - } - } - - return best?.subject ?: throw Exception("missing reserve public key") -} - -/** Extract the reserve public key from an incoming Taler transaction subject */ -fun parseOutgoingSubject(subject: String): Triple<ShortHashCode, BaseURL, String?> { - var iterator = subject.splitToSequence(' ').iterator(); - val first = iterator.next() - if (!iterator.hasNext()) throw Exception("malformed outgoing subject") - val second = iterator.next() - if (iterator.hasNext()) { - val third = iterator.next() - return Triple(EddsaPublicKey(second), BaseURL.parse(third), first) - } else { - return Triple(EddsaPublicKey(first), BaseURL.parse(second), null) - } -} - -/** Format an outgoing subject */ -fun fmtOutgoingSubject(wtid: ShortHashCode, url: BaseURL, metadata: String? = null): String = buildString { - if (metadata != null) { - append(metadata) - append(" ") - } - append(wtid) - append(" ") - append(url) -} - -/** Format an incoming subject */ -fun fmtIncomingSubject(type: IncomingType, key: EddsaPublicKey): String = buildString { - append("Taler ") - when (type) { - IncomingType.kyc -> append("KYC:") - IncomingType.map -> append("MAP:") - IncomingType.reserve -> Unit - } - append(key) -} - -/** Encode a public key as a QR-Bill reference */ -fun subjectFmtQrBill(key: EddsaPublicKey): String { - // High-Entropy Hash (SHA-256) to ensure even distribution - val digest = MessageDigest.getInstance("SHA-256") - val hashInt = BigInteger(1, digest.digest(key.raw)) - - // Modulo 10^26 to fit the Swiss QR data field - val divisor = BigInteger.TEN.pow(26) - val referenceBase = hashInt.remainder(divisor).toString().padStart(26, '0') - - // Modulo 10 Recursive calculation - val lookupTable = intArrayOf(0, 9, 4, 6, 8, 2, 7, 1, 3, 5) - var carry = 0 - - for (char in referenceBase) { - val digit = char.digitToInt() - carry = lookupTable[(carry + digit) % 10] - } - - val checksum = (10 - carry) % 10 - - return referenceBase + checksum -} - -/** Check if a subject is a valid QR-Bill reference */ -fun subjectIsQrBill(reference: String): Boolean { - // Quick length and numeric check - if (reference.length != 27 || !reference.all { it.isDigit() }) { - return false - } - - // Modulo 10 Recursive check - val lookupTable = intArrayOf(0, 9, 4, 6, 8, 2, 7, 1, 3, 5) - var carry = 0 - - for (char in reference) { - val digit = char.digitToInt() - carry = lookupTable[(carry + digit) % 10] - } - - // If the check digit was correct, the final carry will be 0 - return carry == 0 -} -\ No newline at end of file diff --git a/libeufin-common/src/main/kotlin/Table.kt b/libeufin-common/src/main/kotlin/Table.kt @@ -1,86 +0,0 @@ -/* - * 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.common - -import tech.libeufin.common.ANSI.displayLength -import kotlin.math.max - -data class ColumnStyle( - val alignLeft: Boolean = true -) { - companion object { - val DEFAULT = ColumnStyle() - } -} - -fun printTable( - columns: List<String>, - rows: List<List<String>>, - separator: Char = '|', - colStyle: List<ColumnStyle> = listOf() -) { - val cols: List<Pair<String, Int>> = columns.mapIndexed { i, name -> - val maxRow: Int = rows.asSequence().map { - it[i].displayLength() - }.maxOrNull() ?: 0 - Pair(name, max(name.displayLength(), maxRow)) - } - val table = buildString { - fun padding(length: Int) { - repeat(length) { append (' ') } - } - var first = true - for ((name, len) in cols) { - if (!first) { - append(separator) - } else { - first = false - } - val pad = len - name.displayLength() - padding(pad / 2) - append(name) - padding(pad / 2 + if (pad % 2 == 0) { 0 } else { 1 }) - } - append('\n') - for (row in rows) { - var first = true - cols.forEachIndexed { i, met -> - val str = row[i] - val style = colStyle.getOrNull(i) ?: ColumnStyle.DEFAULT - if (!first) { - append(separator) - } else { - first = false - } - val (_, len) = met - val pad = len - str.displayLength() - if (style.alignLeft) { - append(str) - padding(pad) - } else { - padding(pad) - append(str) - } - } - append('\n') - } - } - print(table) -} -\ No newline at end of file diff --git a/libeufin-common/src/main/kotlin/TalerConfig.kt b/libeufin-common/src/main/kotlin/TalerConfig.kt @@ -1,600 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2023, 2024, 2025, 2026 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.common - -import kotlinx.serialization.json.Json -import org.slf4j.Logger -import org.slf4j.LoggerFactory -import java.nio.file.* -import java.time.* -import java.time.format.* -import java.time.temporal.ChronoUnit -import kotlin.io.path.* - -private val logger: Logger = LoggerFactory.getLogger("libeufin-config") - -/** Config error when analyzing and using the taler configuration format */ -class TalerConfigError private constructor (m: String, cause: Throwable? = null) : Exception(m, cause) { - companion object { - /** Error when a specific option value is missing */ - internal fun missing(type: String, section: String, option: String): TalerConfigError = - TalerConfigError("Missing $type option '$option' in section '$section'") - - /** Error when a specific option value is invalid */ - internal fun invalid(type: String, section: String, option: String, err: String): TalerConfigError = - TalerConfigError("Expected $type option '$option' in section '$section': $err") - - /** Generic error not linked to a specific option value */ - fun generic(msg: String, cause: Throwable? = null): TalerConfigError = - TalerConfigError(msg, cause) - } -} - -/** Configuration error when converting option value */ -class ValueError(val msg: String): Exception(msg) - -/** Information about how the configuration is loaded */ -data class ConfigSource( - /** Name of the high-level project */ - val projectName: String = "taler", - /** Name of the component within the package */ - val componentName: String = "taler", - /** - * Executable name that will be located on $PATH to - * find the installation path of the package - */ - val execName: String = "taler-config" -) { - /** Load configuration from string */ - fun fromMem(content: String): TalerConfig { - val loader = ConfigLoader(this) - loader.loadDefaults() - loader.loadFromMem(content.lineSequence(), null, 0) - return loader.finalize() - } - - /** - * Load configuration from [file], if [file] is null load from default configuration file - * - * The entry point for the default configuration will be the first file from this list: - * - $XDG_CONFIG_HOME/$componentName.conf - * - $HOME/.config/$componentName.conf - * - /etc/$componentName.conf - * - /etc/$projectName/$componentName.conf - * */ - fun fromFile(file: Path?): TalerConfig { - /** Search for the default configuration file path */ - fun defaultConfigPath(): Path? { - return sequence { - val xdg = System.getenv("XDG_CONFIG_HOME") - if (xdg != null) yield(Path(xdg, "$componentName.conf")) - - val home = System.getenv("HOME") - if (home != null) yield(Path(home, ".config/$componentName.conf")) - - yield(Path("/etc/$componentName.conf")) - yield(Path("/etc/$projectName/$componentName.conf")) - }.firstOrNull { it.exists() } - } - val path = file ?: defaultConfigPath() - val loader = ConfigLoader(this) - loader.loadDefaults() - if (path != null) loader.loadFromFile(path, 0, 0) - return loader.finalize() - } - - /** Search for the binary installation path in PATH */ - fun installPath(): Path { - val pathEnv = System.getenv("PATH") - for (entry in pathEnv.splitToSequence(':')) { - val path = Path(entry) - if (path.resolve(execName).exists()) { - val parent = path.parent - if (parent != null) { - return parent.toRealPath() - } - } - } - return Path("/usr") - } -} - -/** - * Reader for Taler-style configuration files - * - * The configuration file format is similar to INI files - * and fully described in the taler.conf man page. - * - * @param source information about where to load configuration defaults from - **/ -private class ConfigLoader( - private val source: ConfigSource -) { - private val sections: MutableMap<String, MutableMap<String, String>> = mutableMapOf() - - /** - * Load configuration defaults from the file system - * and populate the PATHS section based on the installation path. - */ - fun loadDefaults() { - val installDir = source.installPath() - - val section = sections.getOrPut("PATHS") { mutableMapOf() } - section["PREFIX"] = "$installDir/" - section["BINDIR"] = "$installDir/bin/" - section["LIBEXECDIR"] = "$installDir/${source.projectName}/libexec/" - section["DOCDIR"] = "$installDir/share/doc/${source.projectName}/" - section["ICONDIR"] = "$installDir/share/icons/" - section["LOCALEDIR"] = "$installDir/share/locale/" - section["LIBDIR"] = "$installDir/lib/${source.projectName}/" - section["DATADIR"] = "$installDir/share/${source.projectName}/" - - val baseConfigDir = installDir.resolve("share/${source.projectName}/config.d") - try { - baseConfigDir.useDirectoryEntries { - for (entry in it) { - loadFromFile(entry, 0, 0) - } - } - } catch (e: Exception) { - when (e) { - is NotDirectoryException -> - logger.warn("Base config directory is not a directory") - is NoSuchFileException -> - logger.warn("Missing base config directory: $baseConfigDir") - else -> throw e - } - } - } - - fun genericError(source: Path?, lineNum: Int, msg: String, cause: String? = null): TalerConfigError { - val message = buildString { - append(msg) - append(" at '") - if (source != null) { - append(source) - } else { - append("mem") - } - append(':') - append(lineNum) - append('\'') - if (cause != null) { - append(": ") - append(cause) - } - } - return TalerConfigError.generic(message) - } - - fun loadFromFile(file: Path, recursionDepth: Int, lineNum: Int) { - if (recursionDepth > 128) { - throw genericError(file, lineNum, "Recursion limit in config inlining") - } - logger.trace("load file at $file") - return try { - file.useLines { - loadFromMem(it, file, recursionDepth+1) - } - } catch (e: Exception) { - when (e) { - is NoSuchFileException -> throw TalerConfigError.generic("Could not read config at '$file': no such file") - is AccessDeniedException -> throw TalerConfigError.generic("Could not read config at '$file': permission denied") - is TalerConfigError -> throw e - else -> throw TalerConfigError.generic("Could not read config at '$file'", e) - } - } - } - - fun loadFromMem(lines: Sequence<String>, source: Path?, recursionDepth: Int) { - var currentSection: MutableMap<String, String>? = null - for ((lineNum, line) in lines.withIndex()) { - if (RE_LINE_OR_COMMENT.matches(line)) { - continue - } - - val directiveMatch = RE_DIRECTIVE.matchEntire(line) - if (directiveMatch != null) { - if (source == null) throw genericError(source, lineNum, "Directives are only supported when loading from file") - val (directiveName, directiveArg) = directiveMatch.destructured - when (directiveName.lowercase()) { - "inline" -> loadFromFile(source.resolveSibling(directiveArg), recursionDepth, lineNum) - "inline-matching" -> { - try { - val pathMatcher = FileSystems.getDefault().getPathMatcher("glob:$directiveArg") - val entries = source.parent.walk() - .filter { pathMatcher.matches(source.parent.relativize(it)) } - for (entry in entries) { - loadFromFile(entry, recursionDepth, lineNum) - } - } catch (e: Exception) { - when (e) { - is java.util.regex.PatternSyntaxException -> - throw genericError(source, lineNum, "Malformed glob regex", e.message) - else -> throw e - } - } - } - "inline-secret" -> { - val sp = directiveArg.split(" ") - if (sp.size != 2) { - throw genericError(source, lineNum, "invalid configuration, @inline-secret@ directive requires exactly two arguments") - } - val sectionName = sp[0] - val secretFilename = source.resolveSibling(sp[1]) - - if (!secretFilename.isReadable()) { - logger.warn("unable to read secrets from $secretFilename") - } else { - loadFromFile(secretFilename, recursionDepth, lineNum) - } - } - else -> throw genericError(source, lineNum, "unsupported directive '$directiveName'") - } - continue - } - - val secMatch = RE_SECTION.matchEntire(line) - if (secMatch != null) { - val (sectionName) = secMatch.destructured - currentSection = sections.getOrPut(sectionName.uppercase()) { mutableMapOf() } - continue - } else if (currentSection == null) { - throw genericError(source, lineNum, "expected section header") - } - - val paramMatch = RE_PARAM.matchEntire(line) - if (paramMatch != null) { - var (optName, optVal) = paramMatch.destructured - if (optVal.length != 1 && optVal.startsWith('"') && optVal.endsWith('"')) { - optVal = optVal.substring(1, optVal.length - 1) - } - currentSection[optName.uppercase()] = optVal - continue - } - throw genericError(source, lineNum, "expected section header, option assignment or directive") - } - } - - fun finalize(): TalerConfig { - return TalerConfig(sections) - } - - companion object { - private val RE_LINE_OR_COMMENT = Regex("^\\s*(#.*)?$") - private val RE_SECTION = Regex("^\\s*\\[\\s*(.*)\\s*\\]\\s*$") - private val RE_PARAM = Regex("^\\s*([^=]+?)\\s*=\\s*(.*?)\\s*$") - private val RE_DIRECTIVE = Regex("^\\s*@([a-zA-Z-_]+)@\\s*(.*?)\\s*$") - } -} - -/** Taler-style configuration */ -class TalerConfig internal constructor( - private val cfg: Map<String, Map<String, String>> -) { - val sections: Set<String> get() = cfg.keys - - /** Create a string representation of the loaded configuration */ - fun stringify(): String = buildString { - for ((section, options) in cfg) { - appendLine("[$section]") - for ((key, value) in options) { - appendLine("$key = $value") - } - appendLine() - } - } - - /** - * Substitute ${...} and $... placeholders in a string - * with values from the PATHS section in the - * configuration and environment variables - * - * This substitution is typically only done for paths. - */ - internal fun pathsub(str: String, recursionDepth: Int = 0): String { - /** Lookup for variable value from PATHS section in the configuration and environment variables */ - fun lookup(name: String, recursionDepth: Int = 0): String? { - val pathRes = section("PATHS").string(name).orNull() - if (pathRes != null) { - return pathsub(pathRes, recursionDepth + 1) - } - return System.getenv(name) - } - - if (recursionDepth > 128) { - throw ValueError("recursion limit in path substitution exceeded for '$str'") - } else if (!str.contains('$')) { // Fast path without variables - return str - } - - var cursor = 0 - val result = StringBuilder() - while (true) { - // Look for next variable - val dollarIndex = str.indexOf("$", cursor) - if (dollarIndex == -1) { - // Reached end of string - result.append(str, cursor, str.length) - break - } - - // Append normal characters - result.append(str, cursor, dollarIndex) - cursor = dollarIndex + 1 - - // Check if variable is enclosed - val enclosed = if (str[cursor] == '{') { - // ${var - cursor++ - true - } else false // $var - - // Extract variable name - val startName = cursor - while (cursor < str.length && (str[cursor].isLetterOrDigit() || str[cursor] == '_')) { - cursor++ - } - val name = str.substring(startName, cursor) - - // Extract variable default if enclosed - val default = if (!enclosed) null else { - if (str[cursor] == '}') { - // ${var} - cursor++ - null - } else if (cursor+1<str.length && str[cursor] == ':' && str[cursor+1] == '-') { - // ${var:-default} - cursor += 2 - val startDefault = cursor - var depth = 1 - // Find end of the ${...} expression - while (cursor < str.length && depth != 0) { - if (str[cursor] == '}') { - depth-- - } else if (cursor+1<str.length && str[cursor] == '$' && str[cursor+1] == '{') { - depth++ - } - cursor++ - } - if (depth != 0) - throw ValueError("unbalanced variable expression '${str.substring(startDefault)}'") - str.substring(startDefault, cursor - 1) - } else { - throw ValueError("bad substitution '${str.substring(cursor-3)}'") - } - } - - // Value resolution - val resolved = lookup(name, recursionDepth + 1) - ?: default?.let { pathsub(it, recursionDepth + 1) } - ?: throw ValueError("unbound variable '$name'") - result.append(resolved) - } - return result.toString() - } - - /** Access [section] */ - fun section(section: String): TalerConfigSection { - val canonSection = section.uppercase() - return TalerConfigSection(this, cfg[canonSection], section) - } -} - -/** Accessor/Converter for Taler-like configuration options */ -class TalerConfigOption<T> internal constructor( - private val raw: String?, - private val option: String, - private val type: String, - private val section: TalerConfigSection, - private val transform: TalerConfigSection.(String) -> T, -) { - /** Converted value or null if missing */ - fun orNull(): T? { - if (raw == null) return null - try { - return section.transform(raw) - } catch (e: ValueError) { - throw TalerConfigError.invalid(type, section.section, option, e.msg) - } catch (e: Exception) { - throw TalerConfigError.invalid(type, section.section, option, e.message ?: e.toString()) - } - } - - /** Converted value or null if missing, log a warning if missing */ - fun orNull(logger: Logger, warn: String): T? { - val value = orNull() - if (value == null) { - val err = TalerConfigError.missing(type, section.section, option).message - logger.warn("$err$warn") - } - return value - } - - /** Converted value of default if missing */ - fun default(default: T): T = orNull() ?: default - - /** Converted value or default if missing, log a warning if missing */ - fun default(default: T, logger: Logger, warn: String): T = orNull(logger, warn) ?: default - - /** Converted value or throw if missing */ - fun require(): T = orNull() ?: throw TalerConfigError.missing(type, section.section, option) -} - -/** Accessor/Converter for Taler-like configuration sections */ -class TalerConfigSection internal constructor( - private val cfg: TalerConfig, - private val entries: Map<String, String>?, - val section: String -) { - /** Setup an accessor/converted for a [type] at [option] using [transform] */ - fun <T> option(option: String, type: String, transform: TalerConfigSection.(String) -> T): TalerConfigOption<T> { - val canonOption = option.uppercase() - var raw = entries?.get(canonOption) - if (raw == "") raw = null - return TalerConfigOption(raw, option, type, this, transform) - } - - /** Access [option] as String */ - fun string(option: String) = option(option, "string") { it } - - /** Access [option] as String with variable substitution */ - fun stringsub(option: String) = option(option, "string") { - cfg.pathsub(it) - } - - /** Access [option] as Regex */ - fun regex(option: String) = option(option, "regex") { Regex(it) } - - /** Access [option] as hexadecimal bytes */ - fun hex(option: String) = option(option, "hex") { - it.replace("\\s".toRegex(), "").decodeUpHex() - } - - /** Access [option] as BaseURL */ - fun baseURL(option: String) = option(option, "baseURL") { BaseURL.parse(it) } - - /** Access [option] as IBAN */ - fun iban(option: String) = option(option, "IBAN") { IBAN.parse(it) } - - /** Access [option] as Int */ - fun number(option: String) = option(option, "number") { - it.toIntOrNull() ?: throw ValueError("'$it' not a valid number") - } - - /** Access [option] as Boolean */ - fun boolean(option: String) = option(option, "boolean") { - when (it.lowercase()) { - "yes" -> true - "no" -> false - else -> throw ValueError("expected 'YES' or 'NO' got '$it'") - } - } - - /** Access [option] as Path */ - fun path(option: String) = option(option, "path") { - Path(cfg.pathsub(it)) - } - - /** Access [option] as Duration */ - fun duration(option: String) = option(option, "temporal") { - if (!TEMPORAL_PATTERN.matches(it)) { - throw ValueError("'$it' not a valid temporal") - } - TIME_AMOUNT_PATTERN.findAll(it).map { match -> - val (rawAmount, unit) = match.destructured - val amount = rawAmount.toLongOrNull() ?: throw ValueError("'$rawAmount' not a valid temporal amount") - val value = when (unit) { - "us" -> 1 - "ms" -> 1000 - "s", "second", "seconds", "\"" -> 1000 * 1000L - "m", "min", "minute", "minutes", "'" -> 60 * 1000 * 1000L - "h", "hour", "hours" -> 60 * 60 * 1000 * 1000L - "d", "day", "days" -> 24 * 60 * 60 * 1000L * 1000L - "week", "weeks" -> 7 * 24 * 60 * 60 * 1000L * 1000L - "year", "years", "a" -> 31536000000000L - else -> throw ValueError("'$unit' not a valid temporal unit") - } - Duration.of(amount * value, ChronoUnit.MICROS) - }.fold(Duration.ZERO) { a, b -> a.plus(b) } - } - - /** Access [option] as Instant */ - fun date(option: String) = option(option, "date") { - try { - dateToInstant(it) - } catch (e: DateTimeParseException) { - val indexFmt = if (e.errorIndex != 0) " at index ${e.errorIndex}" else "" - val causeMsg = e.cause?.message - val causeFmt = if (causeMsg != null) ": ${causeMsg}" else "" - throw ValueError("'$it' not a valid date$indexFmt$causeFmt") - } - } - - /** Access [option] as time */ - fun time(option: String) = option(option, "time") { - try { - LocalTime.parse(it, DateTimeFormatter.ISO_LOCAL_TIME) - } catch (e: DateTimeParseException) { - val indexFmt = if (e.errorIndex != 0) " at index ${e.errorIndex}" else "" - val causeMsg = e.cause?.message - val causeFmt = if (causeMsg != null) ": ${causeMsg}" else "" - throw ValueError("'$it' not a valid time$indexFmt$causeFmt") - } - } - - /** Access [option] as JSON object [T] */ - inline fun <reified T> json(option: String, type: String) = option(option, type) { - try { - Json.decodeFromString<T>(it) - } catch (e: Exception) { - throw ValueError("'$it' is malformed") - } - } - - /** Access [option] as Map<String, String> */ - fun jsonMap(option: String) = json<Map<String, String>>(option, "json key/value map") - - /** Access [option] as TalerAmount */ - fun amount(option: String, currency: String) = option(option, "amount") { - val amount = try { - TalerAmount(it) - } catch (e: CommonError) { - throw ValueError("'$it' is malformed: ${e.message}") - } - - if (amount.currency != currency) { - throw ValueError("expected currency $currency got ${amount.currency}") - } - amount - } - - /** Access a [type] at [option] using a custom [mapper] */ - fun <T> map(option: String, type: String, mapper: Map<String, T>) = option(option, type) { - mapper[it] ?: throw ValueError("expected ${fmtEntriesChoice(mapper)} got '$it'") - } - - /** Access a [type] at [option] using a custom [mapper] with code execution */ - fun <T> mapLambda(option: String, type: String, mapper: Map<String, () -> T>) = option(option, type) { - mapper[it]?.invoke() ?: throw ValueError("expected ${fmtEntriesChoice(mapper)} got '$it'") - } - - companion object { - private val TIME_AMOUNT_PATTERN = Regex("([0-9]+) ?([a-z'\"]+)") - private val TEMPORAL_PATTERN = Regex(" *([0-9]+ ?[a-z'\"]+ *)+") - private val WHITESPACE_PATTERN = Regex("\\s") - - private fun <T> fmtEntriesChoice(mapper: Map<String, T>): String { - return buildString { - val iter = mapper.keys.iterator() - var next = iter.next() - append("'$next'") - while (iter.hasNext()) { - next = iter.next() - if (iter.hasNext()) { - append(", '$next'") - } else { - append(" or '$next'") - } - } - } - } - } -} diff --git a/libeufin-common/src/main/kotlin/TalerErrorCode.kt b/libeufin-common/src/main/kotlin/TalerErrorCode.kt @@ -1,2191 +0,0 @@ -/* - * This file is part of GNU Taler - * Copyright (C) 2012-2026 Taler Systems SA - - * GNU Taler is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published - * by the Free Software Foundation, either version 3 of the License, - * or (at your option) any later version. - - * GNU Taler 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 - * Lesser General Public License for more details. - - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - - * SPDX-License-Identifier: LGPL3.0-or-later - - * Note: the LGPL does not apply to all components of GNU Taler, - * but it does apply to this file. - */ -package tech.libeufin.common - -enum class TalerErrorCode(val code: Int, val status: Int, val description: String) { - - /** Special code to indicate success (no error). */ - NONE(0, 0, "Special code to indicate success (no error)."), - - /** An error response did not include an error code in the format expected by the client. Most likely, the server does not speak the GNU Taler protocol. Check the URL and/or the network connection to the server. */ - INVALID(1, 0, "An error response did not include an error code in the format expected by the client. Most likely, the server does not speak the GNU Taler protocol. Check the URL and/or the network connection to the server."), - - /** An internal failure happened on the client side. Details should be in the local logs. Check if you are using the latest available version or file a report with the developers. */ - GENERIC_CLIENT_INTERNAL_ERROR(2, 0, "An internal failure happened on the client side. Details should be in the local logs. Check if you are using the latest available version or file a report with the developers."), - - /** The client does not support the protocol version advertised by the server. */ - GENERIC_CLIENT_UNSUPPORTED_PROTOCOL_VERSION(3, 0, "The client does not support the protocol version advertised by the server."), - - /** The response we got from the server was not in the expected format. Most likely, the server does not speak the GNU Taler protocol. Check the URL and/or the network connection to the server. */ - GENERIC_INVALID_RESPONSE(10, 0, "The response we got from the server was not in the expected format. Most likely, the server does not speak the GNU Taler protocol. Check the URL and/or the network connection to the server."), - - /** The operation timed out. Trying again might help. Check the network connection. */ - GENERIC_TIMEOUT(11, 0, "The operation timed out. Trying again might help. Check the network connection."), - - /** The protocol version given by the server does not follow the required format. Most likely, the server does not speak the GNU Taler protocol. Check the URL and/or the network connection to the server. */ - GENERIC_VERSION_MALFORMED(12, 0, "The protocol version given by the server does not follow the required format. Most likely, the server does not speak the GNU Taler protocol. Check the URL and/or the network connection to the server."), - - /** The service responded with a reply that was in the right data format, but the content did not satisfy the protocol. Please file a bug report. */ - GENERIC_REPLY_MALFORMED(13, 0, "The service responded with a reply that was in the right data format, but the content did not satisfy the protocol. Please file a bug report."), - - /** There is an error in the client-side configuration, for example an option is set to an invalid value. Check the logs and fix the local configuration. */ - GENERIC_CONFIGURATION_INVALID(14, 0, "There is an error in the client-side configuration, for example an option is set to an invalid value. Check the logs and fix the local configuration."), - - /** The client made a request to a service, but received an error response it does not know how to handle. Please file a bug report. */ - GENERIC_UNEXPECTED_REQUEST_ERROR(15, 0, "The client made a request to a service, but received an error response it does not know how to handle. Please file a bug report."), - - /** The token used by the client to authorize the request does not grant the required permissions for the request. Check the requirements and obtain a suitable authorization token to proceed. */ - GENERIC_TOKEN_PERMISSION_INSUFFICIENT(16, 403, "The token used by the client to authorize the request does not grant the required permissions for the request. Check the requirements and obtain a suitable authorization token to proceed."), - - /** The HTTP method used is invalid for this endpoint. This is likely a bug in the client implementation. Check if you are using the latest available version and/or file a report with the developers. */ - GENERIC_METHOD_INVALID(20, 405, "The HTTP method used is invalid for this endpoint. This is likely a bug in the client implementation. Check if you are using the latest available version and/or file a report with the developers."), - - /** There is no endpoint defined for the URL provided by the client. Check if you used the correct URL and/or file a report with the developers of the client software. */ - GENERIC_ENDPOINT_UNKNOWN(21, 404, "There is no endpoint defined for the URL provided by the client. Check if you used the correct URL and/or file a report with the developers of the client software."), - - /** The JSON in the client's request was malformed. This is likely a bug in the client implementation. Check if you are using the latest available version and/or file a report with the developers. */ - GENERIC_JSON_INVALID(22, 400, "The JSON in the client's request was malformed. This is likely a bug in the client implementation. Check if you are using the latest available version and/or file a report with the developers."), - - /** Some of the HTTP headers provided by the client were malformed and caused the server to not be able to handle the request. This is likely a bug in the client implementation. Check if you are using the latest available version and/or file a report with the developers. */ - GENERIC_HTTP_HEADERS_MALFORMED(23, 400, "Some of the HTTP headers provided by the client were malformed and caused the server to not be able to handle the request. This is likely a bug in the client implementation. Check if you are using the latest available version and/or file a report with the developers."), - - /** The payto:// URI provided by the client is malformed. Check that you are using the correct syntax as of RFC 8905 and/or that you entered the bank account number correctly. */ - GENERIC_PAYTO_URI_MALFORMED(24, 400, "The payto:// URI provided by the client is malformed. Check that you are using the correct syntax as of RFC 8905 and/or that you entered the bank account number correctly."), - - /** A required parameter in the request was missing. This is likely a bug in the client implementation. Check if you are using the latest available version and/or file a report with the developers. */ - GENERIC_PARAMETER_MISSING(25, 400, "A required parameter in the request was missing. This is likely a bug in the client implementation. Check if you are using the latest available version and/or file a report with the developers."), - - /** A parameter in the request was malformed. This is likely a bug in the client implementation. Check if you are using the latest available version and/or file a report with the developers. */ - GENERIC_PARAMETER_MALFORMED(26, 400, "A parameter in the request was malformed. This is likely a bug in the client implementation. Check if you are using the latest available version and/or file a report with the developers."), - - /** The reserve public key was malformed. */ - GENERIC_RESERVE_PUB_MALFORMED(27, 400, "The reserve public key was malformed."), - - /** The body in the request could not be decompressed by the server. This is likely a bug in the client implementation. Check if you are using the latest available version and/or file a report with the developers. */ - GENERIC_COMPRESSION_INVALID(28, 400, "The body in the request could not be decompressed by the server. This is likely a bug in the client implementation. Check if you are using the latest available version and/or file a report with the developers."), - - /** A segment in the path of the URL provided by the client is malformed. Check that you are using the correct encoding for the URL. */ - GENERIC_PATH_SEGMENT_MALFORMED(29, 400, "A segment in the path of the URL provided by the client is malformed. Check that you are using the correct encoding for the URL."), - - /** The currency involved in the operation is not acceptable for this server. Check your configuration and make sure the currency specified for a given service provider is one of the currencies supported by that provider. */ - GENERIC_CURRENCY_MISMATCH(30, 400, "The currency involved in the operation is not acceptable for this server. Check your configuration and make sure the currency specified for a given service provider is one of the currencies supported by that provider."), - - /** The URI is longer than the longest URI the HTTP server is willing to parse. If you believe this was a legitimate request, contact the server administrators and/or the software developers to increase the limit. */ - GENERIC_URI_TOO_LONG(31, 414, "The URI is longer than the longest URI the HTTP server is willing to parse. If you believe this was a legitimate request, contact the server administrators and/or the software developers to increase the limit."), - - /** The body is too large to be permissible for the endpoint. If you believe this was a legitimate request, contact the server administrators and/or the software developers to increase the limit. */ - GENERIC_UPLOAD_EXCEEDS_LIMIT(32, 413, "The body is too large to be permissible for the endpoint. If you believe this was a legitimate request, contact the server administrators and/or the software developers to increase the limit."), - - /** A parameter in the request was given that must not be present. This is likely a bug in the client implementation. Check if you are using the latest available version and/or file a report with the developers. */ - GENERIC_PARAMETER_EXTRA(33, 400, "A parameter in the request was given that must not be present. This is likely a bug in the client implementation. Check if you are using the latest available version and/or file a report with the developers."), - - /** The service refused the request due to lack of proper authorization. Accessing this endpoint requires an access token from the account owner. */ - GENERIC_UNAUTHORIZED(40, 401, "The service refused the request due to lack of proper authorization. Accessing this endpoint requires an access token from the account owner."), - - /** The service refused the request as the given authorization token is unknown. You should request a valid access token from the account owner. */ - GENERIC_TOKEN_UNKNOWN(41, 401, "The service refused the request as the given authorization token is unknown. You should request a valid access token from the account owner."), - - /** The service refused the request as the given authorization token expired. You should request a fresh authorization token from the account owner. */ - GENERIC_TOKEN_EXPIRED(42, 401, "The service refused the request as the given authorization token expired. You should request a fresh authorization token from the account owner."), - - /** The service refused the request as the given authorization token is invalid or malformed. You should check that you have the right credentials. */ - GENERIC_TOKEN_MALFORMED(43, 401, "The service refused the request as the given authorization token is invalid or malformed. You should check that you have the right credentials."), - - /** The service refused the request due to lack of proper rights on the resource. You may need different credentials to be allowed to perform this operation. */ - GENERIC_FORBIDDEN(44, 403, "The service refused the request due to lack of proper rights on the resource. You may need different credentials to be allowed to perform this operation."), - - /** The service failed initialize its connection to the database. The system administrator should check that the service has permissions to access the database and that the database is running. */ - GENERIC_DB_SETUP_FAILED(50, 500, "The service failed initialize its connection to the database. The system administrator should check that the service has permissions to access the database and that the database is running."), - - /** The service encountered an error event to just start the database transaction. The system administrator should check that the database is running. */ - GENERIC_DB_START_FAILED(51, 500, "The service encountered an error event to just start the database transaction. The system administrator should check that the database is running."), - - /** The service failed to store information in its database. The system administrator should check that the database is running and review the service logs. */ - GENERIC_DB_STORE_FAILED(52, 500, "The service failed to store information in its database. The system administrator should check that the database is running and review the service logs."), - - /** The service failed to fetch information from its database. The system administrator should check that the database is running and review the service logs. */ - GENERIC_DB_FETCH_FAILED(53, 500, "The service failed to fetch information from its database. The system administrator should check that the database is running and review the service logs."), - - /** The service encountered an unrecoverable error trying to commit a transaction to the database. The system administrator should check that the database is running and review the service logs. */ - GENERIC_DB_COMMIT_FAILED(54, 500, "The service encountered an unrecoverable error trying to commit a transaction to the database. The system administrator should check that the database is running and review the service logs."), - - /** The service encountered an error event to commit the database transaction, even after repeatedly retrying it there was always a conflicting transaction. This indicates a repeated serialization error; it should only happen if some client maliciously tries to create conflicting concurrent transactions. It could also be a sign of a missing index. Check if you are using the latest available version and/or file a report with the developers. */ - GENERIC_DB_SOFT_FAILURE(55, 500, "The service encountered an error event to commit the database transaction, even after repeatedly retrying it there was always a conflicting transaction. This indicates a repeated serialization error; it should only happen if some client maliciously tries to create conflicting concurrent transactions. It could also be a sign of a missing index. Check if you are using the latest available version and/or file a report with the developers."), - - /** The service's database is inconsistent and violates service-internal invariants. Check if you are using the latest available version and/or file a report with the developers. */ - GENERIC_DB_INVARIANT_FAILURE(56, 500, "The service's database is inconsistent and violates service-internal invariants. Check if you are using the latest available version and/or file a report with the developers."), - - /** The HTTP server experienced an internal invariant failure (bug). Check if you are using the latest available version and/or file a report with the developers. */ - GENERIC_INTERNAL_INVARIANT_FAILURE(60, 500, "The HTTP server experienced an internal invariant failure (bug). Check if you are using the latest available version and/or file a report with the developers."), - - /** The service could not compute a cryptographic hash over some JSON value. Check if you are using the latest available version and/or file a report with the developers. */ - GENERIC_FAILED_COMPUTE_JSON_HASH(61, 500, "The service could not compute a cryptographic hash over some JSON value. Check if you are using the latest available version and/or file a report with the developers."), - - /** The service could not compute an amount. Check if you are using the latest available version and/or file a report with the developers. */ - GENERIC_FAILED_COMPUTE_AMOUNT(62, 500, "The service could not compute an amount. Check if you are using the latest available version and/or file a report with the developers."), - - /** The HTTP server had insufficient memory to parse the request. Restarting services periodically can help, especially if Postgres is using excessive amounts of memory. Check with the system administrator to investigate. */ - GENERIC_PARSER_OUT_OF_MEMORY(70, 500, "The HTTP server had insufficient memory to parse the request. Restarting services periodically can help, especially if Postgres is using excessive amounts of memory. Check with the system administrator to investigate."), - - /** The HTTP server failed to allocate memory. Restarting services periodically can help, especially if Postgres is using excessive amounts of memory. Check with the system administrator to investigate. */ - GENERIC_ALLOCATION_FAILURE(71, 500, "The HTTP server failed to allocate memory. Restarting services periodically can help, especially if Postgres is using excessive amounts of memory. Check with the system administrator to investigate."), - - /** The HTTP server failed to allocate memory for building JSON reply. Restarting services periodically can help, especially if Postgres is using excessive amounts of memory. Check with the system administrator to investigate. */ - GENERIC_JSON_ALLOCATION_FAILURE(72, 500, "The HTTP server failed to allocate memory for building JSON reply. Restarting services periodically can help, especially if Postgres is using excessive amounts of memory. Check with the system administrator to investigate."), - - /** The HTTP server failed to allocate memory for making a CURL request. Restarting services periodically can help, especially if Postgres is using excessive amounts of memory. Check with the system administrator to investigate. */ - GENERIC_CURL_ALLOCATION_FAILURE(73, 500, "The HTTP server failed to allocate memory for making a CURL request. Restarting services periodically can help, especially if Postgres is using excessive amounts of memory. Check with the system administrator to investigate."), - - /** The backend could not locate a required template to generate an HTML reply. The system administrator should check if the resource files are installed in the correct location and are readable to the service. */ - GENERIC_FAILED_TO_LOAD_TEMPLATE(74, 406, "The backend could not locate a required template to generate an HTML reply. The system administrator should check if the resource files are installed in the correct location and are readable to the service."), - - /** The backend could not expand the template to generate an HTML reply. The system administrator should investigate the logs and check if the templates are well-formed. */ - GENERIC_FAILED_TO_EXPAND_TEMPLATE(75, 500, "The backend could not expand the template to generate an HTML reply. The system administrator should investigate the logs and check if the templates are well-formed."), - - /** The requested feature is not implemented by the server. The system administrator of the server may try to update the software or build it with other options to enable the feature. */ - GENERIC_FEATURE_NOT_IMPLEMENTED(76, 501, "The requested feature is not implemented by the server. The system administrator of the server may try to update the software or build it with other options to enable the feature."), - - /** The operating system failed to allocate required resources. Restarting services periodically can help, especially if Postgres is using excessive amounts of memory. Check with the system administrator to investigate. */ - GENERIC_OS_RESOURCE_ALLOCATION_FAILURE(77, 500, "The operating system failed to allocate required resources. Restarting services periodically can help, especially if Postgres is using excessive amounts of memory. Check with the system administrator to investigate."), - - /** Exchange is badly configured and thus cannot operate. */ - EXCHANGE_GENERIC_BAD_CONFIGURATION(1000, 500, "Exchange is badly configured and thus cannot operate."), - - /** Operation specified unknown for this endpoint. */ - EXCHANGE_GENERIC_OPERATION_UNKNOWN(1001, 404, "Operation specified unknown for this endpoint."), - - /** The number of segments included in the URI does not match the number of segments expected by the endpoint. */ - EXCHANGE_GENERIC_WRONG_NUMBER_OF_SEGMENTS(1002, 404, "The number of segments included in the URI does not match the number of segments expected by the endpoint."), - - /** The same coin was already used with a different denomination previously. */ - EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY(1003, 409, "The same coin was already used with a different denomination previously."), - - /** The public key of given to a \"/coins/\" endpoint of the exchange was malformed. */ - EXCHANGE_GENERIC_COINS_INVALID_COIN_PUB(1004, 400, "The public key of given to a \"/coins/\" endpoint of the exchange was malformed."), - - /** The exchange is not aware of the denomination key the wallet requested for the operation. */ - EXCHANGE_GENERIC_DENOMINATION_KEY_UNKNOWN(1005, 404, "The exchange is not aware of the denomination key the wallet requested for the operation."), - - /** The signature of the denomination key over the coin is not valid. */ - EXCHANGE_DENOMINATION_SIGNATURE_INVALID(1006, 403, "The signature of the denomination key over the coin is not valid."), - - /** The exchange failed to perform the operation as it could not find the private keys. This is a problem with the exchange setup, not with the client's request. */ - EXCHANGE_GENERIC_KEYS_MISSING(1007, 503, "The exchange failed to perform the operation as it could not find the private keys. This is a problem with the exchange setup, not with the client's request."), - - /** Validity period of the denomination lies in the future. */ - EXCHANGE_GENERIC_DENOMINATION_VALIDITY_IN_FUTURE(1008, 412, "Validity period of the denomination lies in the future."), - - /** Denomination key of the coin is past its expiration time for the requested operation. */ - EXCHANGE_GENERIC_DENOMINATION_EXPIRED(1009, 410, "Denomination key of the coin is past its expiration time for the requested operation."), - - /** Denomination key of the coin has been revoked. */ - EXCHANGE_GENERIC_DENOMINATION_REVOKED(1010, 410, "Denomination key of the coin has been revoked."), - - /** An operation where the exchange interacted with a security module timed out. */ - EXCHANGE_GENERIC_SECMOD_TIMEOUT(1011, 500, "An operation where the exchange interacted with a security module timed out."), - - /** The respective coin did not have sufficient residual value for the operation. The \"history\" in this response provides the \"residual_value\" of the coin, which may be less than its \"original_value\". */ - EXCHANGE_GENERIC_INSUFFICIENT_FUNDS(1012, 409, "The respective coin did not have sufficient residual value for the operation. The \"history\" in this response provides the \"residual_value\" of the coin, which may be less than its \"original_value\"."), - - /** The exchange had an internal error reconstructing the transaction history of the coin that was being processed. */ - EXCHANGE_GENERIC_COIN_HISTORY_COMPUTATION_FAILED(1013, 500, "The exchange had an internal error reconstructing the transaction history of the coin that was being processed."), - - /** The exchange failed to obtain the transaction history of the given coin from the database while generating an insufficient funds errors. */ - EXCHANGE_GENERIC_HISTORY_DB_ERROR_INSUFFICIENT_FUNDS(1014, 500, "The exchange failed to obtain the transaction history of the given coin from the database while generating an insufficient funds errors."), - - /** The same coin was already used with a different age hash previously. */ - EXCHANGE_GENERIC_COIN_CONFLICTING_AGE_HASH(1015, 409, "The same coin was already used with a different age hash previously."), - - /** The requested operation is not valid for the cipher used by the selected denomination. */ - EXCHANGE_GENERIC_INVALID_DENOMINATION_CIPHER_FOR_OPERATION(1016, 400, "The requested operation is not valid for the cipher used by the selected denomination."), - - /** The provided arguments for the operation use inconsistent ciphers. */ - EXCHANGE_GENERIC_CIPHER_MISMATCH(1017, 400, "The provided arguments for the operation use inconsistent ciphers."), - - /** The number of denominations specified in the request exceeds the limit of the exchange. */ - EXCHANGE_GENERIC_NEW_DENOMS_ARRAY_SIZE_EXCESSIVE(1018, 400, "The number of denominations specified in the request exceeds the limit of the exchange."), - - /** The coin is not known to the exchange (yet). */ - EXCHANGE_GENERIC_COIN_UNKNOWN(1019, 404, "The coin is not known to the exchange (yet)."), - - /** The time at the server is too far off from the time specified in the request. Most likely the client system time is wrong. */ - EXCHANGE_GENERIC_CLOCK_SKEW(1020, 400, "The time at the server is too far off from the time specified in the request. Most likely the client system time is wrong."), - - /** The specified amount for the coin is higher than the value of the denomination of the coin. */ - EXCHANGE_GENERIC_AMOUNT_EXCEEDS_DENOMINATION_VALUE(1021, 400, "The specified amount for the coin is higher than the value of the denomination of the coin."), - - /** The exchange was not properly configured with global fees. */ - EXCHANGE_GENERIC_GLOBAL_FEES_MISSING(1022, 500, "The exchange was not properly configured with global fees."), - - /** The exchange was not properly configured with wire fees. */ - EXCHANGE_GENERIC_WIRE_FEES_MISSING(1023, 500, "The exchange was not properly configured with wire fees."), - - /** The purse public key was malformed. */ - EXCHANGE_GENERIC_PURSE_PUB_MALFORMED(1024, 400, "The purse public key was malformed."), - - /** The purse is unknown. */ - EXCHANGE_GENERIC_PURSE_UNKNOWN(1025, 404, "The purse is unknown."), - - /** The purse has expired. */ - EXCHANGE_GENERIC_PURSE_EXPIRED(1026, 410, "The purse has expired."), - - /** The exchange has no information about the \"reserve_pub\" that was given. */ - EXCHANGE_GENERIC_RESERVE_UNKNOWN(1027, 404, "The exchange has no information about the \"reserve_pub\" that was given."), - - /** The exchange is not allowed to proceed with the operation until the client has satisfied a KYC check. */ - EXCHANGE_GENERIC_KYC_REQUIRED(1028, 451, "The exchange is not allowed to proceed with the operation until the client has satisfied a KYC check."), - - /** Inconsistency between provided age commitment and attest: either none or both must be provided */ - EXCHANGE_PURSE_DEPOSIT_COIN_CONFLICTING_ATTEST_VS_AGE_COMMITMENT(1029, 400, "Inconsistency between provided age commitment and attest: either none or both must be provided"), - - /** The provided attestation for the minimum age couldn't be verified by the exchange. */ - EXCHANGE_PURSE_DEPOSIT_COIN_AGE_ATTESTATION_FAILURE(1030, 400, "The provided attestation for the minimum age couldn't be verified by the exchange."), - - /** The purse was deleted. */ - EXCHANGE_GENERIC_PURSE_DELETED(1031, 410, "The purse was deleted."), - - /** The public key of the AML officer in the URL was malformed. */ - EXCHANGE_GENERIC_AML_OFFICER_PUB_MALFORMED(1032, 400, "The public key of the AML officer in the URL was malformed."), - - /** The signature affirming the GET request of the AML officer is invalid. */ - EXCHANGE_GENERIC_AML_OFFICER_GET_SIGNATURE_INVALID(1033, 403, "The signature affirming the GET request of the AML officer is invalid."), - - /** The specified AML officer does not have access at this time. */ - EXCHANGE_GENERIC_AML_OFFICER_ACCESS_DENIED(1034, 403, "The specified AML officer does not have access at this time."), - - /** The requested operation is denied pending the resolution of an anti-money laundering investigation by the exchange operator. This is a manual process, please wait and retry later. */ - EXCHANGE_GENERIC_AML_PENDING(1035, 451, "The requested operation is denied pending the resolution of an anti-money laundering investigation by the exchange operator. This is a manual process, please wait and retry later."), - - /** The requested operation is denied as the account was frozen on suspicion of money laundering. Please contact the exchange operator. */ - EXCHANGE_GENERIC_AML_FROZEN(1036, 451, "The requested operation is denied as the account was frozen on suspicion of money laundering. Please contact the exchange operator."), - - /** The exchange failed to start a KYC attribute conversion helper process. It is likely configured incorrectly. */ - EXCHANGE_GENERIC_KYC_CONVERTER_FAILED(1037, 500, "The exchange failed to start a KYC attribute conversion helper process. It is likely configured incorrectly."), - - /** The KYC operation failed. This could be because the KYC provider rejected the KYC data provided, or because the user aborted the KYC process. */ - EXCHANGE_GENERIC_KYC_FAILED(1038, 500, "The KYC operation failed. This could be because the KYC provider rejected the KYC data provided, or because the user aborted the KYC process."), - - /** A fallback measure for a KYC operation failed. This is a bug. Users should contact the exchange operator. */ - EXCHANGE_GENERIC_KYC_FALLBACK_FAILED(1039, 500, "A fallback measure for a KYC operation failed. This is a bug. Users should contact the exchange operator."), - - /** The specified fallback measure for a KYC operation is unknown. This is a bug. Users should contact the exchange operator. */ - EXCHANGE_GENERIC_KYC_FALLBACK_UNKNOWN(1040, 500, "The specified fallback measure for a KYC operation is unknown. This is a bug. Users should contact the exchange operator."), - - /** The exchange is not aware of the bank account (payto URI or hash thereof) specified in the request and thus cannot perform the requested operation. The client should check that the select account is correct. */ - EXCHANGE_GENERIC_BANK_ACCOUNT_UNKNOWN(1041, 404, "The exchange is not aware of the bank account (payto URI or hash thereof) specified in the request and thus cannot perform the requested operation. The client should check that the select account is correct."), - - /** The AML processing at the exchange did not terminate in an adequate timeframe. This is likely a configuration problem at the payment service provider. Users should contact the exchange operator. */ - EXCHANGE_GENERIC_AML_PROGRAM_RECURSION_DETECTED(1042, 500, "The AML processing at the exchange did not terminate in an adequate timeframe. This is likely a configuration problem at the payment service provider. Users should contact the exchange operator."), - - /** A check against sanction lists failed. This is indicative of an internal error in the sanction list processing logic. This needs to be investigated by the exchange operator. */ - EXCHANGE_GENERIC_KYC_SANCTION_LIST_CHECK_FAILED(1043, 500, "A check against sanction lists failed. This is indicative of an internal error in the sanction list processing logic. This needs to be investigated by the exchange operator."), - - /** The process to generate a PDF from a template failed. A likely cause is a syntactic error in the template. This needs to be investigated by the exchange operator. */ - EXCHANGE_GENERIC_TYPST_TEMPLATE_FAILURE(1044, 500, "The process to generate a PDF from a template failed. A likely cause is a syntactic error in the template. This needs to be investigated by the exchange operator."), - - /** A process to combine multiple PDFs into one larger document failed. A likely cause is a resource exhaustion problem on the server. This needs to be investigated by the exchange operator. */ - EXCHANGE_GENERIC_PDFTK_FAILURE(1045, 500, "A process to combine multiple PDFs into one larger document failed. A likely cause is a resource exhaustion problem on the server. This needs to be investigated by the exchange operator."), - - /** The process to generate a PDF from a template crashed. A likely cause is a bug in the Typst software. This needs to be investigated by the exchange operator. */ - EXCHANGE_GENERIC_TYPST_CRASH(1046, 500, "The process to generate a PDF from a template crashed. A likely cause is a bug in the Typst software. This needs to be investigated by the exchange operator."), - - /** The process to combine multiple PDFs into a larger document crashed. A likely cause is a bug in the pdftk software. This needs to be investigated by the exchange operator. */ - EXCHANGE_GENERIC_PDFTK_CRASH(1047, 500, "The process to combine multiple PDFs into a larger document crashed. A likely cause is a bug in the pdftk software. This needs to be investigated by the exchange operator."), - - /** One of the binaries needed to generate the PDF is not installed. If this feature is required, the system administrator should make sure Typst and pdftk are both installed. */ - EXCHANGE_GENERIC_NO_TYPST_OR_PDFTK(1048, 501, "One of the binaries needed to generate the PDF is not installed. If this feature is required, the system administrator should make sure Typst and pdftk are both installed."), - - /** The exchange is not aware of the given target account. The specified account is not a customer of this service. */ - EXCHANGE_GENERIC_TARGET_ACCOUNT_UNKNOWN(1049, 404, "The exchange is not aware of the given target account. The specified account is not a customer of this service."), - - /** The exchange did not find information about the specified transaction in the database. */ - EXCHANGE_DEPOSITS_GET_NOT_FOUND(1100, 404, "The exchange did not find information about the specified transaction in the database."), - - /** The wire hash of given to a \"/deposits/\" handler was malformed. */ - EXCHANGE_DEPOSITS_GET_INVALID_H_WIRE(1101, 400, "The wire hash of given to a \"/deposits/\" handler was malformed."), - - /** The merchant key of given to a \"/deposits/\" handler was malformed. */ - EXCHANGE_DEPOSITS_GET_INVALID_MERCHANT_PUB(1102, 400, "The merchant key of given to a \"/deposits/\" handler was malformed."), - - /** The hash of the contract terms given to a \"/deposits/\" handler was malformed. */ - EXCHANGE_DEPOSITS_GET_INVALID_H_CONTRACT_TERMS(1103, 400, "The hash of the contract terms given to a \"/deposits/\" handler was malformed."), - - /** The coin public key of given to a \"/deposits/\" handler was malformed. */ - EXCHANGE_DEPOSITS_GET_INVALID_COIN_PUB(1104, 400, "The coin public key of given to a \"/deposits/\" handler was malformed."), - - /** The signature returned by the exchange in a /deposits/ request was malformed. */ - EXCHANGE_DEPOSITS_GET_INVALID_SIGNATURE_BY_EXCHANGE(1105, 0, "The signature returned by the exchange in a /deposits/ request was malformed."), - - /** The signature of the merchant is invalid. */ - EXCHANGE_DEPOSITS_GET_MERCHANT_SIGNATURE_INVALID(1106, 403, "The signature of the merchant is invalid."), - - /** The provided policy data was not accepted */ - EXCHANGE_DEPOSITS_POLICY_NOT_ACCEPTED(1107, 400, "The provided policy data was not accepted"), - - /** The given reserve does not have sufficient funds to admit the requested withdraw operation at this time. The response includes the current \"balance\" of the reserve as well as the transaction \"history\" that lead to this balance. */ - EXCHANGE_WITHDRAW_INSUFFICIENT_FUNDS(1150, 409, "The given reserve does not have sufficient funds to admit the requested withdraw operation at this time. The response includes the current \"balance\" of the reserve as well as the transaction \"history\" that lead to this balance."), - - /** The given reserve does not have sufficient funds to admit the requested age-withdraw operation at this time. The response includes the current \"balance\" of the reserve as well as the transaction \"history\" that lead to this balance. */ - EXCHANGE_AGE_WITHDRAW_INSUFFICIENT_FUNDS(1151, 409, "The given reserve does not have sufficient funds to admit the requested age-withdraw operation at this time. The response includes the current \"balance\" of the reserve as well as the transaction \"history\" that lead to this balance."), - - /** The amount to withdraw together with the fee exceeds the numeric range for Taler amounts. This is not a client failure, as the coin value and fees come from the exchange's configuration. */ - EXCHANGE_WITHDRAW_AMOUNT_FEE_OVERFLOW(1152, 500, "The amount to withdraw together with the fee exceeds the numeric range for Taler amounts. This is not a client failure, as the coin value and fees come from the exchange's configuration."), - - /** The exchange failed to create the signature using the denomination key. */ - EXCHANGE_WITHDRAW_SIGNATURE_FAILED(1153, 500, "The exchange failed to create the signature using the denomination key."), - - /** The signature of the reserve is not valid. */ - EXCHANGE_WITHDRAW_RESERVE_SIGNATURE_INVALID(1154, 403, "The signature of the reserve is not valid."), - - /** When computing the reserve history, we ended up with a negative overall balance, which should be impossible. */ - EXCHANGE_RESERVE_HISTORY_ERROR_INSUFFICIENT_FUNDS(1155, 500, "When computing the reserve history, we ended up with a negative overall balance, which should be impossible."), - - /** The reserve did not have sufficient funds in it to pay for a full reserve history statement. */ - EXCHANGE_GET_RESERVE_HISTORY_ERROR_INSUFFICIENT_BALANCE(1156, 409, "The reserve did not have sufficient funds in it to pay for a full reserve history statement."), - - /** Withdraw period of the coin to be withdrawn is in the past. */ - EXCHANGE_WITHDRAW_DENOMINATION_KEY_LOST(1158, 410, "Withdraw period of the coin to be withdrawn is in the past."), - - /** The client failed to unblind the blind signature. */ - EXCHANGE_WITHDRAW_UNBLIND_FAILURE(1159, 0, "The client failed to unblind the blind signature."), - - /** The client reused a withdraw nonce, which is not allowed. */ - EXCHANGE_WITHDRAW_NONCE_REUSE(1160, 409, "The client reused a withdraw nonce, which is not allowed."), - - /** The client provided an unknown commitment for an age-withdraw request. */ - EXCHANGE_WITHDRAW_COMMITMENT_UNKNOWN(1161, 400, "The client provided an unknown commitment for an age-withdraw request."), - - /** The total sum of amounts from the denominations did overflow. */ - EXCHANGE_WITHDRAW_AMOUNT_OVERFLOW(1162, 500, "The total sum of amounts from the denominations did overflow."), - - /** The total sum of value and fees from the denominations differs from the committed amount with fees. */ - EXCHANGE_AGE_WITHDRAW_AMOUNT_INCORRECT(1163, 400, "The total sum of value and fees from the denominations differs from the committed amount with fees."), - - /** The original commitment differs from the calculated hash */ - EXCHANGE_WITHDRAW_REVEAL_INVALID_HASH(1164, 400, "The original commitment differs from the calculated hash"), - - /** The maximum age in the commitment is too large for the reserve */ - EXCHANGE_WITHDRAW_MAXIMUM_AGE_TOO_LARGE(1165, 409, "The maximum age in the commitment is too large for the reserve"), - - /** The withdraw operation included the same planchet more than once. This is not allowed. */ - EXCHANGE_WITHDRAW_IDEMPOTENT_PLANCHET(1175, 400, "The withdraw operation included the same planchet more than once. This is not allowed."), - - /** The signature made by the coin over the deposit permission is not valid. */ - EXCHANGE_DEPOSIT_COIN_SIGNATURE_INVALID(1205, 403, "The signature made by the coin over the deposit permission is not valid."), - - /** The same coin was already deposited for the same merchant and contract with other details. */ - EXCHANGE_DEPOSIT_CONFLICTING_CONTRACT(1206, 409, "The same coin was already deposited for the same merchant and contract with other details."), - - /** The stated value of the coin after the deposit fee is subtracted would be negative. */ - EXCHANGE_DEPOSIT_NEGATIVE_VALUE_AFTER_FEE(1207, 400, "The stated value of the coin after the deposit fee is subtracted would be negative."), - - /** The stated refund deadline is after the wire deadline. */ - EXCHANGE_DEPOSIT_REFUND_DEADLINE_AFTER_WIRE_DEADLINE(1208, 400, "The stated refund deadline is after the wire deadline."), - - /** The stated wire deadline is \"never\", which makes no sense. */ - EXCHANGE_DEPOSIT_WIRE_DEADLINE_IS_NEVER(1209, 400, "The stated wire deadline is \"never\", which makes no sense."), - - /** The exchange failed to canonicalize and hash the given wire format. For example, the merchant failed to provide the \"salt\" or a valid payto:// URI in the wire details. Note that while the exchange will do some basic sanity checking on the wire details, it cannot warrant that the banking system will ultimately be able to route to the specified address, even if this check passed. */ - EXCHANGE_DEPOSIT_INVALID_WIRE_FORMAT_JSON(1210, 400, "The exchange failed to canonicalize and hash the given wire format. For example, the merchant failed to provide the \"salt\" or a valid payto:// URI in the wire details. Note that while the exchange will do some basic sanity checking on the wire details, it cannot warrant that the banking system will ultimately be able to route to the specified address, even if this check passed."), - - /** The hash of the given wire address does not match the wire hash specified in the proposal data. */ - EXCHANGE_DEPOSIT_INVALID_WIRE_FORMAT_CONTRACT_HASH_CONFLICT(1211, 400, "The hash of the given wire address does not match the wire hash specified in the proposal data."), - - /** The signature provided by the exchange is not valid. */ - EXCHANGE_DEPOSIT_INVALID_SIGNATURE_BY_EXCHANGE(1221, 0, "The signature provided by the exchange is not valid."), - - /** The deposited amount is smaller than the deposit fee, which would result in a negative contribution. */ - EXCHANGE_DEPOSIT_FEE_ABOVE_AMOUNT(1222, 400, "The deposited amount is smaller than the deposit fee, which would result in a negative contribution."), - - /** The proof of policy fulfillment was invalid. */ - EXCHANGE_EXTENSIONS_INVALID_FULFILLMENT(1240, 400, "The proof of policy fulfillment was invalid."), - - /** The coin history was requested with a bad signature. */ - EXCHANGE_COIN_HISTORY_BAD_SIGNATURE(1251, 403, "The coin history was requested with a bad signature."), - - /** The reserve history was requested with a bad signature. */ - EXCHANGE_RESERVE_HISTORY_BAD_SIGNATURE(1252, 403, "The reserve history was requested with a bad signature."), - - /** The exchange encountered melt fees exceeding the melted coin's contribution. */ - EXCHANGE_MELT_FEES_EXCEED_CONTRIBUTION(1302, 400, "The exchange encountered melt fees exceeding the melted coin's contribution."), - - /** The signature made with the coin to be melted is invalid. */ - EXCHANGE_MELT_COIN_SIGNATURE_INVALID(1303, 403, "The signature made with the coin to be melted is invalid."), - - /** The denomination of the given coin has past its expiration date and it is also not a valid zombie (that is, was not refreshed with the fresh coin being subjected to recoup). */ - EXCHANGE_MELT_COIN_EXPIRED_NO_ZOMBIE(1305, 400, "The denomination of the given coin has past its expiration date and it is also not a valid zombie (that is, was not refreshed with the fresh coin being subjected to recoup)."), - - /** The signature returned by the exchange in a melt request was malformed. */ - EXCHANGE_MELT_INVALID_SIGNATURE_BY_EXCHANGE(1306, 0, "The signature returned by the exchange in a melt request was malformed."), - - /** The provided transfer keys do not match up with the original commitment. Information about the original commitment is included in the response. */ - EXCHANGE_REFRESHES_REVEAL_COMMITMENT_VIOLATION(1353, 409, "The provided transfer keys do not match up with the original commitment. Information about the original commitment is included in the response."), - - /** Failed to produce the blinded signatures over the coins to be returned. */ - EXCHANGE_REFRESHES_REVEAL_SIGNING_ERROR(1354, 500, "Failed to produce the blinded signatures over the coins to be returned."), - - /** The exchange is unaware of the refresh session specified in the request. */ - EXCHANGE_REFRESHES_REVEAL_SESSION_UNKNOWN(1355, 404, "The exchange is unaware of the refresh session specified in the request."), - - /** The size of the cut-and-choose dimension of the private transfer keys request does not match #TALER_CNC_KAPPA - 1. */ - EXCHANGE_REFRESHES_REVEAL_CNC_TRANSFER_ARRAY_SIZE_INVALID(1356, 400, "The size of the cut-and-choose dimension of the private transfer keys request does not match #TALER_CNC_KAPPA - 1."), - - /** The number of envelopes given does not match the number of denomination keys given. */ - EXCHANGE_REFRESHES_REVEAL_NEW_DENOMS_ARRAY_SIZE_MISMATCH(1358, 400, "The number of envelopes given does not match the number of denomination keys given."), - - /** The exchange encountered a numeric overflow totaling up the cost for the refresh operation. */ - EXCHANGE_REFRESHES_REVEAL_COST_CALCULATION_OVERFLOW(1359, 500, "The exchange encountered a numeric overflow totaling up the cost for the refresh operation."), - - /** The exchange's cost calculation shows that the melt amount is below the costs of the transaction. */ - EXCHANGE_REFRESHES_REVEAL_AMOUNT_INSUFFICIENT(1360, 400, "The exchange's cost calculation shows that the melt amount is below the costs of the transaction."), - - /** The signature made with the coin over the link data is invalid. */ - EXCHANGE_REFRESHES_REVEAL_LINK_SIGNATURE_INVALID(1361, 403, "The signature made with the coin over the link data is invalid."), - - /** The refresh session hash given to a /refreshes/ handler was malformed. */ - EXCHANGE_REFRESHES_REVEAL_INVALID_RCH(1362, 400, "The refresh session hash given to a /refreshes/ handler was malformed."), - - /** Operation specified invalid for this endpoint. */ - EXCHANGE_REFRESHES_REVEAL_OPERATION_INVALID(1363, 400, "Operation specified invalid for this endpoint."), - - /** The client provided age commitment data, but age restriction is not supported on this server. */ - EXCHANGE_REFRESHES_REVEAL_AGE_RESTRICTION_NOT_SUPPORTED(1364, 400, "The client provided age commitment data, but age restriction is not supported on this server."), - - /** The client provided invalid age commitment data: missing, not an array, or array of invalid size. */ - EXCHANGE_REFRESHES_REVEAL_AGE_RESTRICTION_COMMITMENT_INVALID(1365, 400, "The client provided invalid age commitment data: missing, not an array, or array of invalid size."), - - /** The coin specified in the link request is unknown to the exchange. */ - EXCHANGE_LINK_COIN_UNKNOWN(1400, 404, "The coin specified in the link request is unknown to the exchange."), - - /** The public key of given to a /transfers/ handler was malformed. */ - EXCHANGE_TRANSFERS_GET_WTID_MALFORMED(1450, 400, "The public key of given to a /transfers/ handler was malformed."), - - /** The exchange did not find information about the specified wire transfer identifier in the database. */ - EXCHANGE_TRANSFERS_GET_WTID_NOT_FOUND(1451, 404, "The exchange did not find information about the specified wire transfer identifier in the database."), - - /** The exchange did not find information about the wire transfer fees it charged. */ - EXCHANGE_TRANSFERS_GET_WIRE_FEE_NOT_FOUND(1452, 500, "The exchange did not find information about the wire transfer fees it charged."), - - /** The exchange found a wire fee that was above the total transfer value (and thus could not have been charged). */ - EXCHANGE_TRANSFERS_GET_WIRE_FEE_INCONSISTENT(1453, 500, "The exchange found a wire fee that was above the total transfer value (and thus could not have been charged)."), - - /** The wait target of the URL was not in the set of expected values. */ - EXCHANGE_PURSES_INVALID_WAIT_TARGET(1475, 400, "The wait target of the URL was not in the set of expected values."), - - /** The signature on the purse status returned by the exchange was invalid. */ - EXCHANGE_PURSES_GET_INVALID_SIGNATURE_BY_EXCHANGE(1476, 0, "The signature on the purse status returned by the exchange was invalid."), - - /** The exchange knows literally nothing about the coin we were asked to refund. But without a transaction history, we cannot issue a refund. This is kind-of OK, the owner should just refresh it directly without executing the refund. */ - EXCHANGE_REFUND_COIN_NOT_FOUND(1500, 404, "The exchange knows literally nothing about the coin we were asked to refund. But without a transaction history, we cannot issue a refund. This is kind-of OK, the owner should just refresh it directly without executing the refund."), - - /** We could not process the refund request as the coin's transaction history does not permit the requested refund because then refunds would exceed the deposit amount. The \"history\" in the response proves this. */ - EXCHANGE_REFUND_CONFLICT_DEPOSIT_INSUFFICIENT(1501, 409, "We could not process the refund request as the coin's transaction history does not permit the requested refund because then refunds would exceed the deposit amount. The \"history\" in the response proves this."), - - /** The exchange knows about the coin we were asked to refund, but not about the specific /deposit operation. Hence, we cannot issue a refund (as we do not know if this merchant public key is authorized to do a refund). */ - EXCHANGE_REFUND_DEPOSIT_NOT_FOUND(1502, 404, "The exchange knows about the coin we were asked to refund, but not about the specific /deposit operation. Hence, we cannot issue a refund (as we do not know if this merchant public key is authorized to do a refund)."), - - /** The exchange can no longer refund the customer/coin as the money was already transferred (paid out) to the merchant. (It should be past the refund deadline.) */ - EXCHANGE_REFUND_MERCHANT_ALREADY_PAID(1503, 410, "The exchange can no longer refund the customer/coin as the money was already transferred (paid out) to the merchant. (It should be past the refund deadline.)"), - - /** The refund fee specified for the request is lower than the refund fee charged by the exchange for the given denomination key of the refunded coin. */ - EXCHANGE_REFUND_FEE_TOO_LOW(1504, 400, "The refund fee specified for the request is lower than the refund fee charged by the exchange for the given denomination key of the refunded coin."), - - /** The refunded amount is smaller than the refund fee, which would result in a negative refund. */ - EXCHANGE_REFUND_FEE_ABOVE_AMOUNT(1505, 400, "The refunded amount is smaller than the refund fee, which would result in a negative refund."), - - /** The signature of the merchant is invalid. */ - EXCHANGE_REFUND_MERCHANT_SIGNATURE_INVALID(1506, 403, "The signature of the merchant is invalid."), - - /** Merchant backend failed to create the refund confirmation signature. */ - EXCHANGE_REFUND_MERCHANT_SIGNING_FAILED(1507, 500, "Merchant backend failed to create the refund confirmation signature."), - - /** The signature returned by the exchange in a refund request was malformed. */ - EXCHANGE_REFUND_INVALID_SIGNATURE_BY_EXCHANGE(1508, 0, "The signature returned by the exchange in a refund request was malformed."), - - /** The failure proof returned by the exchange is incorrect. */ - EXCHANGE_REFUND_INVALID_FAILURE_PROOF_BY_EXCHANGE(1509, 0, "The failure proof returned by the exchange is incorrect."), - - /** Conflicting refund granted before with different amount but same refund transaction ID. */ - EXCHANGE_REFUND_INCONSISTENT_AMOUNT(1510, 424, "Conflicting refund granted before with different amount but same refund transaction ID."), - - /** The given coin signature is invalid for the request. */ - EXCHANGE_RECOUP_SIGNATURE_INVALID(1550, 403, "The given coin signature is invalid for the request."), - - /** The exchange could not find the corresponding withdraw operation. The request is denied. */ - EXCHANGE_RECOUP_WITHDRAW_NOT_FOUND(1551, 404, "The exchange could not find the corresponding withdraw operation. The request is denied."), - - /** The coin's remaining balance is zero. The request is denied. */ - EXCHANGE_RECOUP_COIN_BALANCE_ZERO(1552, 403, "The coin's remaining balance is zero. The request is denied."), - - /** The exchange failed to reproduce the coin's blinding. */ - EXCHANGE_RECOUP_BLINDING_FAILED(1553, 500, "The exchange failed to reproduce the coin's blinding."), - - /** The coin's remaining balance is zero. The request is denied. */ - EXCHANGE_RECOUP_COIN_BALANCE_NEGATIVE(1554, 500, "The coin's remaining balance is zero. The request is denied."), - - /** The coin's denomination has not been revoked yet. */ - EXCHANGE_RECOUP_NOT_ELIGIBLE(1555, 404, "The coin's denomination has not been revoked yet."), - - /** The given coin signature is invalid for the request. */ - EXCHANGE_RECOUP_REFRESH_SIGNATURE_INVALID(1575, 403, "The given coin signature is invalid for the request."), - - /** The exchange could not find the corresponding melt operation. The request is denied. */ - EXCHANGE_RECOUP_REFRESH_MELT_NOT_FOUND(1576, 404, "The exchange could not find the corresponding melt operation. The request is denied."), - - /** The exchange failed to reproduce the coin's blinding. */ - EXCHANGE_RECOUP_REFRESH_BLINDING_FAILED(1578, 500, "The exchange failed to reproduce the coin's blinding."), - - /** The coin's denomination has not been revoked yet. */ - EXCHANGE_RECOUP_REFRESH_NOT_ELIGIBLE(1580, 404, "The coin's denomination has not been revoked yet."), - - /** This exchange does not allow clients to request /keys for times other than the current (exchange) time. */ - EXCHANGE_KEYS_TIMETRAVEL_FORBIDDEN(1600, 403, "This exchange does not allow clients to request /keys for times other than the current (exchange) time."), - - /** A signature in the server's response was malformed. */ - EXCHANGE_WIRE_SIGNATURE_INVALID(1650, 0, "A signature in the server's response was malformed."), - - /** No bank accounts are enabled for the exchange. The administrator should enable-account using the taler-exchange-offline tool. */ - EXCHANGE_WIRE_NO_ACCOUNTS_CONFIGURED(1651, 500, "No bank accounts are enabled for the exchange. The administrator should enable-account using the taler-exchange-offline tool."), - - /** The payto:// URI stored in the exchange database for its bank account is malformed. */ - EXCHANGE_WIRE_INVALID_PAYTO_CONFIGURED(1652, 500, "The payto:// URI stored in the exchange database for its bank account is malformed."), - - /** No wire fees are configured for an enabled wire method of the exchange. The administrator must set the wire-fee using the taler-exchange-offline tool. */ - EXCHANGE_WIRE_FEES_NOT_CONFIGURED(1653, 500, "No wire fees are configured for an enabled wire method of the exchange. The administrator must set the wire-fee using the taler-exchange-offline tool."), - - /** This purse was previously created with different meta data. */ - EXCHANGE_RESERVES_PURSE_CREATE_CONFLICTING_META_DATA(1675, 409, "This purse was previously created with different meta data."), - - /** This purse was previously merged with different meta data. */ - EXCHANGE_RESERVES_PURSE_MERGE_CONFLICTING_META_DATA(1676, 409, "This purse was previously merged with different meta data."), - - /** The reserve has insufficient funds to create another purse. */ - EXCHANGE_RESERVES_PURSE_CREATE_INSUFFICIENT_FUNDS(1677, 409, "The reserve has insufficient funds to create another purse."), - - /** The purse fee specified for the request is lower than the purse fee charged by the exchange at this time. */ - EXCHANGE_RESERVES_PURSE_FEE_TOO_LOW(1678, 400, "The purse fee specified for the request is lower than the purse fee charged by the exchange at this time."), - - /** The payment request cannot be deleted anymore, as it either already completed or timed out. */ - EXCHANGE_PURSE_DELETE_ALREADY_DECIDED(1679, 409, "The payment request cannot be deleted anymore, as it either already completed or timed out."), - - /** The signature affirming the purse deletion is invalid. */ - EXCHANGE_PURSE_DELETE_SIGNATURE_INVALID(1680, 403, "The signature affirming the purse deletion is invalid."), - - /** Withdrawal from the reserve requires age restriction to be set. */ - EXCHANGE_RESERVES_AGE_RESTRICTION_REQUIRED(1681, 403, "Withdrawal from the reserve requires age restriction to be set."), - - /** The exchange failed to talk to the process responsible for its private denomination keys or the helpers had no denominations (properly) configured. */ - EXCHANGE_DENOMINATION_HELPER_UNAVAILABLE(1700, 502, "The exchange failed to talk to the process responsible for its private denomination keys or the helpers had no denominations (properly) configured."), - - /** The response from the denomination key helper process was malformed. */ - EXCHANGE_DENOMINATION_HELPER_BUG(1701, 500, "The response from the denomination key helper process was malformed."), - - /** The helper refuses to sign with the key, because it is too early: the validity period has not yet started. */ - EXCHANGE_DENOMINATION_HELPER_TOO_EARLY(1702, 400, "The helper refuses to sign with the key, because it is too early: the validity period has not yet started."), - - /** The signature of the exchange on the reply was invalid. */ - EXCHANGE_PURSE_DEPOSIT_EXCHANGE_SIGNATURE_INVALID(1725, 0, "The signature of the exchange on the reply was invalid."), - - /** The exchange failed to talk to the process responsible for its private signing keys. */ - EXCHANGE_SIGNKEY_HELPER_UNAVAILABLE(1750, 502, "The exchange failed to talk to the process responsible for its private signing keys."), - - /** The response from the online signing key helper process was malformed. */ - EXCHANGE_SIGNKEY_HELPER_BUG(1751, 500, "The response from the online signing key helper process was malformed."), - - /** The helper refuses to sign with the key, because it is too early: the validity period has not yet started. */ - EXCHANGE_SIGNKEY_HELPER_TOO_EARLY(1752, 400, "The helper refuses to sign with the key, because it is too early: the validity period has not yet started."), - - /** The signatures from the master exchange public key are missing, thus the exchange cannot currently sign its API responses. The exchange operator must use taler-exchange-offline to sign the current key material. */ - EXCHANGE_SIGNKEY_HELPER_OFFLINE_MISSING(1753, 500, "The signatures from the master exchange public key are missing, thus the exchange cannot currently sign its API responses. The exchange operator must use taler-exchange-offline to sign the current key material."), - - /** The purse expiration time is in the past at the time of its creation. */ - EXCHANGE_RESERVES_PURSE_EXPIRATION_BEFORE_NOW(1775, 400, "The purse expiration time is in the past at the time of its creation."), - - /** The purse expiration time is set to never, which is not allowed. */ - EXCHANGE_RESERVES_PURSE_EXPIRATION_IS_NEVER(1776, 400, "The purse expiration time is set to never, which is not allowed."), - - /** The signature affirming the merge of the purse is invalid. */ - EXCHANGE_RESERVES_PURSE_MERGE_SIGNATURE_INVALID(1777, 403, "The signature affirming the merge of the purse is invalid."), - - /** The signature by the reserve affirming the merge is invalid. */ - EXCHANGE_RESERVES_RESERVE_MERGE_SIGNATURE_INVALID(1778, 403, "The signature by the reserve affirming the merge is invalid."), - - /** The signature by the reserve affirming the open operation is invalid. */ - EXCHANGE_RESERVES_OPEN_BAD_SIGNATURE(1785, 403, "The signature by the reserve affirming the open operation is invalid."), - - /** The signature by the reserve affirming the close operation is invalid. */ - EXCHANGE_RESERVES_CLOSE_BAD_SIGNATURE(1786, 403, "The signature by the reserve affirming the close operation is invalid."), - - /** The signature by the reserve affirming the attestion request is invalid. */ - EXCHANGE_RESERVES_ATTEST_BAD_SIGNATURE(1787, 403, "The signature by the reserve affirming the attestion request is invalid."), - - /** The exchange does not know an origin account to which the remaining reserve balance could be wired to, and the wallet failed to provide one. */ - EXCHANGE_RESERVES_CLOSE_NO_TARGET_ACCOUNT(1788, 409, "The exchange does not know an origin account to which the remaining reserve balance could be wired to, and the wallet failed to provide one."), - - /** The reserve balance is insufficient to pay for the open operation. */ - EXCHANGE_RESERVES_OPEN_INSUFFICIENT_FUNDS(1789, 409, "The reserve balance is insufficient to pay for the open operation."), - - /** The auditor that was supposed to be disabled is unknown to this exchange. */ - EXCHANGE_MANAGEMENT_AUDITOR_NOT_FOUND(1800, 404, "The auditor that was supposed to be disabled is unknown to this exchange."), - - /** The exchange has a more recently signed conflicting instruction and is thus refusing the current change (replay detected). */ - EXCHANGE_MANAGEMENT_AUDITOR_MORE_RECENT_PRESENT(1801, 409, "The exchange has a more recently signed conflicting instruction and is thus refusing the current change (replay detected)."), - - /** The signature to add or enable the auditor does not validate. */ - EXCHANGE_MANAGEMENT_AUDITOR_ADD_SIGNATURE_INVALID(1802, 403, "The signature to add or enable the auditor does not validate."), - - /** The signature to disable the auditor does not validate. */ - EXCHANGE_MANAGEMENT_AUDITOR_DEL_SIGNATURE_INVALID(1803, 403, "The signature to disable the auditor does not validate."), - - /** The signature to revoke the denomination does not validate. */ - EXCHANGE_MANAGEMENT_DENOMINATION_REVOKE_SIGNATURE_INVALID(1804, 403, "The signature to revoke the denomination does not validate."), - - /** The signature to revoke the online signing key does not validate. */ - EXCHANGE_MANAGEMENT_SIGNKEY_REVOKE_SIGNATURE_INVALID(1805, 403, "The signature to revoke the online signing key does not validate."), - - /** The exchange has a more recently signed conflicting instruction and is thus refusing the current change (replay detected). */ - EXCHANGE_MANAGEMENT_WIRE_MORE_RECENT_PRESENT(1806, 409, "The exchange has a more recently signed conflicting instruction and is thus refusing the current change (replay detected)."), - - /** The signingkey specified is unknown to the exchange. */ - EXCHANGE_MANAGEMENT_KEYS_SIGNKEY_UNKNOWN(1807, 404, "The signingkey specified is unknown to the exchange."), - - /** The signature to publish wire account does not validate. */ - EXCHANGE_MANAGEMENT_WIRE_DETAILS_SIGNATURE_INVALID(1808, 403, "The signature to publish wire account does not validate."), - - /** The signature to add the wire account does not validate. */ - EXCHANGE_MANAGEMENT_WIRE_ADD_SIGNATURE_INVALID(1809, 403, "The signature to add the wire account does not validate."), - - /** The signature to disable the wire account does not validate. */ - EXCHANGE_MANAGEMENT_WIRE_DEL_SIGNATURE_INVALID(1810, 403, "The signature to disable the wire account does not validate."), - - /** The wire account to be disabled is unknown to the exchange. */ - EXCHANGE_MANAGEMENT_WIRE_NOT_FOUND(1811, 404, "The wire account to be disabled is unknown to the exchange."), - - /** The signature to affirm wire fees does not validate. */ - EXCHANGE_MANAGEMENT_WIRE_FEE_SIGNATURE_INVALID(1812, 403, "The signature to affirm wire fees does not validate."), - - /** The signature conflicts with a previous signature affirming different fees. */ - EXCHANGE_MANAGEMENT_WIRE_FEE_MISMATCH(1813, 409, "The signature conflicts with a previous signature affirming different fees."), - - /** The signature affirming the denomination key is invalid. */ - EXCHANGE_MANAGEMENT_KEYS_DENOMKEY_ADD_SIGNATURE_INVALID(1814, 403, "The signature affirming the denomination key is invalid."), - - /** The signature affirming the signing key is invalid. */ - EXCHANGE_MANAGEMENT_KEYS_SIGNKEY_ADD_SIGNATURE_INVALID(1815, 403, "The signature affirming the signing key is invalid."), - - /** The signature conflicts with a previous signature affirming different fees. */ - EXCHANGE_MANAGEMENT_GLOBAL_FEE_MISMATCH(1816, 409, "The signature conflicts with a previous signature affirming different fees."), - - /** The signature affirming the fee structure is invalid. */ - EXCHANGE_MANAGEMENT_GLOBAL_FEE_SIGNATURE_INVALID(1817, 403, "The signature affirming the fee structure is invalid."), - - /** The signature affirming the profit drain is invalid. */ - EXCHANGE_MANAGEMENT_DRAIN_PROFITS_SIGNATURE_INVALID(1818, 403, "The signature affirming the profit drain is invalid."), - - /** The signature affirming the AML decision is invalid. */ - EXCHANGE_AML_DECISION_ADD_SIGNATURE_INVALID(1825, 403, "The signature affirming the AML decision is invalid."), - - /** The AML officer specified is not allowed to make AML decisions right now. */ - EXCHANGE_AML_DECISION_INVALID_OFFICER(1826, 403, "The AML officer specified is not allowed to make AML decisions right now."), - - /** There is a more recent AML decision on file. The decision was rejected as timestamps of AML decisions must be monotonically increasing. */ - EXCHANGE_AML_DECISION_MORE_RECENT_PRESENT(1827, 409, "There is a more recent AML decision on file. The decision was rejected as timestamps of AML decisions must be monotonically increasing."), - - /** There AML decision would impose an AML check of a type that is not provided by any KYC provider known to the exchange. */ - EXCHANGE_AML_DECISION_UNKNOWN_CHECK(1828, 400, "There AML decision would impose an AML check of a type that is not provided by any KYC provider known to the exchange."), - - /** The signature affirming the change in the AML officer status is invalid. */ - EXCHANGE_MANAGEMENT_UPDATE_AML_OFFICER_SIGNATURE_INVALID(1830, 403, "The signature affirming the change in the AML officer status is invalid."), - - /** A more recent decision about the AML officer status is known to the exchange. */ - EXCHANGE_MANAGEMENT_AML_OFFICERS_MORE_RECENT_PRESENT(1831, 409, "A more recent decision about the AML officer status is known to the exchange."), - - /** The exchange already has this denomination key configured, but with different meta data. This should not be possible, contact the developers for support. */ - EXCHANGE_MANAGEMENT_CONFLICTING_DENOMINATION_META_DATA(1832, 409, "The exchange already has this denomination key configured, but with different meta data. This should not be possible, contact the developers for support."), - - /** The exchange already has this signing key configured, but with different meta data. This should not be possible, contact the developers for support. */ - EXCHANGE_MANAGEMENT_CONFLICTING_SIGNKEY_META_DATA(1833, 409, "The exchange already has this signing key configured, but with different meta data. This should not be possible, contact the developers for support."), - - /** The purse was previously created with different meta data. */ - EXCHANGE_PURSE_CREATE_CONFLICTING_META_DATA(1850, 409, "The purse was previously created with different meta data."), - - /** The purse was previously created with a different contract. */ - EXCHANGE_PURSE_CREATE_CONFLICTING_CONTRACT_STORED(1851, 409, "The purse was previously created with a different contract."), - - /** A coin signature for a deposit into the purse is invalid. */ - EXCHANGE_PURSE_CREATE_COIN_SIGNATURE_INVALID(1852, 403, "A coin signature for a deposit into the purse is invalid."), - - /** The purse expiration time is in the past. */ - EXCHANGE_PURSE_CREATE_EXPIRATION_BEFORE_NOW(1853, 400, "The purse expiration time is in the past."), - - /** The purse expiration time is \"never\". */ - EXCHANGE_PURSE_CREATE_EXPIRATION_IS_NEVER(1854, 400, "The purse expiration time is \"never\"."), - - /** The purse signature over the purse meta data is invalid. */ - EXCHANGE_PURSE_CREATE_SIGNATURE_INVALID(1855, 403, "The purse signature over the purse meta data is invalid."), - - /** The signature over the encrypted contract is invalid. */ - EXCHANGE_PURSE_ECONTRACT_SIGNATURE_INVALID(1856, 403, "The signature over the encrypted contract is invalid."), - - /** The signature from the exchange over the confirmation is invalid. */ - EXCHANGE_PURSE_CREATE_EXCHANGE_SIGNATURE_INVALID(1857, 0, "The signature from the exchange over the confirmation is invalid."), - - /** The coin was previously deposited with different meta data. */ - EXCHANGE_PURSE_DEPOSIT_CONFLICTING_META_DATA(1858, 409, "The coin was previously deposited with different meta data."), - - /** The encrypted contract was previously uploaded with different meta data. */ - EXCHANGE_PURSE_ECONTRACT_CONFLICTING_META_DATA(1859, 409, "The encrypted contract was previously uploaded with different meta data."), - - /** The deposited amount is less than the purse fee. */ - EXCHANGE_CREATE_PURSE_NEGATIVE_VALUE_AFTER_FEE(1860, 400, "The deposited amount is less than the purse fee."), - - /** The signature using the merge key is invalid. */ - EXCHANGE_PURSE_MERGE_INVALID_MERGE_SIGNATURE(1876, 403, "The signature using the merge key is invalid."), - - /** The signature using the reserve key is invalid. */ - EXCHANGE_PURSE_MERGE_INVALID_RESERVE_SIGNATURE(1877, 403, "The signature using the reserve key is invalid."), - - /** The targeted purse is not yet full and thus cannot be merged. Retrying the request later may succeed. */ - EXCHANGE_PURSE_NOT_FULL(1878, 409, "The targeted purse is not yet full and thus cannot be merged. Retrying the request later may succeed."), - - /** The signature from the exchange over the confirmation is invalid. */ - EXCHANGE_PURSE_MERGE_EXCHANGE_SIGNATURE_INVALID(1879, 0, "The signature from the exchange over the confirmation is invalid."), - - /** The exchange of the target account is not a partner of this exchange. */ - EXCHANGE_MERGE_PURSE_PARTNER_UNKNOWN(1880, 404, "The exchange of the target account is not a partner of this exchange."), - - /** The signature affirming the new partner is invalid. */ - EXCHANGE_MANAGEMENT_ADD_PARTNER_SIGNATURE_INVALID(1890, 403, "The signature affirming the new partner is invalid."), - - /** Conflicting data for the partner already exists with the exchange. */ - EXCHANGE_MANAGEMENT_ADD_PARTNER_DATA_CONFLICT(1891, 409, "Conflicting data for the partner already exists with the exchange."), - - /** The auditor signature over the denomination meta data is invalid. */ - EXCHANGE_AUDITORS_AUDITOR_SIGNATURE_INVALID(1900, 403, "The auditor signature over the denomination meta data is invalid."), - - /** The auditor that was specified is unknown to this exchange. */ - EXCHANGE_AUDITORS_AUDITOR_UNKNOWN(1901, 412, "The auditor that was specified is unknown to this exchange."), - - /** The auditor that was specified is no longer used by this exchange. */ - EXCHANGE_AUDITORS_AUDITOR_INACTIVE(1902, 410, "The auditor that was specified is no longer used by this exchange."), - - /** The exchange tried to run an AML program, but that program did not terminate on time. Contact the exchange operator to address the AML program bug or performance issue. If it is not a performance issue, the timeout might have to be increased (requires changes to the source code). */ - EXCHANGE_KYC_GENERIC_AML_PROGRAM_TIMEOUT(1918, 500, "The exchange tried to run an AML program, but that program did not terminate on time. Contact the exchange operator to address the AML program bug or performance issue. If it is not a performance issue, the timeout might have to be increased (requires changes to the source code)."), - - /** The KYC info access token is not recognized. Hence the request was denied. */ - EXCHANGE_KYC_INFO_AUTHORIZATION_FAILED(1919, 403, "The KYC info access token is not recognized. Hence the request was denied."), - - /** The exchange got stuck in a long series of (likely recursive) KYC rules without user-inputs that did not result in a timely conclusion. This is a configuration failure. Please contact the administrator. */ - EXCHANGE_KYC_RECURSIVE_RULE_DETECTED(1920, 500, "The exchange got stuck in a long series of (likely recursive) KYC rules without user-inputs that did not result in a timely conclusion. This is a configuration failure. Please contact the administrator."), - - /** The submitted KYC data lacks an attribute that is required by the KYC form. Please submit the complete form. */ - EXCHANGE_KYC_AML_FORM_INCOMPLETE(1921, 400, "The submitted KYC data lacks an attribute that is required by the KYC form. Please submit the complete form."), - - /** The request requires an AML program which is no longer configured at the exchange. Contact the exchange operator to address the configuration issue. */ - EXCHANGE_KYC_GENERIC_AML_PROGRAM_GONE(1922, 500, "The request requires an AML program which is no longer configured at the exchange. Contact the exchange operator to address the configuration issue."), - - /** The given check is not of type 'form' and thus using this handler for form submission is incorrect. */ - EXCHANGE_KYC_NOT_A_FORM(1923, 400, "The given check is not of type 'form' and thus using this handler for form submission is incorrect."), - - /** The request requires a check which is no longer configured at the exchange. Contact the exchange operator to address the configuration issue. */ - EXCHANGE_KYC_GENERIC_CHECK_GONE(1924, 500, "The request requires a check which is no longer configured at the exchange. Contact the exchange operator to address the configuration issue."), - - /** The signature affirming the wallet's KYC request was invalid. */ - EXCHANGE_KYC_WALLET_SIGNATURE_INVALID(1925, 403, "The signature affirming the wallet's KYC request was invalid."), - - /** The exchange received an unexpected malformed response from its KYC backend. */ - EXCHANGE_KYC_PROOF_BACKEND_INVALID_RESPONSE(1926, 502, "The exchange received an unexpected malformed response from its KYC backend."), - - /** The backend signaled an unexpected failure. */ - EXCHANGE_KYC_PROOF_BACKEND_ERROR(1927, 502, "The backend signaled an unexpected failure."), - - /** The backend signaled an authorization failure. */ - EXCHANGE_KYC_PROOF_BACKEND_AUTHORIZATION_FAILED(1928, 403, "The backend signaled an authorization failure."), - - /** The exchange is unaware of having made an the authorization request. */ - EXCHANGE_KYC_PROOF_REQUEST_UNKNOWN(1929, 404, "The exchange is unaware of having made an the authorization request."), - - /** The KYC authorization signature was invalid. Hence the request was denied. */ - EXCHANGE_KYC_CHECK_AUTHORIZATION_FAILED(1930, 403, "The KYC authorization signature was invalid. Hence the request was denied."), - - /** The request used a logic specifier that is not known to the exchange. */ - EXCHANGE_KYC_GENERIC_LOGIC_UNKNOWN(1931, 404, "The request used a logic specifier that is not known to the exchange."), - - /** The request requires a logic which is no longer configured at the exchange. */ - EXCHANGE_KYC_GENERIC_LOGIC_GONE(1932, 500, "The request requires a logic which is no longer configured at the exchange."), - - /** The logic plugin had a bug in its interaction with the KYC provider. */ - EXCHANGE_KYC_GENERIC_LOGIC_BUG(1933, 500, "The logic plugin had a bug in its interaction with the KYC provider."), - - /** The exchange could not process the request with its KYC provider because the provider refused access to the service. This indicates some configuration issue at the Taler exchange operator. */ - EXCHANGE_KYC_GENERIC_PROVIDER_ACCESS_REFUSED(1934, 511, "The exchange could not process the request with its KYC provider because the provider refused access to the service. This indicates some configuration issue at the Taler exchange operator."), - - /** There was a timeout in the interaction between the exchange and the KYC provider. The most likely cause is some networking problem. Trying again later might succeed. */ - EXCHANGE_KYC_GENERIC_PROVIDER_TIMEOUT(1935, 504, "There was a timeout in the interaction between the exchange and the KYC provider. The most likely cause is some networking problem. Trying again later might succeed."), - - /** The KYC provider responded with a status that was completely unexpected by the KYC logic of the exchange. */ - EXCHANGE_KYC_GENERIC_PROVIDER_UNEXPECTED_REPLY(1936, 502, "The KYC provider responded with a status that was completely unexpected by the KYC logic of the exchange."), - - /** The rate limit of the exchange at the KYC provider has been exceeded. Trying much later might work. */ - EXCHANGE_KYC_GENERIC_PROVIDER_RATE_LIMIT_EXCEEDED(1937, 503, "The rate limit of the exchange at the KYC provider has been exceeded. Trying much later might work."), - - /** The request to the webhook lacked proper authorization or authentication data. */ - EXCHANGE_KYC_WEBHOOK_UNAUTHORIZED(1938, 401, "The request to the webhook lacked proper authorization or authentication data."), - - /** The exchange is unaware of the requested payto URI with respect to the KYC status. */ - EXCHANGE_KYC_CHECK_REQUEST_UNKNOWN(1939, 404, "The exchange is unaware of the requested payto URI with respect to the KYC status."), - - /** The exchange has no account public key to check the KYC authorization signature against. Hence the request was denied. The user should do a wire transfer to the exchange with the KYC authorization key in the subject. */ - EXCHANGE_KYC_CHECK_AUTHORIZATION_KEY_UNKNOWN(1940, 409, "The exchange has no account public key to check the KYC authorization signature against. Hence the request was denied. The user should do a wire transfer to the exchange with the KYC authorization key in the subject."), - - /** The form has been previously uploaded, and may only be filed once. The user should be redirected to their main KYC page and see if any other steps need to be taken. */ - EXCHANGE_KYC_FORM_ALREADY_UPLOADED(1941, 409, "The form has been previously uploaded, and may only be filed once. The user should be redirected to their main KYC page and see if any other steps need to be taken."), - - /** The internal state of the exchange specifying KYC measures is malformed. Please contact technical support. */ - EXCHANGE_KYC_MEASURES_MALFORMED(1942, 500, "The internal state of the exchange specifying KYC measures is malformed. Please contact technical support."), - - /** The specified index does not refer to a valid KYC measure. Please check the URL. */ - EXCHANGE_KYC_MEASURE_INDEX_INVALID(1943, 404, "The specified index does not refer to a valid KYC measure. Please check the URL."), - - /** The operation is not supported by the selected KYC logic. This is either caused by a configuration change or some invalid use of the API. Please contact technical support. */ - EXCHANGE_KYC_INVALID_LOGIC_TO_CHECK(1944, 409, "The operation is not supported by the selected KYC logic. This is either caused by a configuration change or some invalid use of the API. Please contact technical support."), - - /** The AML program failed. This is either caused by a configuration change or a bug. Please contact technical support. */ - EXCHANGE_KYC_AML_PROGRAM_FAILURE(1945, 500, "The AML program failed. This is either caused by a configuration change or a bug. Please contact technical support."), - - /** The AML program returned a malformed result. This is a bug. Please contact technical support. */ - EXCHANGE_KYC_AML_PROGRAM_MALFORMED_RESULT(1946, 500, "The AML program returned a malformed result. This is a bug. Please contact technical support."), - - /** The response from the KYC provider lacked required attributes. Please contact technical support. */ - EXCHANGE_KYC_GENERIC_PROVIDER_INCOMPLETE_REPLY(1947, 502, "The response from the KYC provider lacked required attributes. Please contact technical support."), - - /** The context of the KYC check lacked required fields. This is a bug. Please contact technical support. */ - EXCHANGE_KYC_GENERIC_PROVIDER_INCOMPLETE_CONTEXT(1948, 500, "The context of the KYC check lacked required fields. This is a bug. Please contact technical support."), - - /** The logic plugin had a bug in its AML processing. This is a bug. Please contact technical support. */ - EXCHANGE_KYC_GENERIC_AML_LOGIC_BUG(1949, 500, "The logic plugin had a bug in its AML processing. This is a bug. Please contact technical support."), - - /** The exchange does not know a contract under the given contract public key. */ - EXCHANGE_CONTRACTS_UNKNOWN(1950, 404, "The exchange does not know a contract under the given contract public key."), - - /** The URL does not encode a valid exchange public key in its path. */ - EXCHANGE_CONTRACTS_INVALID_CONTRACT_PUB(1951, 400, "The URL does not encode a valid exchange public key in its path."), - - /** The returned encrypted contract did not decrypt. */ - EXCHANGE_CONTRACTS_DECRYPTION_FAILED(1952, 0, "The returned encrypted contract did not decrypt."), - - /** The signature on the encrypted contract did not validate. */ - EXCHANGE_CONTRACTS_SIGNATURE_INVALID(1953, 0, "The signature on the encrypted contract did not validate."), - - /** The decrypted contract was malformed. */ - EXCHANGE_CONTRACTS_DECODING_FAILED(1954, 0, "The decrypted contract was malformed."), - - /** A coin signature for a deposit into the purse is invalid. */ - EXCHANGE_PURSE_DEPOSIT_COIN_SIGNATURE_INVALID(1975, 403, "A coin signature for a deposit into the purse is invalid."), - - /** It is too late to deposit coins into the purse. */ - EXCHANGE_PURSE_DEPOSIT_DECIDED_ALREADY(1976, 410, "It is too late to deposit coins into the purse."), - - /** The exchange is currently processing the KYC status and is not able to return a response yet. */ - EXCHANGE_KYC_INFO_BUSY(1977, 202, "The exchange is currently processing the KYC status and is not able to return a response yet."), - - /** TOTP key is not valid. */ - EXCHANGE_TOTP_KEY_INVALID(1980, 0, "TOTP key is not valid."), - - /** The backend could not find the merchant instance specified in the request. */ - MERCHANT_GENERIC_INSTANCE_UNKNOWN(2000, 404, "The backend could not find the merchant instance specified in the request."), - - /** The start and end-times in the wire fee structure leave a hole. This is not allowed. */ - MERCHANT_GENERIC_HOLE_IN_WIRE_FEE_STRUCTURE(2001, 0, "The start and end-times in the wire fee structure leave a hole. This is not allowed."), - - /** The master key of the exchange does not match the one configured for this merchant. As a result, we refuse to do business with this exchange. The administrator should check if they configured the exchange correctly in the merchant backend. */ - MERCHANT_GENERIC_EXCHANGE_MASTER_KEY_MISMATCH(2002, 502, "The master key of the exchange does not match the one configured for this merchant. As a result, we refuse to do business with this exchange. The administrator should check if they configured the exchange correctly in the merchant backend."), - - /** The product category is not known to the backend. */ - MERCHANT_GENERIC_CATEGORY_UNKNOWN(2003, 404, "The product category is not known to the backend."), - - /** The unit referenced in the request is not known to the backend. */ - MERCHANT_GENERIC_UNIT_UNKNOWN(2004, 404, "The unit referenced in the request is not known to the backend."), - - /** The proposal is not known to the backend. */ - MERCHANT_GENERIC_ORDER_UNKNOWN(2005, 404, "The proposal is not known to the backend."), - - /** The order provided to the backend could not be completed, because a product to be completed via inventory data is not actually in our inventory. */ - MERCHANT_GENERIC_PRODUCT_UNKNOWN(2006, 404, "The order provided to the backend could not be completed, because a product to be completed via inventory data is not actually in our inventory."), - - /** The reward ID is unknown. This could happen if the reward has expired. */ - MERCHANT_GENERIC_REWARD_ID_UNKNOWN(2007, 404, "The reward ID is unknown. This could happen if the reward has expired."), - - /** The contract obtained from the merchant backend was malformed. */ - MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID(2008, 500, "The contract obtained from the merchant backend was malformed."), - - /** The order we found does not match the provided contract hash. */ - MERCHANT_GENERIC_CONTRACT_HASH_DOES_NOT_MATCH_ORDER(2009, 403, "The order we found does not match the provided contract hash."), - - /** The exchange failed to provide a valid response to the merchant's /keys request. */ - MERCHANT_GENERIC_EXCHANGE_KEYS_FAILURE(2010, 502, "The exchange failed to provide a valid response to the merchant's /keys request."), - - /** The exchange failed to respond to the merchant on time. */ - MERCHANT_GENERIC_EXCHANGE_TIMEOUT(2011, 504, "The exchange failed to respond to the merchant on time."), - - /** The merchant failed to talk to the exchange. */ - MERCHANT_GENERIC_EXCHANGE_CONNECT_FAILURE(2012, 500, "The merchant failed to talk to the exchange."), - - /** The exchange returned a maformed response. */ - MERCHANT_GENERIC_EXCHANGE_REPLY_MALFORMED(2013, 502, "The exchange returned a maformed response."), - - /** The exchange returned an unexpected response status. */ - MERCHANT_GENERIC_EXCHANGE_UNEXPECTED_STATUS(2014, 502, "The exchange returned an unexpected response status."), - - /** The merchant refused the request due to lack of authorization. */ - MERCHANT_GENERIC_UNAUTHORIZED(2015, 401, "The merchant refused the request due to lack of authorization."), - - /** The merchant instance specified in the request was deleted. */ - MERCHANT_GENERIC_INSTANCE_DELETED(2016, 404, "The merchant instance specified in the request was deleted."), - - /** The backend could not find the inbound wire transfer specified in the request. */ - MERCHANT_GENERIC_TRANSFER_UNKNOWN(2017, 404, "The backend could not find the inbound wire transfer specified in the request."), - - /** The backend could not find the template(id) because it is not exist. */ - MERCHANT_GENERIC_TEMPLATE_UNKNOWN(2018, 404, "The backend could not find the template(id) because it is not exist."), - - /** The backend could not find the webhook(id) because it is not exist. */ - MERCHANT_GENERIC_WEBHOOK_UNKNOWN(2019, 404, "The backend could not find the webhook(id) because it is not exist."), - - /** The backend could not find the webhook(serial) because it is not exist. */ - MERCHANT_GENERIC_PENDING_WEBHOOK_UNKNOWN(2020, 404, "The backend could not find the webhook(serial) because it is not exist."), - - /** The backend could not find the OTP device(id) because it is not exist. */ - MERCHANT_GENERIC_OTP_DEVICE_UNKNOWN(2021, 404, "The backend could not find the OTP device(id) because it is not exist."), - - /** The account is not known to the backend. */ - MERCHANT_GENERIC_ACCOUNT_UNKNOWN(2022, 404, "The account is not known to the backend."), - - /** The wire hash was malformed. */ - MERCHANT_GENERIC_H_WIRE_MALFORMED(2023, 400, "The wire hash was malformed."), - - /** The currency specified in the operation does not work with the current state of the given resource. */ - MERCHANT_GENERIC_CURRENCY_MISMATCH(2024, 409, "The currency specified in the operation does not work with the current state of the given resource."), - - /** The exchange specified in the operation is not trusted by this exchange. The client should limit its operation to exchanges enabled by the merchant, or ask the merchant to enable additional exchanges in the configuration. */ - MERCHANT_GENERIC_EXCHANGE_UNTRUSTED(2025, 400, "The exchange specified in the operation is not trusted by this exchange. The client should limit its operation to exchanges enabled by the merchant, or ask the merchant to enable additional exchanges in the configuration."), - - /** The token family is not known to the backend. */ - MERCHANT_GENERIC_TOKEN_FAMILY_UNKNOWN(2026, 404, "The token family is not known to the backend."), - - /** The token family key is not known to the backend. Check the local system time on the client, maybe an expired (or not yet valid) token was used. */ - MERCHANT_GENERIC_TOKEN_KEY_UNKNOWN(2027, 404, "The token family key is not known to the backend. Check the local system time on the client, maybe an expired (or not yet valid) token was used."), - - /** The merchant backend is not configured to support the DONAU protocol. */ - MERCHANT_GENERIC_DONAU_NOT_CONFIGURED(2028, 501, "The merchant backend is not configured to support the DONAU protocol."), - - /** The public signing key given in the exchange response is not in the current keys response. It is possible that the operation will succeed later after the merchant has downloaded an updated keys response. */ - MERCHANT_EXCHANGE_SIGN_PUB_UNKNOWN(2029, 0, "The public signing key given in the exchange response is not in the current keys response. It is possible that the operation will succeed later after the merchant has downloaded an updated keys response."), - - /** The merchant backend does not support the requested feature. */ - MERCHANT_GENERIC_FEATURE_NOT_AVAILABLE(2030, 501, "The merchant backend does not support the requested feature."), - - /** This operation requires multi-factor authorization and the respective instance does not have a sufficient number of factors that could be validated configured. You need to ask the system administrator to perform this operation. */ - MERCHANT_GENERIC_MFA_MISSING(2031, 403, "This operation requires multi-factor authorization and the respective instance does not have a sufficient number of factors that could be validated configured. You need to ask the system administrator to perform this operation."), - - /** A donation authority (Donau) provided an invalid response. This should be analyzed by the administrator. Trying again later may help. */ - MERCHANT_GENERIC_DONAU_INVALID_RESPONSE(2032, 502, "A donation authority (Donau) provided an invalid response. This should be analyzed by the administrator. Trying again later may help."), - - /** The unit referenced in the request is builtin and cannot be modified or deleted. */ - MERCHANT_GENERIC_UNIT_BUILTIN(2033, 409, "The unit referenced in the request is builtin and cannot be modified or deleted."), - - /** The report ID provided to the backend is not known to the backend. */ - MERCHANT_GENERIC_REPORT_UNKNOWN(2034, 404, "The report ID provided to the backend is not known to the backend."), - - /** The report ID provided to the backend is not known to the backend. */ - MERCHANT_GENERIC_REPORT_GENERATOR_UNCONFIGURED(2035, 501, "The report ID provided to the backend is not known to the backend."), - - /** The product group ID provided to the backend is not known to the backend. */ - MERCHANT_GENERIC_PRODUCT_GROUP_UNKNOWN(2036, 404, "The product group ID provided to the backend is not known to the backend."), - - /** The money pod ID provided to the backend is not known to the backend. */ - MERCHANT_GENERIC_MONEY_POT_UNKNOWN(2037, 404, "The money pod ID provided to the backend is not known to the backend."), - - /** The session ID provided to the backend is not known to the backend. */ - MERCHANT_GENERIC_SESSION_UNKNOWN(2038, 404, "The session ID provided to the backend is not known to the backend."), - - /** The merchant does not have a charity associated with the selected Donau. As a result, it cannot generate the requested donation receipt. This could happen if the charity was removed from the backend between order creation and payment. */ - MERCHANT_GENERIC_DONAU_CHARITY_UNKNOWN(2039, 404, "The merchant does not have a charity associated with the selected Donau. As a result, it cannot generate the requested donation receipt. This could happen if the charity was removed from the backend between order creation and payment."), - - /** The merchant does not expect any transfer with the given ID and can thus not return any details about it. */ - MERCHANT_GENERIC_EXPECTED_TRANSFER_UNKNOWN(2040, 404, "The merchant does not expect any transfer with the given ID and can thus not return any details about it."), - - /** The Donau is not known to the backend. */ - MERCHANT_GENERIC_DONAU_UNKNOWN(2041, 404, "The Donau is not known to the backend."), - - /** The access token is not known to the backend. */ - MERCHANT_GENERIC_ACCESS_TOKEN_UNKNOWN(2042, 404, "The access token is not known to the backend."), - - /** One of the binaries needed to generate the PDF is not installed. If this feature is required, the system administrator should make sure Typst and pdftk are both installed. */ - MERCHANT_GENERIC_NO_TYPST_OR_PDFTK(2048, 501, "One of the binaries needed to generate the PDF is not installed. If this feature is required, the system administrator should make sure Typst and pdftk are both installed."), - - /** The exchange failed to provide a valid answer to the tracking request, thus those details are not in the response. */ - MERCHANT_GET_ORDERS_EXCHANGE_TRACKING_FAILURE(2100, 200, "The exchange failed to provide a valid answer to the tracking request, thus those details are not in the response."), - - /** The merchant backend failed to construct the request for tracking to the exchange, thus tracking details are not in the response. */ - MERCHANT_GET_ORDERS_ID_EXCHANGE_REQUEST_FAILURE(2103, 500, "The merchant backend failed to construct the request for tracking to the exchange, thus tracking details are not in the response."), - - /** The merchant backend failed trying to contact the exchange for tracking details, thus those details are not in the response. */ - MERCHANT_GET_ORDERS_ID_EXCHANGE_LOOKUP_START_FAILURE(2104, 500, "The merchant backend failed trying to contact the exchange for tracking details, thus those details are not in the response."), - - /** The claim token used to authenticate the client is invalid for this order. */ - MERCHANT_GET_ORDERS_ID_INVALID_TOKEN(2105, 403, "The claim token used to authenticate the client is invalid for this order."), - - /** The contract terms hash used to authenticate the client is invalid for this order. */ - MERCHANT_GET_ORDERS_ID_INVALID_CONTRACT_HASH(2106, 403, "The contract terms hash used to authenticate the client is invalid for this order."), - - /** The contract terms version is not understood by the merchant backend. Most likely the merchant backend was downgraded to a version incompatible with the content of the database. */ - MERCHANT_GET_ORDERS_ID_INVALID_CONTRACT_VERSION(2107, 500, "The contract terms version is not understood by the merchant backend. Most likely the merchant backend was downgraded to a version incompatible with the content of the database."), - - /** The provided TAN code is invalid for this challenge. */ - MERCHANT_TAN_CHALLENGE_FAILED(2125, 409, "The provided TAN code is invalid for this challenge."), - - /** The backend is not aware of the specified MFA challenge. */ - MERCHANT_TAN_CHALLENGE_UNKNOWN(2126, 404, "The backend is not aware of the specified MFA challenge."), - - /** There have been too many attempts to solve the challenge. A new TAN must be requested. */ - MERCHANT_TAN_TOO_MANY_ATTEMPTS(2127, 429, "There have been too many attempts to solve the challenge. A new TAN must be requested."), - - /** The backend failed to launch a helper process required for the multi-factor authentication step. The backend operator should check the logs and fix the Taler merchant backend configuration. */ - MERCHANT_TAN_MFA_HELPER_EXEC_FAILED(2128, 502, "The backend failed to launch a helper process required for the multi-factor authentication step. The backend operator should check the logs and fix the Taler merchant backend configuration."), - - /** The challenge was already solved. Thus, we refuse to send it again. */ - MERCHANT_TAN_CHALLENGE_SOLVED(2129, 410, "The challenge was already solved. Thus, we refuse to send it again."), - - /** It is too early to request another transmission of the challenge. The client should wait and see if they received the previous challenge. */ - MERCHANT_TAN_TOO_EARLY(2130, 429, "It is too early to request another transmission of the challenge. The client should wait and see if they received the previous challenge."), - - /** There have been too many attempts to solve MFA. The client may attempt again in the future. */ - MERCHANT_MFA_FORBIDDEN(2131, 403, "There have been too many attempts to solve MFA. The client may attempt again in the future."), - - /** The exchange responded saying that funds were insufficient (for example, due to double-spending). */ - MERCHANT_POST_ORDERS_ID_PAY_INSUFFICIENT_FUNDS(2150, 409, "The exchange responded saying that funds were insufficient (for example, due to double-spending)."), - - /** The denomination key used for payment is not listed among the denomination keys of the exchange. */ - MERCHANT_POST_ORDERS_ID_PAY_DENOMINATION_KEY_NOT_FOUND(2151, 400, "The denomination key used for payment is not listed among the denomination keys of the exchange."), - - /** The denomination key used for payment is not audited by an auditor approved by the merchant. */ - MERCHANT_POST_ORDERS_ID_PAY_DENOMINATION_KEY_AUDITOR_FAILURE(2152, 400, "The denomination key used for payment is not audited by an auditor approved by the merchant."), - - /** There was an integer overflow totaling up the amounts or deposit fees in the payment. */ - MERCHANT_POST_ORDERS_ID_PAY_AMOUNT_OVERFLOW(2153, 500, "There was an integer overflow totaling up the amounts or deposit fees in the payment."), - - /** The deposit fees exceed the total value of the payment. */ - MERCHANT_POST_ORDERS_ID_PAY_FEES_EXCEED_PAYMENT(2154, 400, "The deposit fees exceed the total value of the payment."), - - /** After considering deposit and wire fees, the payment is insufficient to satisfy the required amount for the contract. The client should revisit the logic used to calculate fees it must cover. */ - MERCHANT_POST_ORDERS_ID_PAY_INSUFFICIENT_DUE_TO_FEES(2155, 400, "After considering deposit and wire fees, the payment is insufficient to satisfy the required amount for the contract. The client should revisit the logic used to calculate fees it must cover."), - - /** Even if we do not consider deposit and wire fees, the payment is insufficient to satisfy the required amount for the contract. */ - MERCHANT_POST_ORDERS_ID_PAY_PAYMENT_INSUFFICIENT(2156, 400, "Even if we do not consider deposit and wire fees, the payment is insufficient to satisfy the required amount for the contract."), - - /** The signature over the contract of one of the coins was invalid. */ - MERCHANT_POST_ORDERS_ID_PAY_COIN_SIGNATURE_INVALID(2157, 403, "The signature over the contract of one of the coins was invalid."), - - /** When we tried to find information about the exchange to issue the deposit, we failed. This usually only happens if the merchant backend is somehow unable to get its own HTTP client logic to work. */ - MERCHANT_POST_ORDERS_ID_PAY_EXCHANGE_LOOKUP_FAILED(2158, 500, "When we tried to find information about the exchange to issue the deposit, we failed. This usually only happens if the merchant backend is somehow unable to get its own HTTP client logic to work."), - - /** The refund deadline in the contract is after the transfer deadline. */ - MERCHANT_POST_ORDERS_ID_PAY_REFUND_DEADLINE_PAST_WIRE_TRANSFER_DEADLINE(2159, 500, "The refund deadline in the contract is after the transfer deadline."), - - /** The order was already paid (maybe by another wallet). */ - MERCHANT_POST_ORDERS_ID_PAY_ALREADY_PAID(2160, 409, "The order was already paid (maybe by another wallet)."), - - /** The payment is too late, the offer has expired. */ - MERCHANT_POST_ORDERS_ID_PAY_OFFER_EXPIRED(2161, 410, "The payment is too late, the offer has expired."), - - /** The \"merchant\" field is missing in the proposal data. This is an internal error as the proposal is from the merchant's own database at this point. */ - MERCHANT_POST_ORDERS_ID_PAY_MERCHANT_FIELD_MISSING(2162, 500, "The \"merchant\" field is missing in the proposal data. This is an internal error as the proposal is from the merchant's own database at this point."), - - /** Failed to locate merchant's account information matching the wire hash given in the proposal. */ - MERCHANT_POST_ORDERS_ID_PAY_WIRE_HASH_UNKNOWN(2163, 500, "Failed to locate merchant's account information matching the wire hash given in the proposal."), - - /** The deposit time for the denomination has expired. */ - MERCHANT_POST_ORDERS_ID_PAY_DENOMINATION_DEPOSIT_EXPIRED(2165, 410, "The deposit time for the denomination has expired."), - - /** The exchange of the deposited coin charges a wire fee that could not be added to the total (total amount too high). */ - MERCHANT_POST_ORDERS_ID_PAY_EXCHANGE_WIRE_FEE_ADDITION_FAILED(2166, 500, "The exchange of the deposited coin charges a wire fee that could not be added to the total (total amount too high)."), - - /** The contract was not fully paid because of refunds. Note that clients MAY treat this as paid if, for example, contracts must be executed despite of refunds. */ - MERCHANT_POST_ORDERS_ID_PAY_REFUNDED(2167, 402, "The contract was not fully paid because of refunds. Note that clients MAY treat this as paid if, for example, contracts must be executed despite of refunds."), - - /** According to our database, we have refunded more than we were paid (which should not be possible). */ - MERCHANT_POST_ORDERS_ID_PAY_REFUNDS_EXCEED_PAYMENTS(2168, 500, "According to our database, we have refunded more than we were paid (which should not be possible)."), - - /** The refund request is too late because it is past the wire transfer deadline of the order. The merchant must find a different way to pay back the money to the customer. */ - MERCHANT_PRIVATE_POST_REFUND_AFTER_WIRE_DEADLINE(2169, 410, "The refund request is too late because it is past the wire transfer deadline of the order. The merchant must find a different way to pay back the money to the customer."), - - /** The payment failed at the exchange. */ - MERCHANT_POST_ORDERS_ID_PAY_EXCHANGE_FAILED(2170, 502, "The payment failed at the exchange."), - - /** The payment required a minimum age but one of the coins (of a denomination with support for age restriction) did not provide any age_commitment. */ - MERCHANT_POST_ORDERS_ID_PAY_AGE_COMMITMENT_MISSING(2171, 400, "The payment required a minimum age but one of the coins (of a denomination with support for age restriction) did not provide any age_commitment."), - - /** The payment required a minimum age but one of the coins provided an age_commitment that contained a wrong number of public keys compared to the number of age groups defined in the denomination of the coin. */ - MERCHANT_POST_ORDERS_ID_PAY_AGE_COMMITMENT_SIZE_MISMATCH(2172, 400, "The payment required a minimum age but one of the coins provided an age_commitment that contained a wrong number of public keys compared to the number of age groups defined in the denomination of the coin."), - - /** The payment required a minimum age but one of the coins provided a minimum_age_sig that couldn't be verified with the given age_commitment for that particular minimum age. */ - MERCHANT_POST_ORDERS_ID_PAY_AGE_VERIFICATION_FAILED(2173, 400, "The payment required a minimum age but one of the coins provided a minimum_age_sig that couldn't be verified with the given age_commitment for that particular minimum age."), - - /** The payment required no minimum age but one of the coins (of a denomination with support for age restriction) did not provide the required h_age_commitment. */ - MERCHANT_POST_ORDERS_ID_PAY_AGE_COMMITMENT_HASH_MISSING(2174, 400, "The payment required no minimum age but one of the coins (of a denomination with support for age restriction) did not provide the required h_age_commitment."), - - /** The exchange does not support the selected bank account of the merchant. Likely the merchant had stale data on the bank accounts of the exchange and thus selected an inappropriate exchange when making the offer. */ - MERCHANT_POST_ORDERS_ID_PAY_WIRE_METHOD_UNSUPPORTED(2175, 409, "The exchange does not support the selected bank account of the merchant. Likely the merchant had stale data on the bank accounts of the exchange and thus selected an inappropriate exchange when making the offer."), - - /** The payment requires the wallet to select a choice from the choices array and pass it in the 'choice_index' field of the request. */ - MERCHANT_POST_ORDERS_ID_PAY_CHOICE_INDEX_MISSING(2176, 400, "The payment requires the wallet to select a choice from the choices array and pass it in the 'choice_index' field of the request."), - - /** The 'choice_index' field is invalid. */ - MERCHANT_POST_ORDERS_ID_PAY_CHOICE_INDEX_OUT_OF_BOUNDS(2177, 400, "The 'choice_index' field is invalid."), - - /** The provided 'tokens' array does not match with the required input tokens of the order. */ - MERCHANT_POST_ORDERS_ID_PAY_INPUT_TOKENS_MISMATCH(2178, 400, "The provided 'tokens' array does not match with the required input tokens of the order."), - - /** Invalid token issue signature (blindly signed by merchant) for provided token. */ - MERCHANT_POST_ORDERS_ID_PAY_TOKEN_ISSUE_SIG_INVALID(2179, 400, "Invalid token issue signature (blindly signed by merchant) for provided token."), - - /** Invalid token use signature (EdDSA, signed by wallet) for provided token. */ - MERCHANT_POST_ORDERS_ID_PAY_TOKEN_USE_SIG_INVALID(2180, 400, "Invalid token use signature (EdDSA, signed by wallet) for provided token."), - - /** The provided number of tokens does not match the required number. */ - MERCHANT_POST_ORDERS_ID_PAY_TOKEN_COUNT_MISMATCH(2181, 400, "The provided number of tokens does not match the required number."), - - /** The provided number of token envelopes does not match the specified number. */ - MERCHANT_POST_ORDERS_ID_PAY_TOKEN_ENVELOPE_COUNT_MISMATCH(2182, 400, "The provided number of token envelopes does not match the specified number."), - - /** Invalid token because it was already used, is expired or not yet valid. */ - MERCHANT_POST_ORDERS_ID_PAY_TOKEN_INVALID(2183, 409, "Invalid token because it was already used, is expired or not yet valid."), - - /** The payment violates a transaction limit configured at the given exchange. The wallet has a bug in that it failed to check exchange limits during coin selection. Please report the bug to your wallet developer. */ - MERCHANT_POST_ORDERS_ID_PAY_EXCHANGE_TRANSACTION_LIMIT_VIOLATION(2184, 400, "The payment violates a transaction limit configured at the given exchange. The wallet has a bug in that it failed to check exchange limits during coin selection. Please report the bug to your wallet developer."), - - /** The donation amount provided in the BKPS does not match the amount of the order choice. */ - MERCHANT_POST_ORDERS_ID_PAY_DONATION_AMOUNT_MISMATCH(2185, 409, "The donation amount provided in the BKPS does not match the amount of the order choice."), - - /** Some of the exchanges involved refused the request for reasons related to legitimization. The wallet should try with coins of different exchanges. The merchant should check if they have some legitimization process pending at the exchange. */ - MERCHANT_POST_ORDERS_ID_PAY_EXCHANGE_LEGALLY_REFUSED(2186, 451, "Some of the exchanges involved refused the request for reasons related to legitimization. The wallet should try with coins of different exchanges. The merchant should check if they have some legitimization process pending at the exchange."), - - /** The contract hash does not match the given order ID. */ - MERCHANT_POST_ORDERS_ID_PAID_CONTRACT_HASH_MISMATCH(2200, 400, "The contract hash does not match the given order ID."), - - /** The signature of the merchant is not valid for the given contract hash. */ - MERCHANT_POST_ORDERS_ID_PAID_COIN_SIGNATURE_INVALID(2201, 403, "The signature of the merchant is not valid for the given contract hash."), - - /** A token family with this ID but conflicting data exists. */ - MERCHANT_POST_TOKEN_FAMILY_CONFLICT(2225, 409, "A token family with this ID but conflicting data exists."), - - /** The backend is unaware of a token family with the given ID. */ - MERCHANT_PATCH_TOKEN_FAMILY_NOT_FOUND(2226, 404, "The backend is unaware of a token family with the given ID."), - - /** The merchant failed to send the exchange the refund request. */ - MERCHANT_POST_ORDERS_ID_ABORT_EXCHANGE_REFUND_FAILED(2251, 500, "The merchant failed to send the exchange the refund request."), - - /** The merchant failed to find the exchange to process the lookup. */ - MERCHANT_POST_ORDERS_ID_ABORT_EXCHANGE_LOOKUP_FAILED(2252, 500, "The merchant failed to find the exchange to process the lookup."), - - /** The merchant could not find the contract. */ - MERCHANT_POST_ORDERS_ID_ABORT_CONTRACT_NOT_FOUND(2253, 404, "The merchant could not find the contract."), - - /** The payment was already completed and thus cannot be aborted anymore. */ - MERCHANT_POST_ORDERS_ID_ABORT_REFUND_REFUSED_PAYMENT_COMPLETE(2254, 412, "The payment was already completed and thus cannot be aborted anymore."), - - /** The hash provided by the wallet does not match the order. */ - MERCHANT_POST_ORDERS_ID_ABORT_CONTRACT_HASH_MISSMATCH(2255, 403, "The hash provided by the wallet does not match the order."), - - /** The array of coins cannot be empty. */ - MERCHANT_POST_ORDERS_ID_ABORT_COINS_ARRAY_EMPTY(2256, 400, "The array of coins cannot be empty."), - - /** We are waiting for the exchange to provide us with key material before checking the wire transfer. */ - MERCHANT_EXCHANGE_TRANSFERS_AWAITING_KEYS(2258, 202, "We are waiting for the exchange to provide us with key material before checking the wire transfer."), - - /** We are waiting for the exchange to provide us with the list of aggregated transactions. */ - MERCHANT_EXCHANGE_TRANSFERS_AWAITING_LIST(2259, 202, "We are waiting for the exchange to provide us with the list of aggregated transactions."), - - /** The endpoint indicated in the wire transfer does not belong to a GNU Taler exchange. */ - MERCHANT_EXCHANGE_TRANSFERS_FATAL_NO_EXCHANGE(2260, 200, "The endpoint indicated in the wire transfer does not belong to a GNU Taler exchange."), - - /** The exchange indicated in the wire transfer claims to know nothing about the wire transfer. */ - MERCHANT_EXCHANGE_TRANSFERS_FATAL_NOT_FOUND(2261, 0, "The exchange indicated in the wire transfer claims to know nothing about the wire transfer."), - - /** The interaction with the exchange is delayed due to rate limiting. */ - MERCHANT_EXCHANGE_TRANSFERS_RATE_LIMITED(2262, 202, "The interaction with the exchange is delayed due to rate limiting."), - - /** We experienced a transient failure in our interaction with the exchange. */ - MERCHANT_EXCHANGE_TRANSFERS_TRANSIENT_FAILURE(2263, 202, "We experienced a transient failure in our interaction with the exchange."), - - /** The response from the exchange was unacceptable and should be reviewed with an auditor. */ - MERCHANT_EXCHANGE_TRANSFERS_HARD_FAILURE(2264, 200, "The response from the exchange was unacceptable and should be reviewed with an auditor."), - - /** The merchant backend failed to reach the banking gateway to shorten the wire transfer subject. This probably means that the banking gateway of the exchange is currently down. Contact the exchange operator or simply retry again later. */ - MERCHANT_POST_ACCOUNTS_KYCAUTH_BANK_GATEWAY_UNREACHABLE(2275, 502, "The merchant backend failed to reach the banking gateway to shorten the wire transfer subject. This probably means that the banking gateway of the exchange is currently down. Contact the exchange operator or simply retry again later."), - - /** The merchant backend failed to reach the banking gateway to shorten the wire transfer subject. This probably means that the banking gateway of the exchange is currently down. Contact the exchange operator or simply retry again later. */ - MERCHANT_POST_ACCOUNTS_EXCHANGE_TOO_OLD(2276, 502, "The merchant backend failed to reach the banking gateway to shorten the wire transfer subject. This probably means that the banking gateway of the exchange is currently down. Contact the exchange operator or simply retry again later."), - - /** The merchant backend failed to reach the specified exchange. This probably means that the exchange is currently down. Contact the exchange operator or simply retry again later. */ - MERCHANT_POST_ACCOUNTS_KYCAUTH_EXCHANGE_UNREACHABLE(2277, 502, "The merchant backend failed to reach the specified exchange. This probably means that the exchange is currently down. Contact the exchange operator or simply retry again later."), - - /** We could not claim the order because the backend is unaware of it. */ - MERCHANT_POST_ORDERS_ID_CLAIM_NOT_FOUND(2300, 404, "We could not claim the order because the backend is unaware of it."), - - /** We could not claim the order because someone else claimed it first. */ - MERCHANT_POST_ORDERS_ID_CLAIM_ALREADY_CLAIMED(2301, 409, "We could not claim the order because someone else claimed it first."), - - /** The client-side experienced an internal failure. */ - MERCHANT_POST_ORDERS_ID_CLAIM_CLIENT_INTERNAL_FAILURE(2302, 0, "The client-side experienced an internal failure."), - - /** The unclaim signature of the wallet is not valid for the given contract hash. */ - MERCHANT_POST_ORDERS_UNCLAIM_SIGNATURE_INVALID(2303, 403, "The unclaim signature of the wallet is not valid for the given contract hash."), - - /** The backend failed to sign the refund request. */ - MERCHANT_POST_ORDERS_ID_REFUND_SIGNATURE_FAILED(2350, 0, "The backend failed to sign the refund request."), - - /** The client failed to unblind the signature returned by the merchant. */ - MERCHANT_REWARD_PICKUP_UNBLIND_FAILURE(2400, 0, "The client failed to unblind the signature returned by the merchant."), - - /** The exchange returned a failure code for the withdraw operation. */ - MERCHANT_REWARD_PICKUP_EXCHANGE_ERROR(2403, 502, "The exchange returned a failure code for the withdraw operation."), - - /** The merchant failed to add up the amounts to compute the pick up value. */ - MERCHANT_REWARD_PICKUP_SUMMATION_FAILED(2404, 500, "The merchant failed to add up the amounts to compute the pick up value."), - - /** The reward expired. */ - MERCHANT_REWARD_PICKUP_HAS_EXPIRED(2405, 410, "The reward expired."), - - /** The requested withdraw amount exceeds the amount remaining to be picked up. */ - MERCHANT_REWARD_PICKUP_AMOUNT_EXCEEDS_REWARD_REMAINING(2406, 400, "The requested withdraw amount exceeds the amount remaining to be picked up."), - - /** The merchant did not find the specified denomination key in the exchange's key set. */ - MERCHANT_REWARD_PICKUP_DENOMINATION_UNKNOWN(2407, 409, "The merchant did not find the specified denomination key in the exchange's key set."), - - /** The merchant instance has no active bank accounts configured. However, at least one bank account must be available to create new orders. */ - MERCHANT_PRIVATE_POST_ORDERS_INSTANCE_CONFIGURATION_LACKS_WIRE(2500, 404, "The merchant instance has no active bank accounts configured. However, at least one bank account must be available to create new orders."), - - /** The proposal had no timestamp and the merchant backend failed to obtain the current local time. */ - MERCHANT_PRIVATE_POST_ORDERS_NO_LOCALTIME(2501, 500, "The proposal had no timestamp and the merchant backend failed to obtain the current local time."), - - /** The order provided to the backend could not be parsed; likely some required fields were missing or ill-formed. */ - MERCHANT_PRIVATE_POST_ORDERS_PROPOSAL_PARSE_ERROR(2502, 400, "The order provided to the backend could not be parsed; likely some required fields were missing or ill-formed."), - - /** A conflicting order (sharing the same order identifier) already exists at this merchant backend instance. */ - MERCHANT_PRIVATE_POST_ORDERS_ALREADY_EXISTS(2503, 409, "A conflicting order (sharing the same order identifier) already exists at this merchant backend instance."), - - /** The order creation request is invalid because the given wire deadline is before the refund deadline. */ - MERCHANT_PRIVATE_POST_ORDERS_REFUND_AFTER_WIRE_DEADLINE(2504, 400, "The order creation request is invalid because the given wire deadline is before the refund deadline."), - - /** The order creation request is invalid because the delivery date given is in the past. */ - MERCHANT_PRIVATE_POST_ORDERS_DELIVERY_DATE_IN_PAST(2505, 400, "The order creation request is invalid because the delivery date given is in the past."), - - /** The order creation request is invalid because a wire deadline of \"never\" is not allowed. */ - MERCHANT_PRIVATE_POST_ORDERS_WIRE_DEADLINE_IS_NEVER(2506, 400, "The order creation request is invalid because a wire deadline of \"never\" is not allowed."), - - /** The order creation request is invalid because the given payment deadline is in the past. */ - MERCHANT_PRIVATE_POST_ORDERS_PAY_DEADLINE_IN_PAST(2507, 400, "The order creation request is invalid because the given payment deadline is in the past."), - - /** The order creation request is invalid because the given refund deadline is in the past. */ - MERCHANT_PRIVATE_POST_ORDERS_REFUND_DEADLINE_IN_PAST(2508, 400, "The order creation request is invalid because the given refund deadline is in the past."), - - /** The backend does not trust any exchange that would allow funds to be wired to any bank account of this instance using the wire method specified with the order. (Note that right now, we do not support the use of exchange bank accounts with mandatory currency conversion.) One likely cause for this is that the taler-merchant-exchangekeyupdate process is not running. */ - MERCHANT_PRIVATE_POST_ORDERS_NO_EXCHANGES_FOR_WIRE_METHOD(2509, 409, "The backend does not trust any exchange that would allow funds to be wired to any bank account of this instance using the wire method specified with the order. (Note that right now, we do not support the use of exchange bank accounts with mandatory currency conversion.) One likely cause for this is that the taler-merchant-exchangekeyupdate process is not running."), - - /** One of the paths to forget is malformed. */ - MERCHANT_PRIVATE_PATCH_ORDERS_ID_FORGET_PATH_SYNTAX_INCORRECT(2510, 400, "One of the paths to forget is malformed."), - - /** One of the paths to forget was not marked as forgettable. */ - MERCHANT_PRIVATE_PATCH_ORDERS_ID_FORGET_PATH_NOT_FORGETTABLE(2511, 409, "One of the paths to forget was not marked as forgettable."), - - /** The refund amount would violate a refund transaction limit configured at the given exchange. Please find another way to refund the customer, and inquire with your legislator why they make strange banking regulations. */ - MERCHANT_POST_ORDERS_ID_REFUND_EXCHANGE_TRANSACTION_LIMIT_VIOLATION(2512, 451, "The refund amount would violate a refund transaction limit configured at the given exchange. Please find another way to refund the customer, and inquire with your legislator why they make strange banking regulations."), - - /** The total order amount exceeds hard legal transaction limits from the available exchanges, thus a customer could never legally make this payment. You may try to increase your limits by passing legitimization checks with exchange operators. You could also inquire with your legislator why the limits are prohibitively low for your business. */ - MERCHANT_PRIVATE_POST_ORDERS_AMOUNT_EXCEEDS_LEGAL_LIMITS(2513, 451, "The total order amount exceeds hard legal transaction limits from the available exchanges, thus a customer could never legally make this payment. You may try to increase your limits by passing legitimization checks with exchange operators. You could also inquire with your legislator why the limits are prohibitively low for your business."), - - /** A currency specified to be paid in the contract is not supported by any exchange that this instance can currently use. Possible solutions include (1) specifying a different currency, (2) adding additional suitable exchange operators to the merchant backend configuration, or (3) satisfying compliance rules of an configured exchange to begin using the service of that provider. */ - MERCHANT_PRIVATE_POST_ORDERS_NO_EXCHANGE_FOR_CURRENCY(2514, 409, "A currency specified to be paid in the contract is not supported by any exchange that this instance can currently use. Possible solutions include (1) specifying a different currency, (2) adding additional suitable exchange operators to the merchant backend configuration, or (3) satisfying compliance rules of an configured exchange to begin using the service of that provider."), - - /** The order provided to the backend could not be deleted, our offer is still valid and awaiting payment. Deletion may work later after the offer has expired if it remains unpaid. */ - MERCHANT_PRIVATE_DELETE_ORDERS_AWAITING_PAYMENT(2520, 409, "The order provided to the backend could not be deleted, our offer is still valid and awaiting payment. Deletion may work later after the offer has expired if it remains unpaid."), - - /** The order provided to the backend could not be deleted as the order was already paid. */ - MERCHANT_PRIVATE_DELETE_ORDERS_ALREADY_PAID(2521, 409, "The order provided to the backend could not be deleted as the order was already paid."), - - /** The client requested a report granularity that is not available at the backend. Possible solutions include extending the backend code and/or the database statistic triggers to support the desired data granularity. Alternatively, the client could request a different granularity. */ - MERCHANT_PRIVATE_GET_STATISTICS_REPORT_GRANULARITY_UNAVAILABLE(2525, 410, "The client requested a report granularity that is not available at the backend. Possible solutions include extending the backend code and/or the database statistic triggers to support the desired data granularity. Alternatively, the client could request a different granularity."), - - /** The amount to be refunded is inconsistent: either is lower than the previous amount being awarded, or it exceeds the original price paid by the customer. */ - MERCHANT_PRIVATE_POST_ORDERS_ID_REFUND_INCONSISTENT_AMOUNT(2530, 409, "The amount to be refunded is inconsistent: either is lower than the previous amount being awarded, or it exceeds the original price paid by the customer."), - - /** Only paid orders can be refunded, and the frontend specified an unpaid order to issue a refund for. */ - MERCHANT_PRIVATE_POST_ORDERS_ID_REFUND_ORDER_UNPAID(2531, 409, "Only paid orders can be refunded, and the frontend specified an unpaid order to issue a refund for."), - - /** The refund delay was set to 0 and thus no refunds are ever allowed for this order. */ - MERCHANT_PRIVATE_POST_ORDERS_ID_REFUND_NOT_ALLOWED_BY_CONTRACT(2532, 403, "The refund delay was set to 0 and thus no refunds are ever allowed for this order."), - - /** The token family slug provided in this order could not be found in the merchant database. */ - MERCHANT_PRIVATE_POST_ORDERS_TOKEN_FAMILY_SLUG_UNKNOWN(2533, 404, "The token family slug provided in this order could not be found in the merchant database."), - - /** A token family referenced in this order is either expired or not valid yet. */ - MERCHANT_PRIVATE_POST_ORDERS_TOKEN_FAMILY_NOT_VALID(2534, 409, "A token family referenced in this order is either expired or not valid yet."), - - /** The exchange says it does not know this transfer. */ - MERCHANT_PRIVATE_POST_TRANSFERS_EXCHANGE_UNKNOWN(2550, 502, "The exchange says it does not know this transfer."), - - /** We internally failed to execute the /track/transfer request. */ - MERCHANT_PRIVATE_POST_TRANSFERS_REQUEST_ERROR(2551, 502, "We internally failed to execute the /track/transfer request."), - - /** The amount transferred differs between what was submitted and what the exchange claimed. */ - MERCHANT_PRIVATE_POST_TRANSFERS_CONFLICTING_TRANSFERS(2552, 409, "The amount transferred differs between what was submitted and what the exchange claimed."), - - /** The exchange gave conflicting information about a coin which has been wire transferred. */ - MERCHANT_PRIVATE_POST_TRANSFERS_CONFLICTING_REPORTS(2553, 409, "The exchange gave conflicting information about a coin which has been wire transferred."), - - /** The exchange charged a different wire fee than what it originally advertised, and it is higher. */ - MERCHANT_PRIVATE_POST_TRANSFERS_BAD_WIRE_FEE(2554, 502, "The exchange charged a different wire fee than what it originally advertised, and it is higher."), - - /** We did not find the account that the transfer was made to. */ - MERCHANT_PRIVATE_POST_TRANSFERS_ACCOUNT_NOT_FOUND(2555, 404, "We did not find the account that the transfer was made to."), - - /** The backend could not delete the transfer as the echange already replied to our inquiry about it and we have integrated the result. */ - MERCHANT_PRIVATE_DELETE_TRANSFERS_ALREADY_CONFIRMED(2556, 409, "The backend could not delete the transfer as the echange already replied to our inquiry about it and we have integrated the result."), - - /** The backend could not persist the wire transfer due to the state of the backend. This usually means that a wire transfer with the same wire transfer subject but a different amount was previously submitted to the backend. */ - MERCHANT_PRIVATE_POST_TRANSFERS_CONFLICTING_SUBMISSION(2557, 409, "The backend could not persist the wire transfer due to the state of the backend. This usually means that a wire transfer with the same wire transfer subject but a different amount was previously submitted to the backend."), - - /** The target bank account given by the exchange is not (or no longer) known at the merchant instance. */ - MERCHANT_EXCHANGE_TRANSFERS_TARGET_ACCOUNT_UNKNOWN(2558, 0, "The target bank account given by the exchange is not (or no longer) known at the merchant instance."), - - /** The amount transferred differs between what was submitted and what the exchange claimed. */ - MERCHANT_EXCHANGE_TRANSFERS_CONFLICTING_TRANSFERS(2563, 0, "The amount transferred differs between what was submitted and what the exchange claimed."), - - /** The report ID provided to the backend is not known to the backend. */ - MERCHANT_REPORT_GENERATOR_FAILED(2570, 501, "The report ID provided to the backend is not known to the backend."), - - /** Failed to fetch the data for the report from the backend. */ - MERCHANT_REPORT_FETCH_FAILED(2571, 502, "Failed to fetch the data for the report from the backend."), - - /** The merchant backend cannot create an instance under the given identifier as one already exists. Use PATCH to modify the existing entry. */ - MERCHANT_PRIVATE_POST_INSTANCES_ALREADY_EXISTS(2600, 409, "The merchant backend cannot create an instance under the given identifier as one already exists. Use PATCH to modify the existing entry."), - - /** The merchant backend cannot create an instance because the authentication configuration field is malformed. */ - MERCHANT_PRIVATE_POST_INSTANCES_BAD_AUTH(2601, 400, "The merchant backend cannot create an instance because the authentication configuration field is malformed."), - - /** The merchant backend cannot update an instance's authentication settings because the provided authentication settings are malformed. */ - MERCHANT_PRIVATE_POST_INSTANCE_AUTH_BAD_AUTH(2602, 400, "The merchant backend cannot update an instance's authentication settings because the provided authentication settings are malformed."), - - /** The merchant backend cannot create an instance under the given identifier, the previous one was deleted but must be purged first. */ - MERCHANT_PRIVATE_POST_INSTANCES_PURGE_REQUIRED(2603, 409, "The merchant backend cannot create an instance under the given identifier, the previous one was deleted but must be purged first."), - - /** The merchant backend cannot update an instance under the given identifier, the previous one was deleted but must be purged first. */ - MERCHANT_PRIVATE_PATCH_INSTANCES_PURGE_REQUIRED(2625, 409, "The merchant backend cannot update an instance under the given identifier, the previous one was deleted but must be purged first."), - - /** The bank account referenced in the requested operation was not found. */ - MERCHANT_PRIVATE_ACCOUNT_DELETE_UNKNOWN_ACCOUNT(2626, 404, "The bank account referenced in the requested operation was not found."), - - /** The bank account specified in the request already exists at the merchant. */ - MERCHANT_PRIVATE_ACCOUNT_EXISTS(2627, 409, "The bank account specified in the request already exists at the merchant."), - - /** The bank account specified is not acceptable for this exchange. The exchange either does not support the wire method or something else about the specific account. Consult the exchange account constraints and specify a different bank account if you want to use this exchange. */ - MERCHANT_PRIVATE_ACCOUNT_NOT_ELIGIBLE_FOR_EXCHANGE(2628, 409, "The bank account specified is not acceptable for this exchange. The exchange either does not support the wire method or something else about the specific account. Consult the exchange account constraints and specify a different bank account if you want to use this exchange."), - - /** The product ID exists. */ - MERCHANT_PRIVATE_POST_PRODUCTS_CONFLICT_PRODUCT_EXISTS(2650, 409, "The product ID exists."), - - /** A category with the same name exists already. */ - MERCHANT_PRIVATE_POST_CATEGORIES_CONFLICT_CATEGORY_EXISTS(2651, 409, "A category with the same name exists already."), - - /** The update would have reduced the total amount of product lost, which is not allowed. */ - MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_LOST_REDUCED(2660, 409, "The update would have reduced the total amount of product lost, which is not allowed."), - - /** The update would have mean that more stocks were lost than what remains from total inventory after sales, which is not allowed. */ - MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_LOST_EXCEEDS_STOCKS(2661, 400, "The update would have mean that more stocks were lost than what remains from total inventory after sales, which is not allowed."), - - /** The update would have reduced the total amount of product in stock, which is not allowed. */ - MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_STOCKED_REDUCED(2662, 409, "The update would have reduced the total amount of product in stock, which is not allowed."), - - /** The update would have reduced the total amount of product sold, which is not allowed. */ - MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_SOLD_REDUCED(2663, 409, "The update would have reduced the total amount of product sold, which is not allowed."), - - /** The lock request is for more products than we have left (unlocked) in stock. */ - MERCHANT_PRIVATE_POST_PRODUCTS_LOCK_INSUFFICIENT_STOCKS(2670, 410, "The lock request is for more products than we have left (unlocked) in stock."), - - /** The deletion request is for a product that is locked. The product cannot be deleted until the existing offer to expires. */ - MERCHANT_PRIVATE_DELETE_PRODUCTS_CONFLICTING_LOCK(2680, 409, "The deletion request is for a product that is locked. The product cannot be deleted until the existing offer to expires."), - - /** The proposed name for the product group is already in use. You should select a different name. */ - MERCHANT_PRIVATE_PRODUCT_GROUP_CONFLICTING_NAME(2690, 409, "The proposed name for the product group is already in use. You should select a different name."), - - /** The proposed name for the money pot is already in use. You should select a different name. */ - MERCHANT_PRIVATE_MONEY_POT_CONFLICTING_NAME(2691, 409, "The proposed name for the money pot is already in use. You should select a different name."), - - /** The total amount in the money pot is different from the amount required by the request. The client should fetch the current pot total and retry with the latest amount to succeed. */ - MERCHANT_PRIVATE_MONEY_POT_CONFLICTING_TOTAL(2692, 409, "The total amount in the money pot is different from the amount required by the request. The client should fetch the current pot total and retry with the latest amount to succeed."), - - /** The requested wire method is not supported by the exchange. */ - MERCHANT_PRIVATE_POST_RESERVES_UNSUPPORTED_WIRE_METHOD(2700, 409, "The requested wire method is not supported by the exchange."), - - /** The requested exchange does not allow rewards. */ - MERCHANT_PRIVATE_POST_RESERVES_REWARDS_NOT_ALLOWED(2701, 409, "The requested exchange does not allow rewards."), - - /** The reserve could not be deleted because it is unknown. */ - MERCHANT_PRIVATE_DELETE_RESERVES_NO_SUCH_RESERVE(2710, 404, "The reserve could not be deleted because it is unknown."), - - /** The reserve that was used to fund the rewards has expired. */ - MERCHANT_PRIVATE_POST_REWARD_AUTHORIZE_RESERVE_EXPIRED(2750, 410, "The reserve that was used to fund the rewards has expired."), - - /** The reserve that was used to fund the rewards was not found in the DB. */ - MERCHANT_PRIVATE_POST_REWARD_AUTHORIZE_RESERVE_UNKNOWN(2751, 503, "The reserve that was used to fund the rewards was not found in the DB."), - - /** The backend knows the instance that was supposed to support the reward, and it was configured for rewardping. However, the funds remaining are insufficient to cover the reward, and the merchant should top up the reserve. */ - MERCHANT_PRIVATE_POST_REWARD_AUTHORIZE_INSUFFICIENT_FUNDS(2752, 0, "The backend knows the instance that was supposed to support the reward, and it was configured for rewardping. However, the funds remaining are insufficient to cover the reward, and the merchant should top up the reserve."), - - /** The backend failed to find a reserve needed to authorize the reward. */ - MERCHANT_PRIVATE_POST_REWARD_AUTHORIZE_RESERVE_NOT_FOUND(2753, 503, "The backend failed to find a reserve needed to authorize the reward."), - - /** The merchant backend encountered a failure in computing the deposit total. */ - MERCHANT_PRIVATE_GET_ORDERS_ID_AMOUNT_ARITHMETIC_FAILURE(2800, 200, "The merchant backend encountered a failure in computing the deposit total."), - - /** The template ID already exists. */ - MERCHANT_PRIVATE_POST_TEMPLATES_CONFLICT_TEMPLATE_EXISTS(2850, 409, "The template ID already exists."), - - /** The OTP device ID already exists. */ - MERCHANT_PRIVATE_POST_OTP_DEVICES_CONFLICT_OTP_DEVICE_EXISTS(2851, 409, "The OTP device ID already exists."), - - /** Amount given in the using template and in the template contract. There is a conflict. */ - MERCHANT_POST_USING_TEMPLATES_AMOUNT_CONFLICT_TEMPLATES_CONTRACT_AMOUNT(2860, 409, "Amount given in the using template and in the template contract. There is a conflict."), - - /** Subject given in the using template and in the template contract. There is a conflict. */ - MERCHANT_POST_USING_TEMPLATES_SUMMARY_CONFLICT_TEMPLATES_CONTRACT_SUBJECT(2861, 409, "Subject given in the using template and in the template contract. There is a conflict."), - - /** Amount not given in the using template and in the template contract. There is a conflict. */ - MERCHANT_POST_USING_TEMPLATES_NO_AMOUNT(2862, 409, "Amount not given in the using template and in the template contract. There is a conflict."), - - /** Subject not given in the using template and in the template contract. There is a conflict. */ - MERCHANT_POST_USING_TEMPLATES_NO_SUMMARY(2863, 409, "Subject not given in the using template and in the template contract. There is a conflict."), - - /** The selected template has a different type than the one specified in the request of the client. This may happen if the template was updated since the last time the client fetched it. The client should re-fetch the current template and send a request of the correct type. */ - MERCHANT_POST_USING_TEMPLATES_WRONG_TYPE(2864, 409, "The selected template has a different type than the one specified in the request of the client. This may happen if the template was updated since the last time the client fetched it. The client should re-fetch the current template and send a request of the correct type."), - - /** The selected template does not allow one of the specified products to be included in the order. This may happen if the template was updated since the last time the client fetched it. The client should re-fetch the current template and send a request of the correct type. */ - MERCHANT_POST_USING_TEMPLATES_WRONG_PRODUCT(2865, 409, "The selected template does not allow one of the specified products to be included in the order. This may happen if the template was updated since the last time the client fetched it. The client should re-fetch the current template and send a request of the correct type."), - - /** The selected combination of products does not allow the backend to compute a price for the order in any of the supported currencies. This may happen if the template was updated since the last time the client fetched it or if the wallet assembled an unsupported combination of products. The site administrator might want to specify additional prices for products, while the client should re-fetch the current template and send a request with a combination of products for which prices exist in the same currency. */ - MERCHANT_POST_USING_TEMPLATES_NO_CURRENCY(2866, 409, "The selected combination of products does not allow the backend to compute a price for the order in any of the supported currencies. This may happen if the template was updated since the last time the client fetched it or if the wallet assembled an unsupported combination of products. The site administrator might want to specify additional prices for products, while the client should re-fetch the current template and send a request with a combination of products for which prices exist in the same currency."), - - /** The webhook ID elready exists. */ - MERCHANT_PRIVATE_POST_WEBHOOKS_CONFLICT_WEBHOOK_EXISTS(2900, 409, "The webhook ID elready exists."), - - /** The webhook serial elready exists. */ - MERCHANT_PRIVATE_POST_PENDING_WEBHOOKS_CONFLICT_PENDING_WEBHOOK_EXISTS(2910, 409, "The webhook serial elready exists."), - - /** The auditor refused the connection due to a lack of authorization. */ - AUDITOR_GENERIC_UNAUTHORIZED(3001, 401, "The auditor refused the connection due to a lack of authorization."), - - /** This method is not allowed here. */ - AUDITOR_GENERIC_METHOD_NOT_ALLOWED(3002, 405, "This method is not allowed here."), - - /** The signature from the exchange on the deposit confirmation is invalid. */ - AUDITOR_DEPOSIT_CONFIRMATION_SIGNATURE_INVALID(3100, 403, "The signature from the exchange on the deposit confirmation is invalid."), - - /** The exchange key used for the signature on the deposit confirmation was revoked. */ - AUDITOR_EXCHANGE_SIGNING_KEY_REVOKED(3101, 410, "The exchange key used for the signature on the deposit confirmation was revoked."), - - /** The requested resource could not be found. */ - AUDITOR_RESOURCE_NOT_FOUND(3102, 404, "The requested resource could not be found."), - - /** The URI is missing a path component. */ - AUDITOR_URI_MISSING_PATH_COMPONENT(3103, 400, "The URI is missing a path component."), - - /** Wire transfer attempted with credit and debit party being the same bank account. */ - BANK_SAME_ACCOUNT(5101, 400, "Wire transfer attempted with credit and debit party being the same bank account."), - - /** Wire transfer impossible, due to financial limitation of the party that attempted the payment. */ - BANK_UNALLOWED_DEBIT(5102, 409, "Wire transfer impossible, due to financial limitation of the party that attempted the payment."), - - /** Negative numbers are not allowed (as value and/or fraction) to instantiate an amount object. */ - BANK_NEGATIVE_NUMBER_AMOUNT(5103, 400, "Negative numbers are not allowed (as value and/or fraction) to instantiate an amount object."), - - /** A too big number was used (as value and/or fraction) to instantiate an amount object. */ - BANK_NUMBER_TOO_BIG(5104, 400, "A too big number was used (as value and/or fraction) to instantiate an amount object."), - - /** The bank account referenced in the requested operation was not found. */ - BANK_UNKNOWN_ACCOUNT(5106, 404, "The bank account referenced in the requested operation was not found."), - - /** The transaction referenced in the requested operation (typically a reject operation), was not found. */ - BANK_TRANSACTION_NOT_FOUND(5107, 404, "The transaction referenced in the requested operation (typically a reject operation), was not found."), - - /** Bank received a malformed amount string. */ - BANK_BAD_FORMAT_AMOUNT(5108, 400, "Bank received a malformed amount string."), - - /** The client does not own the account credited by the transaction which is to be rejected, so it has no rights do reject it. */ - BANK_REJECT_NO_RIGHTS(5109, 403, "The client does not own the account credited by the transaction which is to be rejected, so it has no rights do reject it."), - - /** This error code is returned when no known exception types captured the exception. */ - BANK_UNMANAGED_EXCEPTION(5110, 500, "This error code is returned when no known exception types captured the exception."), - - /** This error code is used for all those exceptions that do not really need a specific error code to return to the client. Used for example when a client is trying to register with a unavailable username. */ - BANK_SOFT_EXCEPTION(5111, 500, "This error code is used for all those exceptions that do not really need a specific error code to return to the client. Used for example when a client is trying to register with a unavailable username."), - - /** The request UID for a request to transfer funds has already been used, but with different details for the transfer. */ - BANK_TRANSFER_REQUEST_UID_REUSED(5112, 409, "The request UID for a request to transfer funds has already been used, but with different details for the transfer."), - - /** The withdrawal operation already has a reserve selected. The current request conflicts with the existing selection. */ - BANK_WITHDRAWAL_OPERATION_RESERVE_SELECTION_CONFLICT(5113, 409, "The withdrawal operation already has a reserve selected. The current request conflicts with the existing selection."), - - /** The wire transfer subject duplicates an existing reserve public key. But wire transfer subjects must be unique. */ - BANK_DUPLICATE_RESERVE_PUB_SUBJECT(5114, 409, "The wire transfer subject duplicates an existing reserve public key. But wire transfer subjects must be unique."), - - /** The client requested a transaction that is so far in the past, that it has been forgotten by the bank. */ - BANK_ANCIENT_TRANSACTION_GONE(5115, 410, "The client requested a transaction that is so far in the past, that it has been forgotten by the bank."), - - /** The client attempted to abort a transaction that was already confirmed. */ - BANK_ABORT_CONFIRM_CONFLICT(5116, 409, "The client attempted to abort a transaction that was already confirmed."), - - /** The client attempted to confirm a transaction that was already aborted. */ - BANK_CONFIRM_ABORT_CONFLICT(5117, 409, "The client attempted to confirm a transaction that was already aborted."), - - /** The client attempted to register an account with the same name. */ - BANK_REGISTER_CONFLICT(5118, 409, "The client attempted to register an account with the same name."), - - /** The client attempted to confirm a withdrawal operation before the wallet posted the required details. */ - BANK_POST_WITHDRAWAL_OPERATION_REQUIRED(5119, 400, "The client attempted to confirm a withdrawal operation before the wallet posted the required details."), - - /** The client tried to register a new account under a reserved username (like 'admin' for example). */ - BANK_RESERVED_USERNAME_CONFLICT(5120, 409, "The client tried to register a new account under a reserved username (like 'admin' for example)."), - - /** The client tried to register a new account with an username already in use. */ - BANK_REGISTER_USERNAME_REUSE(5121, 409, "The client tried to register a new account with an username already in use."), - - /** The client tried to register a new account with a payto:// URI already in use. */ - BANK_REGISTER_PAYTO_URI_REUSE(5122, 409, "The client tried to register a new account with a payto:// URI already in use."), - - /** The client tried to delete an account with a non null balance. */ - BANK_ACCOUNT_BALANCE_NOT_ZERO(5123, 409, "The client tried to delete an account with a non null balance."), - - /** The client tried to create a transaction or an operation that credit an unknown account. */ - BANK_UNKNOWN_CREDITOR(5124, 409, "The client tried to create a transaction or an operation that credit an unknown account."), - - /** The client tried to create a transaction or an operation that debit an unknown account. */ - BANK_UNKNOWN_DEBTOR(5125, 409, "The client tried to create a transaction or an operation that debit an unknown account."), - - /** The client tried to perform an action prohibited for exchange accounts. */ - BANK_ACCOUNT_IS_EXCHANGE(5126, 409, "The client tried to perform an action prohibited for exchange accounts."), - - /** The client tried to perform an action reserved for exchange accounts. */ - BANK_ACCOUNT_IS_NOT_EXCHANGE(5127, 409, "The client tried to perform an action reserved for exchange accounts."), - - /** Received currency conversion is wrong. */ - BANK_BAD_CONVERSION(5128, 409, "Received currency conversion is wrong."), - - /** The account referenced in this operation is missing tan info for the chosen channel. */ - BANK_MISSING_TAN_INFO(5129, 409, "The account referenced in this operation is missing tan info for the chosen channel."), - - /** The client attempted to confirm a transaction with incomplete info. */ - BANK_CONFIRM_INCOMPLETE(5130, 409, "The client attempted to confirm a transaction with incomplete info."), - - /** The request rate is too high. The server is refusing requests to guard against brute-force attacks. */ - BANK_TAN_RATE_LIMITED(5131, 429, "The request rate is too high. The server is refusing requests to guard against brute-force attacks."), - - /** This TAN channel is not supported. */ - BANK_TAN_CHANNEL_NOT_SUPPORTED(5132, 501, "This TAN channel is not supported."), - - /** Failed to send TAN using the helper script. Either script is not found, or script timeout, or script terminated with a non-successful result. */ - BANK_TAN_CHANNEL_SCRIPT_FAILED(5133, 500, "Failed to send TAN using the helper script. Either script is not found, or script timeout, or script terminated with a non-successful result."), - - /** The client's response to the challenge was invalid. */ - BANK_TAN_CHALLENGE_FAILED(5134, 403, "The client's response to the challenge was invalid."), - - /** A non-admin user has tried to change their legal name. */ - BANK_NON_ADMIN_PATCH_LEGAL_NAME(5135, 409, "A non-admin user has tried to change their legal name."), - - /** A non-admin user has tried to change their debt limit. */ - BANK_NON_ADMIN_PATCH_DEBT_LIMIT(5136, 409, "A non-admin user has tried to change their debt limit."), - - /** A non-admin user has tried to change their password whihout providing the current one. */ - BANK_NON_ADMIN_PATCH_MISSING_OLD_PASSWORD(5137, 409, "A non-admin user has tried to change their password whihout providing the current one."), - - /** Provided old password does not match current password. */ - BANK_PATCH_BAD_OLD_PASSWORD(5138, 409, "Provided old password does not match current password."), - - /** An admin user has tried to become an exchange. */ - BANK_PATCH_ADMIN_EXCHANGE(5139, 409, "An admin user has tried to become an exchange."), - - /** A non-admin user has tried to change their cashout account. */ - BANK_NON_ADMIN_PATCH_CASHOUT(5140, 409, "A non-admin user has tried to change their cashout account."), - - /** A non-admin user has tried to change their contact info. */ - BANK_NON_ADMIN_PATCH_CONTACT(5141, 409, "A non-admin user has tried to change their contact info."), - - /** The client tried to create a transaction that credit the admin account. */ - BANK_ADMIN_CREDITOR(5142, 409, "The client tried to create a transaction that credit the admin account."), - - /** The referenced challenge was not found. */ - BANK_CHALLENGE_NOT_FOUND(5143, 404, "The referenced challenge was not found."), - - /** The referenced challenge has expired. */ - BANK_TAN_CHALLENGE_EXPIRED(5144, 409, "The referenced challenge has expired."), - - /** A non-admin user has tried to create an account with 2fa. */ - BANK_NON_ADMIN_SET_TAN_CHANNEL(5145, 409, "A non-admin user has tried to create an account with 2fa."), - - /** A non-admin user has tried to set their minimum cashout amount. */ - BANK_NON_ADMIN_SET_MIN_CASHOUT(5146, 409, "A non-admin user has tried to set their minimum cashout amount."), - - /** Amount of currency conversion it less than the minimum allowed. */ - BANK_CONVERSION_AMOUNT_TO_SMALL(5147, 409, "Amount of currency conversion it less than the minimum allowed."), - - /** Specified amount will not work for this withdrawal. */ - BANK_AMOUNT_DIFFERS(5148, 409, "Specified amount will not work for this withdrawal."), - - /** The backend requires an amount to be specified. */ - BANK_AMOUNT_REQUIRED(5149, 409, "The backend requires an amount to be specified."), - - /** Provided password is too short. */ - BANK_PASSWORD_TOO_SHORT(5150, 409, "Provided password is too short."), - - /** Provided password is too long. */ - BANK_PASSWORD_TOO_LONG(5151, 409, "Provided password is too long."), - - /** Bank account is locked and cannot authenticate using his password. */ - BANK_ACCOUNT_LOCKED(5152, 403, "Bank account is locked and cannot authenticate using his password."), - - /** The client attempted to update a transaction' details that was already aborted. */ - BANK_UPDATE_ABORT_CONFLICT(5153, 409, "The client attempted to update a transaction' details that was already aborted."), - - /** The wtid for a request to transfer funds has already been used, but with a different request unpaid. */ - BANK_TRANSFER_WTID_REUSED(5154, 409, "The wtid for a request to transfer funds has already been used, but with a different request unpaid."), - - /** A non-admin user has tried to set their conversion rate class */ - BANK_NON_ADMIN_SET_CONVERSION_RATE_CLASS(5155, 409, "A non-admin user has tried to set their conversion rate class"), - - /** The referenced conversion rate class was not found */ - BANK_CONVERSION_RATE_CLASS_UNKNOWN(5156, 409, "The referenced conversion rate class was not found"), - - /** The client tried to use an already taken name. */ - BANK_NAME_REUSE(5157, 409, "The client tried to use an already taken name."), - - /** This subject format is not supported. */ - BANK_UNSUPPORTED_SUBJECT_FORMAT(5158, 409, "This subject format is not supported."), - - /** The derived subject is already used. */ - BANK_DERIVATION_REUSE(5159, 409, "The derived subject is already used."), - - /** The provided signature is invalid. */ - BANK_BAD_SIGNATURE(5160, 409, "The provided signature is invalid."), - - /** The provided timestamp is too old. */ - BANK_OLD_TIMESTAMP(5161, 409, "The provided timestamp is too old."), - - /** The authorization_pub for a request to transfer funds has already been used for another non recurrent transfer. */ - BANK_TRANSFER_MAPPING_REUSED(5162, 409, "The authorization_pub for a request to transfer funds has already been used for another non recurrent transfer."), - - /** The authorization_pub for a request to transfer funds is not currently registered. */ - BANK_TRANSFER_MAPPING_UNKNOWN(5163, 409, "The authorization_pub for a request to transfer funds is not currently registered."), - - /** The sync service failed find the account in its database. */ - SYNC_ACCOUNT_UNKNOWN(6100, 404, "The sync service failed find the account in its database."), - - /** The SHA-512 hash provided in the If-None-Match header is malformed. */ - SYNC_BAD_IF_NONE_MATCH(6101, 400, "The SHA-512 hash provided in the If-None-Match header is malformed."), - - /** The SHA-512 hash provided in the If-Match header is malformed or missing. */ - SYNC_BAD_IF_MATCH(6102, 400, "The SHA-512 hash provided in the If-Match header is malformed or missing."), - - /** The signature provided in the \"Sync-Signature\" header is malformed or missing. */ - SYNC_BAD_SYNC_SIGNATURE(6103, 400, "The signature provided in the \"Sync-Signature\" header is malformed or missing."), - - /** The signature provided in the \"Sync-Signature\" header does not match the account, old or new Etags. */ - SYNC_INVALID_SIGNATURE(6104, 403, "The signature provided in the \"Sync-Signature\" header does not match the account, old or new Etags."), - - /** The \"Content-length\" field for the upload is not a number. */ - SYNC_MALFORMED_CONTENT_LENGTH(6105, 400, "The \"Content-length\" field for the upload is not a number."), - - /** The \"Content-length\" field for the upload is too big based on the server's terms of service. */ - SYNC_EXCESSIVE_CONTENT_LENGTH(6106, 413, "The \"Content-length\" field for the upload is too big based on the server's terms of service."), - - /** The server is out of memory to handle the upload. Trying again later may succeed. */ - SYNC_OUT_OF_MEMORY_ON_CONTENT_LENGTH(6107, 413, "The server is out of memory to handle the upload. Trying again later may succeed."), - - /** The uploaded data does not match the Etag. */ - SYNC_INVALID_UPLOAD(6108, 400, "The uploaded data does not match the Etag."), - - /** HTTP server experienced a timeout while awaiting promised payment. */ - SYNC_PAYMENT_GENERIC_TIMEOUT(6109, 408, "HTTP server experienced a timeout while awaiting promised payment."), - - /** Sync could not setup the payment request with its own backend. */ - SYNC_PAYMENT_CREATE_BACKEND_ERROR(6110, 500, "Sync could not setup the payment request with its own backend."), - - /** The sync service failed find the backup to be updated in its database. */ - SYNC_PREVIOUS_BACKUP_UNKNOWN(6111, 404, "The sync service failed find the backup to be updated in its database."), - - /** The \"Content-length\" field for the upload is missing. */ - SYNC_MISSING_CONTENT_LENGTH(6112, 400, "The \"Content-length\" field for the upload is missing."), - - /** Sync had problems communicating with its payment backend. */ - SYNC_GENERIC_BACKEND_ERROR(6113, 502, "Sync had problems communicating with its payment backend."), - - /** Sync experienced a timeout communicating with its payment backend. */ - SYNC_GENERIC_BACKEND_TIMEOUT(6114, 504, "Sync experienced a timeout communicating with its payment backend."), - - /** The wallet does not implement a version of the exchange protocol that is compatible with the protocol version of the exchange. */ - WALLET_EXCHANGE_PROTOCOL_VERSION_INCOMPATIBLE(7000, 501, "The wallet does not implement a version of the exchange protocol that is compatible with the protocol version of the exchange."), - - /** The wallet encountered an unexpected exception. This is likely a bug in the wallet implementation. */ - WALLET_UNEXPECTED_EXCEPTION(7001, 500, "The wallet encountered an unexpected exception. This is likely a bug in the wallet implementation."), - - /** The wallet received a response from a server, but the response can't be parsed. */ - WALLET_RECEIVED_MALFORMED_RESPONSE(7002, 0, "The wallet received a response from a server, but the response can't be parsed."), - - /** The wallet tried to make a network request, but it received no response. */ - WALLET_NETWORK_ERROR(7003, 0, "The wallet tried to make a network request, but it received no response."), - - /** The wallet tried to make a network request, but it was throttled. */ - WALLET_HTTP_REQUEST_THROTTLED(7004, 0, "The wallet tried to make a network request, but it was throttled."), - - /** The wallet made a request to a service, but received an error response it does not know how to handle. */ - WALLET_UNEXPECTED_REQUEST_ERROR(7005, 0, "The wallet made a request to a service, but received an error response it does not know how to handle."), - - /** The denominations offered by the exchange are insufficient. Likely the exchange is badly configured or not maintained. */ - WALLET_EXCHANGE_DENOMINATIONS_INSUFFICIENT(7006, 0, "The denominations offered by the exchange are insufficient. Likely the exchange is badly configured or not maintained."), - - /** The wallet does not support the operation requested by a client. */ - WALLET_CORE_API_OPERATION_UNKNOWN(7007, 0, "The wallet does not support the operation requested by a client."), - - /** The given taler://pay URI is invalid. */ - WALLET_INVALID_TALER_PAY_URI(7008, 0, "The given taler://pay URI is invalid."), - - /** The signature on a coin by the exchange's denomination key is invalid after unblinding it. */ - WALLET_EXCHANGE_COIN_SIGNATURE_INVALID(7009, 0, "The signature on a coin by the exchange's denomination key is invalid after unblinding it."), - - /** The wallet core service is not available. */ - WALLET_CORE_NOT_AVAILABLE(7011, 0, "The wallet core service is not available."), - - /** The bank has aborted a withdrawal operation, and thus a withdrawal can't complete. */ - WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK(7012, 0, "The bank has aborted a withdrawal operation, and thus a withdrawal can't complete."), - - /** An HTTP request made by the wallet timed out. */ - WALLET_HTTP_REQUEST_GENERIC_TIMEOUT(7013, 0, "An HTTP request made by the wallet timed out."), - - /** The order has already been claimed by another wallet. */ - WALLET_ORDER_ALREADY_CLAIMED(7014, 0, "The order has already been claimed by another wallet."), - - /** A group of withdrawal operations (typically for the same reserve at the same exchange) has errors and will be tried again later. */ - WALLET_WITHDRAWAL_GROUP_INCOMPLETE(7015, 0, "A group of withdrawal operations (typically for the same reserve at the same exchange) has errors and will be tried again later."), - - /** The signature on a coin by the exchange's denomination key (obtained through the merchant via a reward) is invalid after unblinding it. */ - WALLET_REWARD_COIN_SIGNATURE_INVALID(7016, 0, "The signature on a coin by the exchange's denomination key (obtained through the merchant via a reward) is invalid after unblinding it."), - - /** The wallet does not implement a version of the bank integration API that is compatible with the version offered by the bank. */ - WALLET_BANK_INTEGRATION_PROTOCOL_VERSION_INCOMPATIBLE(7017, 0, "The wallet does not implement a version of the bank integration API that is compatible with the version offered by the bank."), - - /** The wallet processed a taler://pay URI, but the merchant base URL in the downloaded contract terms does not match the merchant base URL derived from the URI. */ - WALLET_CONTRACT_TERMS_BASE_URL_MISMATCH(7018, 0, "The wallet processed a taler://pay URI, but the merchant base URL in the downloaded contract terms does not match the merchant base URL derived from the URI."), - - /** The merchant's signature on the contract terms is invalid. */ - WALLET_CONTRACT_TERMS_SIGNATURE_INVALID(7019, 0, "The merchant's signature on the contract terms is invalid."), - - /** The contract terms given by the merchant are malformed. */ - WALLET_CONTRACT_TERMS_MALFORMED(7020, 0, "The contract terms given by the merchant are malformed."), - - /** A pending operation failed, and thus the request can't be completed. */ - WALLET_PENDING_OPERATION_FAILED(7021, 0, "A pending operation failed, and thus the request can't be completed."), - - /** A payment was attempted, but the merchant had an internal server error (5xx). */ - WALLET_PAY_MERCHANT_SERVER_ERROR(7022, 0, "A payment was attempted, but the merchant had an internal server error (5xx)."), - - /** The crypto worker failed. */ - WALLET_CRYPTO_WORKER_ERROR(7023, 0, "The crypto worker failed."), - - /** The crypto worker received a bad request. */ - WALLET_CRYPTO_WORKER_BAD_REQUEST(7024, 0, "The crypto worker received a bad request."), - - /** A KYC step is required before withdrawal can proceed. */ - WALLET_WITHDRAWAL_KYC_REQUIRED(7025, 0, "A KYC step is required before withdrawal can proceed."), - - /** The wallet does not have sufficient balance to create a deposit group. */ - WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE(7026, 0, "The wallet does not have sufficient balance to create a deposit group."), - - /** The wallet does not have sufficient balance to create a peer push payment. */ - WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE(7027, 0, "The wallet does not have sufficient balance to create a peer push payment."), - - /** The wallet does not have sufficient balance to pay for an invoice. */ - WALLET_PEER_PULL_PAYMENT_INSUFFICIENT_BALANCE(7028, 0, "The wallet does not have sufficient balance to pay for an invoice."), - - /** A group of refresh operations has errors and will be tried again later. */ - WALLET_REFRESH_GROUP_INCOMPLETE(7029, 0, "A group of refresh operations has errors and will be tried again later."), - - /** The exchange's self-reported base URL does not match the one that the wallet is using. */ - WALLET_EXCHANGE_BASE_URL_MISMATCH(7030, 0, "The exchange's self-reported base URL does not match the one that the wallet is using."), - - /** The order has already been paid by another wallet. */ - WALLET_ORDER_ALREADY_PAID(7031, 0, "The order has already been paid by another wallet."), - - /** An exchange that is required for some request is currently not available. */ - WALLET_EXCHANGE_UNAVAILABLE(7032, 0, "An exchange that is required for some request is currently not available."), - - /** An exchange entry is still used by the exchange, thus it can't be deleted without purging. */ - WALLET_EXCHANGE_ENTRY_USED(7033, 0, "An exchange entry is still used by the exchange, thus it can't be deleted without purging."), - - /** The wallet database is unavailable and the wallet thus is not operational. */ - WALLET_DB_UNAVAILABLE(7034, 0, "The wallet database is unavailable and the wallet thus is not operational."), - - /** A taler:// URI is malformed and can't be parsed. */ - WALLET_TALER_URI_MALFORMED(7035, 0, "A taler:// URI is malformed and can't be parsed."), - - /** A wallet-core request was cancelled and thus can't provide a response. */ - WALLET_CORE_REQUEST_CANCELLED(7036, 0, "A wallet-core request was cancelled and thus can't provide a response."), - - /** A wallet-core request failed because the user needs to first accept the exchange's terms of service. */ - WALLET_EXCHANGE_TOS_NOT_ACCEPTED(7037, 0, "A wallet-core request failed because the user needs to first accept the exchange's terms of service."), - - /** An exchange entry could not be updated, as the exchange's new details conflict with the new details. */ - WALLET_EXCHANGE_ENTRY_UPDATE_CONFLICT(7038, 0, "An exchange entry could not be updated, as the exchange's new details conflict with the new details."), - - /** The wallet's information about the exchange is outdated. */ - WALLET_EXCHANGE_ENTRY_OUTDATED(7039, 0, "The wallet's information about the exchange is outdated."), - - /** The merchant needs to do KYC first, the payment could not be completed. */ - WALLET_PAY_MERCHANT_KYC_MISSING(7040, 0, "The merchant needs to do KYC first, the payment could not be completed."), - - /** A peer-pull-debit transaction was aborted because the exchange reported the purse as gone. */ - WALLET_PEER_PULL_DEBIT_PURSE_GONE(7041, 0, "A peer-pull-debit transaction was aborted because the exchange reported the purse as gone."), - - /** A transaction was aborted on explicit request by the user. */ - WALLET_TRANSACTION_ABORTED_BY_USER(7042, 0, "A transaction was aborted on explicit request by the user."), - - /** A transaction was abandoned on explicit request by the user. */ - WALLET_TRANSACTION_ABANDONED_BY_USER(7043, 0, "A transaction was abandoned on explicit request by the user."), - - /** A payment was attempted, but the merchant claims the order is gone (likely expired). */ - WALLET_PAY_MERCHANT_ORDER_GONE(7044, 0, "A payment was attempted, but the merchant claims the order is gone (likely expired)."), - - /** The wallet does not have an entry for the requested exchange. */ - WALLET_EXCHANGE_ENTRY_NOT_FOUND(7045, 0, "The wallet does not have an entry for the requested exchange."), - - /** The wallet is not able to process the request due to the transaction's state. */ - WALLET_REQUEST_TRANSACTION_STATE_UNSUPPORTED(7046, 0, "The wallet is not able to process the request due to the transaction's state."), - - /** A transaction could not be processed due to an unrecoverable protocol violation. */ - WALLET_TRANSACTION_PROTOCOL_VIOLATION(7047, 0, "A transaction could not be processed due to an unrecoverable protocol violation."), - - /** A parameter in the request is malformed or missing. */ - WALLET_CORE_API_BAD_REQUEST(7048, 0, "A parameter in the request is malformed or missing."), - - /** The order could not be found. Maybe the merchant deleted it. */ - WALLET_MERCHANT_ORDER_NOT_FOUND(7049, 0, "The order could not be found. Maybe the merchant deleted it."), - - /** We encountered a timeout with our payment backend. */ - ANASTASIS_GENERIC_BACKEND_TIMEOUT(8000, 504, "We encountered a timeout with our payment backend."), - - /** The backend requested payment, but the request is malformed. */ - ANASTASIS_GENERIC_INVALID_PAYMENT_REQUEST(8001, 0, "The backend requested payment, but the request is malformed."), - - /** The backend got an unexpected reply from the payment processor. */ - ANASTASIS_GENERIC_BACKEND_ERROR(8002, 502, "The backend got an unexpected reply from the payment processor."), - - /** The \"Content-length\" field for the upload is missing. */ - ANASTASIS_GENERIC_MISSING_CONTENT_LENGTH(8003, 400, "The \"Content-length\" field for the upload is missing."), - - /** The \"Content-length\" field for the upload is malformed. */ - ANASTASIS_GENERIC_MALFORMED_CONTENT_LENGTH(8004, 400, "The \"Content-length\" field for the upload is malformed."), - - /** The backend failed to setup an order with the payment processor. */ - ANASTASIS_GENERIC_ORDER_CREATE_BACKEND_ERROR(8005, 502, "The backend failed to setup an order with the payment processor."), - - /** The backend was not authorized to check for payment with the payment processor. */ - ANASTASIS_GENERIC_PAYMENT_CHECK_UNAUTHORIZED(8006, 500, "The backend was not authorized to check for payment with the payment processor."), - - /** The backend could not check payment status with the payment processor. */ - ANASTASIS_GENERIC_PAYMENT_CHECK_START_FAILED(8007, 500, "The backend could not check payment status with the payment processor."), - - /** The Anastasis provider could not be reached. */ - ANASTASIS_GENERIC_PROVIDER_UNREACHABLE(8008, 0, "The Anastasis provider could not be reached."), - - /** HTTP server experienced a timeout while awaiting promised payment. */ - ANASTASIS_PAYMENT_GENERIC_TIMEOUT(8009, 408, "HTTP server experienced a timeout while awaiting promised payment."), - - /** The key share is unknown to the provider. */ - ANASTASIS_TRUTH_UNKNOWN(8108, 404, "The key share is unknown to the provider."), - - /** The authorization method used for the key share is no longer supported by the provider. */ - ANASTASIS_TRUTH_AUTHORIZATION_METHOD_NO_LONGER_SUPPORTED(8109, 500, "The authorization method used for the key share is no longer supported by the provider."), - - /** The client needs to respond to the challenge. */ - ANASTASIS_TRUTH_CHALLENGE_RESPONSE_REQUIRED(8110, 403, "The client needs to respond to the challenge."), - - /** The client's response to the challenge was invalid. */ - ANASTASIS_TRUTH_CHALLENGE_FAILED(8111, 403, "The client's response to the challenge was invalid."), - - /** The backend is not aware of having issued the provided challenge code. Either this is the wrong code, or it has expired. */ - ANASTASIS_TRUTH_CHALLENGE_UNKNOWN(8112, 404, "The backend is not aware of having issued the provided challenge code. Either this is the wrong code, or it has expired."), - - /** The backend failed to initiate the authorization process. */ - ANASTASIS_TRUTH_AUTHORIZATION_START_FAILED(8114, 500, "The backend failed to initiate the authorization process."), - - /** The authorization succeeded, but the key share is no longer available. */ - ANASTASIS_TRUTH_KEY_SHARE_GONE(8115, 404, "The authorization succeeded, but the key share is no longer available."), - - /** The backend forgot the order we asked the client to pay for */ - ANASTASIS_TRUTH_ORDER_DISAPPEARED(8116, 502, "The backend forgot the order we asked the client to pay for"), - - /** The backend itself reported a bad exchange interaction. */ - ANASTASIS_TRUTH_BACKEND_EXCHANGE_BAD(8117, 502, "The backend itself reported a bad exchange interaction."), - - /** The backend reported a payment status we did not expect. */ - ANASTASIS_TRUTH_UNEXPECTED_PAYMENT_STATUS(8118, 500, "The backend reported a payment status we did not expect."), - - /** The backend failed to setup the order for payment. */ - ANASTASIS_TRUTH_PAYMENT_CREATE_BACKEND_ERROR(8119, 502, "The backend failed to setup the order for payment."), - - /** The decryption of the key share failed with the provided key. */ - ANASTASIS_TRUTH_DECRYPTION_FAILED(8120, 400, "The decryption of the key share failed with the provided key."), - - /** The request rate is too high. The server is refusing requests to guard against brute-force attacks. */ - ANASTASIS_TRUTH_RATE_LIMITED(8121, 429, "The request rate is too high. The server is refusing requests to guard against brute-force attacks."), - - /** A request to issue a challenge is not valid for this authentication method. */ - ANASTASIS_TRUTH_CHALLENGE_WRONG_METHOD(8123, 400, "A request to issue a challenge is not valid for this authentication method."), - - /** The backend failed to store the key share because the UUID is already in use. */ - ANASTASIS_TRUTH_UPLOAD_UUID_EXISTS(8150, 409, "The backend failed to store the key share because the UUID is already in use."), - - /** The backend failed to store the key share because the authorization method is not supported. */ - ANASTASIS_TRUTH_UPLOAD_METHOD_NOT_SUPPORTED(8151, 400, "The backend failed to store the key share because the authorization method is not supported."), - - /** The provided phone number is not an acceptable number. */ - ANASTASIS_SMS_PHONE_INVALID(8200, 409, "The provided phone number is not an acceptable number."), - - /** Failed to run the SMS transmission helper process. */ - ANASTASIS_SMS_HELPER_EXEC_FAILED(8201, 500, "Failed to run the SMS transmission helper process."), - - /** Provider failed to send SMS. Helper terminated with a non-successful result. */ - ANASTASIS_SMS_HELPER_COMMAND_FAILED(8202, 500, "Provider failed to send SMS. Helper terminated with a non-successful result."), - - /** The provided email address is not an acceptable address. */ - ANASTASIS_EMAIL_INVALID(8210, 409, "The provided email address is not an acceptable address."), - - /** Failed to run the E-mail transmission helper process. */ - ANASTASIS_EMAIL_HELPER_EXEC_FAILED(8211, 500, "Failed to run the E-mail transmission helper process."), - - /** Provider failed to send E-mail. Helper terminated with a non-successful result. */ - ANASTASIS_EMAIL_HELPER_COMMAND_FAILED(8212, 500, "Provider failed to send E-mail. Helper terminated with a non-successful result."), - - /** The provided postal address is not an acceptable address. */ - ANASTASIS_POST_INVALID(8220, 409, "The provided postal address is not an acceptable address."), - - /** Failed to run the mail transmission helper process. */ - ANASTASIS_POST_HELPER_EXEC_FAILED(8221, 500, "Failed to run the mail transmission helper process."), - - /** Provider failed to send mail. Helper terminated with a non-successful result. */ - ANASTASIS_POST_HELPER_COMMAND_FAILED(8222, 500, "Provider failed to send mail. Helper terminated with a non-successful result."), - - /** The provided IBAN address is not an acceptable IBAN. */ - ANASTASIS_IBAN_INVALID(8230, 409, "The provided IBAN address is not an acceptable IBAN."), - - /** The provider has not yet received the IBAN wire transfer authorizing the disclosure of the key share. */ - ANASTASIS_IBAN_MISSING_TRANSFER(8231, 403, "The provider has not yet received the IBAN wire transfer authorizing the disclosure of the key share."), - - /** The backend did not find a TOTP key in the data provided. */ - ANASTASIS_TOTP_KEY_MISSING(8240, 409, "The backend did not find a TOTP key in the data provided."), - - /** The key provided does not satisfy the format restrictions for an Anastasis TOTP key. */ - ANASTASIS_TOTP_KEY_INVALID(8241, 409, "The key provided does not satisfy the format restrictions for an Anastasis TOTP key."), - - /** The given if-none-match header is malformed. */ - ANASTASIS_POLICY_BAD_IF_NONE_MATCH(8301, 400, "The given if-none-match header is malformed."), - - /** The server is out of memory to handle the upload. Trying again later may succeed. */ - ANASTASIS_POLICY_OUT_OF_MEMORY_ON_CONTENT_LENGTH(8304, 413, "The server is out of memory to handle the upload. Trying again later may succeed."), - - /** The signature provided in the \"Anastasis-Policy-Signature\" header is malformed or missing. */ - ANASTASIS_POLICY_BAD_SIGNATURE(8305, 400, "The signature provided in the \"Anastasis-Policy-Signature\" header is malformed or missing."), - - /** The given if-match header is malformed. */ - ANASTASIS_POLICY_BAD_IF_MATCH(8306, 400, "The given if-match header is malformed."), - - /** The uploaded data does not match the Etag. */ - ANASTASIS_POLICY_INVALID_UPLOAD(8307, 400, "The uploaded data does not match the Etag."), - - /** The provider is unaware of the requested policy. */ - ANASTASIS_POLICY_NOT_FOUND(8350, 404, "The provider is unaware of the requested policy."), - - /** The given action is invalid for the current state of the reducer. */ - ANASTASIS_REDUCER_ACTION_INVALID(8400, 0, "The given action is invalid for the current state of the reducer."), - - /** The given state of the reducer is invalid. */ - ANASTASIS_REDUCER_STATE_INVALID(8401, 0, "The given state of the reducer is invalid."), - - /** The given input to the reducer is invalid. */ - ANASTASIS_REDUCER_INPUT_INVALID(8402, 0, "The given input to the reducer is invalid."), - - /** The selected authentication method does not work for the Anastasis provider. */ - ANASTASIS_REDUCER_AUTHENTICATION_METHOD_NOT_SUPPORTED(8403, 0, "The selected authentication method does not work for the Anastasis provider."), - - /** The given input and action do not work for the current state. */ - ANASTASIS_REDUCER_INPUT_INVALID_FOR_STATE(8404, 0, "The given input and action do not work for the current state."), - - /** We experienced an unexpected failure interacting with the backend. */ - ANASTASIS_REDUCER_BACKEND_FAILURE(8405, 0, "We experienced an unexpected failure interacting with the backend."), - - /** The contents of a resource file did not match our expectations. */ - ANASTASIS_REDUCER_RESOURCE_MALFORMED(8406, 0, "The contents of a resource file did not match our expectations."), - - /** A required resource file is missing. */ - ANASTASIS_REDUCER_RESOURCE_MISSING(8407, 0, "A required resource file is missing."), - - /** An input did not match the regular expression. */ - ANASTASIS_REDUCER_INPUT_REGEX_FAILED(8408, 0, "An input did not match the regular expression."), - - /** An input did not match the custom validation logic. */ - ANASTASIS_REDUCER_INPUT_VALIDATION_FAILED(8409, 0, "An input did not match the custom validation logic."), - - /** Our attempts to download the recovery document failed with all providers. Most likely the personal information you entered differs from the information you provided during the backup process and you should go back to the previous step. Alternatively, if you used a backup provider that is unknown to this application, you should add that provider manually. */ - ANASTASIS_REDUCER_POLICY_LOOKUP_FAILED(8410, 0, "Our attempts to download the recovery document failed with all providers. Most likely the personal information you entered differs from the information you provided during the backup process and you should go back to the previous step. Alternatively, if you used a backup provider that is unknown to this application, you should add that provider manually."), - - /** Anastasis provider reported a fatal failure. */ - ANASTASIS_REDUCER_BACKUP_PROVIDER_FAILED(8411, 0, "Anastasis provider reported a fatal failure."), - - /** Anastasis provider failed to respond to the configuration request. */ - ANASTASIS_REDUCER_PROVIDER_CONFIG_FAILED(8412, 0, "Anastasis provider failed to respond to the configuration request."), - - /** The policy we downloaded is malformed. Must have been a client error while creating the backup. */ - ANASTASIS_REDUCER_POLICY_MALFORMED(8413, 0, "The policy we downloaded is malformed. Must have been a client error while creating the backup."), - - /** We failed to obtain the policy, likely due to a network issue. */ - ANASTASIS_REDUCER_NETWORK_FAILED(8414, 0, "We failed to obtain the policy, likely due to a network issue."), - - /** The recovered secret did not match the required syntax. */ - ANASTASIS_REDUCER_SECRET_MALFORMED(8415, 0, "The recovered secret did not match the required syntax."), - - /** The challenge data provided is too large for the available providers. */ - ANASTASIS_REDUCER_CHALLENGE_DATA_TOO_BIG(8416, 0, "The challenge data provided is too large for the available providers."), - - /** The provided core secret is too large for some of the providers. */ - ANASTASIS_REDUCER_SECRET_TOO_BIG(8417, 0, "The provided core secret is too large for some of the providers."), - - /** The provider returned in invalid configuration. */ - ANASTASIS_REDUCER_PROVIDER_INVALID_CONFIG(8418, 0, "The provider returned in invalid configuration."), - - /** The reducer encountered an internal error, likely a bug that needs to be reported. */ - ANASTASIS_REDUCER_INTERNAL_ERROR(8419, 0, "The reducer encountered an internal error, likely a bug that needs to be reported."), - - /** The reducer already synchronized with all providers. */ - ANASTASIS_REDUCER_PROVIDERS_ALREADY_SYNCED(8420, 0, "The reducer already synchronized with all providers."), - - /** The Donau failed to perform the operation as it could not find the private keys. This is a problem with the Donau setup, not with the client's request. */ - DONAU_GENERIC_KEYS_MISSING(8607, 503, "The Donau failed to perform the operation as it could not find the private keys. This is a problem with the Donau setup, not with the client's request."), - - /** The signature of the charity key is not valid. */ - DONAU_CHARITY_SIGNATURE_INVALID(8608, 403, "The signature of the charity key is not valid."), - - /** The charity is unknown. */ - DONAU_CHARITY_NOT_FOUND(8609, 404, "The charity is unknown."), - - /** The donation amount specified in the request exceeds the limit of the charity. */ - DONAU_EXCEEDING_DONATION_LIMIT(8610, 400, "The donation amount specified in the request exceeds the limit of the charity."), - - /** The Donau is not aware of the donation unit requested for the operation. */ - DONAU_GENERIC_DONATION_UNIT_UNKNOWN(8611, 404, "The Donau is not aware of the donation unit requested for the operation."), - - /** The Donau failed to talk to the process responsible for its private donation unit keys or the helpers had no donation units (properly) configured. */ - DONAU_DONATION_UNIT_HELPER_UNAVAILABLE(8612, 502, "The Donau failed to talk to the process responsible for its private donation unit keys or the helpers had no donation units (properly) configured."), - - /** The Donau failed to talk to the process responsible for its private signing keys. */ - DONAU_SIGNKEY_HELPER_UNAVAILABLE(8613, 502, "The Donau failed to talk to the process responsible for its private signing keys."), - - /** The response from the online signing key helper process was malformed. */ - DONAU_SIGNKEY_HELPER_BUG(8614, 500, "The response from the online signing key helper process was malformed."), - - /** The number of segments included in the URI does not match the number of segments expected by the endpoint. */ - DONAU_GENERIC_WRONG_NUMBER_OF_SEGMENTS(8615, 404, "The number of segments included in the URI does not match the number of segments expected by the endpoint."), - - /** The signature of the donation receipt is not valid. */ - DONAU_DONATION_RECEIPT_SIGNATURE_INVALID(8616, 403, "The signature of the donation receipt is not valid."), - - /** The client reused a unique donor identifier nonce, which is not allowed. */ - DONAU_DONOR_IDENTIFIER_NONCE_REUSE(8617, 409, "The client reused a unique donor identifier nonce, which is not allowed."), - - /** A charity with the same public key is already registered. */ - DONAU_CHARITY_PUB_EXISTS(8618, 404, "A charity with the same public key is already registered."), - - /** A generic error happened in the LibEuFin nexus. See the enclose details JSON for more information. */ - LIBEUFIN_NEXUS_GENERIC_ERROR(9000, 0, "A generic error happened in the LibEuFin nexus. See the enclose details JSON for more information."), - - /** An uncaught exception happened in the LibEuFin nexus service. */ - LIBEUFIN_NEXUS_UNCAUGHT_EXCEPTION(9001, 500, "An uncaught exception happened in the LibEuFin nexus service."), - - /** A generic error happened in the LibEuFin sandbox. See the enclose details JSON for more information. */ - LIBEUFIN_SANDBOX_GENERIC_ERROR(9500, 0, "A generic error happened in the LibEuFin sandbox. See the enclose details JSON for more information."), - - /** An uncaught exception happened in the LibEuFin sandbox service. */ - LIBEUFIN_SANDBOX_UNCAUGHT_EXCEPTION(9501, 500, "An uncaught exception happened in the LibEuFin sandbox service."), - - /** This validation method is not supported by the service. */ - TALDIR_METHOD_NOT_SUPPORTED(9600, 404, "This validation method is not supported by the service."), - - /** Number of allowed attempts for initiating a challenge exceeded. */ - TALDIR_REGISTER_RATE_LIMITED(9601, 429, "Number of allowed attempts for initiating a challenge exceeded."), - - /** The client is unknown or unauthorized. */ - CHALLENGER_GENERIC_CLIENT_UNKNOWN(9750, 404, "The client is unknown or unauthorized."), - - /** The client is not authorized to use the given redirect URI. */ - CHALLENGER_GENERIC_CLIENT_FORBIDDEN_BAD_REDIRECT_URI(9751, 403, "The client is not authorized to use the given redirect URI."), - - /** The service failed to execute its helper process to send the challenge. */ - CHALLENGER_HELPER_EXEC_FAILED(9752, 500, "The service failed to execute its helper process to send the challenge."), - - /** The grant is unknown to the service (it could also have expired). */ - CHALLENGER_GRANT_UNKNOWN(9753, 404, "The grant is unknown to the service (it could also have expired)."), - - /** The code given is not even well-formed. */ - CHALLENGER_CLIENT_FORBIDDEN_BAD_CODE(9754, 403, "The code given is not even well-formed."), - - /** The service is not aware of the referenced validation process. */ - CHALLENGER_GENERIC_VALIDATION_UNKNOWN(9755, 404, "The service is not aware of the referenced validation process."), - - /** The code given is not valid. */ - CHALLENGER_CLIENT_FORBIDDEN_INVALID_CODE(9756, 403, "The code given is not valid."), - - /** Too many attempts have been made, validation is temporarily disabled for this address. */ - CHALLENGER_TOO_MANY_ATTEMPTS(9757, 429, "Too many attempts have been made, validation is temporarily disabled for this address."), - - /** The PIN code provided is incorrect. */ - CHALLENGER_INVALID_PIN(9758, 403, "The PIN code provided is incorrect."), - - /** The token cannot be valid as no address was ever provided by the client. */ - CHALLENGER_MISSING_ADDRESS(9759, 409, "The token cannot be valid as no address was ever provided by the client."), - - /** The client is not allowed to change the address being validated. */ - CHALLENGER_CLIENT_FORBIDDEN_READ_ONLY(9760, 403, "The client is not allowed to change the address being validated."), - - /** End of error code range. */ - END(9999, 0, "End of error code range."), - - -} diff --git a/libeufin-common/src/main/kotlin/TalerMessage.kt b/libeufin-common/src/main/kotlin/TalerMessage.kt @@ -1,508 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2024, 2025, 2026 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.common - -import java.nio.ByteBuffer -import java.nio.ByteOrder -import java.security.MessageDigest -import io.github.smiley4.schemakenerator.core.annotations.Description -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import tech.libeufin.common.crypto.CryptoUtil.hashStringNbo -import tech.libeufin.common.crypto.NBO - -enum class IncomingType { - reserve, - kyc, - map -} - -@Description("State of a wire transfer") -enum class TransferStatusState { - pending, - transient_failure, - permanent_failure, - success -} - -/** Response GET /taler-wire-gateway/config */ -@Serializable -@Description("Wire gateway configuration response") -data class WireGatewayConfig( - @Description("Currency supported by the gateway") - val currency: String, - @Description("Whether account check is supported") - val support_account_check: Boolean -) { - @Description("API name identifier") - val name: String = "taler-wire-gateway" - @Description("API version string") - val version: String = WIRE_GATEWAY_API_VERSION -} - -/** Request POST /taler-wire-gateway/transfer */ -@Serializable -@Description("Wire transfer request") -data class TransferRequest( - @Description("Unique identifier for this request") - val request_uid: HashCode, - @Description("Amount to transfer") - val amount: TalerAmount, - @Description("Base URL of the exchange") - val exchange_base_url: BaseURL, - @Description("Wire transfer identifier") - val wtid: ShortHashCode, - @Description("Payto URI of the credit account") - val credit_account: Payto, - @Description("Optional transfer metadata") - val metadata: String? = null, -) { - init { - if (metadata != null && !METADATA_REGEX.matches(metadata)) - throw badRequest("metadata '$metadata' is malformed, must match [a-zA-Z0-9-.+:]{1,40}") - } - - companion object { - private val METADATA_REGEX = Regex("^[a-zA-Z0-9-.:]{1,40}$") - } -} - -/** Response POST /taler-wire-gateway/transfer */ -@Serializable -@Description("Wire transfer response") -data class TransferResponse( - @Description("Timestamp of the transfer") - val timestamp: TalerTimestamp, - @Description("Database row identifier") - val row_id: Long -) - -/** Request GET /taler-wire-gateway/transfers */ -@Serializable -@Description("List of wire transfers") -data class TransferList( - @Description("List of transfer statuses") - val transfers: List<TransferListStatus>, - @Description("Payto URI of the debit account") - val debit_account: String -) - -@Serializable -@Description("Transfer status in a list response") -data class TransferListStatus( - @Description("Database row identifier") - val row_id: Long, - @Description("Current transfer status") - val status: TransferStatusState, - @Description("Transfer amount") - val amount: TalerAmount, - @Description("Payto URI of the credit account") - val credit_account: String, - @Description("Timestamp of the transfer") - val timestamp: TalerTimestamp -) - -/** Request GET /taler-wire-gateway/transfers/{ROW_iD} */ -@Serializable -@Description("Detailed status of a single transfer") -data class TransferStatus( - @Description("Current transfer status") - val status: TransferStatusState, - @Description("Optional status message") - val status_msg: String? = null, - @Description("Transfer amount") - val amount: TalerAmount, - @Description("URL of the originating exchange") - val origin_exchange_url: String, - @Description("Optional transfer metadata") - val metadata: String? = null, - @Description("Wire transfer identifier") - val wtid: ShortHashCode, - @Description("Payto URI of the credit account") - val credit_account: String, - @Description("Timestamp of the transfer") - val timestamp: TalerTimestamp -) - -/** Request POST /taler-wire-gateway/admin/add-incoming */ -@Serializable -@Description("Request to add an incoming transaction") -data class AddIncomingRequest( - @Description("Amount of the incoming transaction") - val amount: TalerAmount, - @Description("Reserve public key") - val reserve_pub: EddsaPublicKey, - @Description("Payto URI of the debit account") - val debit_account: Payto -) - -/** Response POST /taler-wire-gateway/admin/add-incoming */ -@Serializable -@Description("Response to adding an incoming transaction") -data class AddIncomingResponse( - @Description("Timestamp of the transaction") - val timestamp: TalerTimestamp, - @Description("Database row identifier") - val row_id: Long -) - -/** Request POST /taler-wire-gateway/admin/add-kycauth */ -@Serializable -@Description("Request to add a KYC auth transaction") -data class AddKycauthRequest( - @Description("Amount of the KYC auth transaction") - val amount: TalerAmount, - @Description("Account public key for KYC") - val account_pub: EddsaPublicKey, - @Description("Payto URI of the debit account") - val debit_account: Payto -) - -/** Request POST /taler-wire-gateway/admin/add-mapped */ -@Serializable -data class AddMappedRequest( - val amount: TalerAmount, - val authorization_pub: EddsaPublicKey, - val debit_account: Payto -) - -/** Response GET /taler-wire-gateway/history/incoming */ -@Serializable -@Description("History of incoming transactions") -data class IncomingHistory( - @Description("List of incoming transactions") - val incoming_transactions: List<IncomingBankTransaction>, - @Description("Payto URI of the credit account") - val credit_account: String -) - -/** Inner response GET /taler-wire-gateway/history/incoming */ -@Serializable -@Description("Incoming bank transaction details") -sealed interface IncomingBankTransaction { - val row_id: Long - val date: TalerTimestamp - val amount: TalerAmount - val debit_account: String - val credit_fee: TalerAmount? -} - -@Serializable -@SerialName("KYCAUTH") -@Description("Incoming KYC authentication transaction") -data class IncomingKycAuthTransaction( - @Description("Database row identifier") - override val row_id: Long, - @Description("Timestamp of the transaction") - override val date: TalerTimestamp, - @Description("Transaction amount") - override val amount: TalerAmount, - @Description("Optional credit fee") - override val credit_fee: TalerAmount? = null, - @Description("Payto URI of the debit account") - override val debit_account: String, - @Description("Account public key for KYC") - val account_pub: EddsaPublicKey, - @Description("Optional authorization public key") - val authorization_pub: EddsaPublicKey? = null, - @Description("Optional authorization signature") - val authorization_sig: EddsaSignature? = null, -) : IncomingBankTransaction - -@Serializable -@SerialName("RESERVE") -@Description("Incoming reserve transaction") -data class IncomingReserveTransaction( - @Description("Database row identifier") - override val row_id: Long, - @Description("Timestamp of the transaction") - override val date: TalerTimestamp, - @Description("Transaction amount") - override val amount: TalerAmount, - @Description("Optional credit fee") - override val credit_fee: TalerAmount? = null, - @Description("Payto URI of the debit account") - override val debit_account: String, - @Description("Reserve public key") - val reserve_pub: EddsaPublicKey, - @Description("Optional authorization public key") - val authorization_pub: EddsaPublicKey? = null, - @Description("Optional authorization signature") - val authorization_sig: EddsaSignature? = null, -) : IncomingBankTransaction - -@Serializable -@SerialName("WAD") -@Description("Incoming WAD transaction") -data class IncomingWadTransaction( - @Description("Database row identifier") - override val row_id: Long, - @Description("Timestamp of the transaction") - override val date: TalerTimestamp, - @Description("Transaction amount") - override val amount: TalerAmount, - @Description("Optional credit fee") - override val credit_fee: TalerAmount? = null, - @Description("Payto URI of the debit account") - override val debit_account: String, - @Description("URL of the originating exchange") - val origin_exchange_url: String, - @Description("WAD identifier") - val wad_id: String // TODO 24 bytes Base32 -) : IncomingBankTransaction - -/** Response GET /taler-wire-gateway/history/outgoing */ -@Serializable -@Description("History of outgoing transactions") -data class OutgoingHistory( - @Description("List of outgoing transactions") - val outgoing_transactions: List<OutgoingTransaction>, - @Description("Payto URI of the debit account") - val debit_account: String -) - -/** Inner response GET /taler-wire-gateway/history/outgoing */ -@Serializable -@Description("Single outgoing transaction details") -data class OutgoingTransaction( - @Description("Database row identifier") - val row_id: Long, // DB row ID of the payment. - @Description("Timestamp of the transaction") - val date: TalerTimestamp, - @Description("Transaction amount") - val amount: TalerAmount, - @Description("Payto URI of the credit account") - val credit_account: String, - @Description("Wire transfer identifier") - val wtid: ShortHashCode, - @Description("Base URL of the exchange") - val exchange_base_url: String, - @Description("Optional transfer metadata") - val metadata: String? = null, - @Description("Optional debit fee") - val debit_fee: TalerAmount? = null -) - -/** Response GET /taler-wire-gateway/account/check */ -@Serializable -@Description("Account information response") -class AccountInfo() - -/** Response GET /taler-prepared-transfer/config */ -@Serializable -@Description("Prepared transfer configuration") -data class PreparedTransferConfig( - @Description("Currency supported") - val currency: String, - @Description("List of supported subject formats") - val supported_formats: List<SubjectFormat> -) { - @Description("API name identifier") - val name: String = "taler-prepared-transfer" - @Description("API version string") - val version: String = WIRE_TRANSFER_API_VERSION -} - - -/** Inner response GET /taler-prepared-transfer/registration */ -@Serializable -@Description("Transfer subject information") -sealed interface TransferSubject { - @Serializable - @SerialName("SIMPLE") - @Description("Simple text transfer subject") - data class Simple( - @Description("Plain text transfer subject") - val subject: String, - @Description("Credit amount for the transfer") - val credit_amount: TalerAmount - ) : TransferSubject - - @Serializable - @SerialName("URI") - @Description("URI-based transfer subject") - data class Uri( - @Description("Taler URI for the transfer") - val uri: String, - @Description("Credit amount for the transfer") - val credit_amount: TalerAmount - ) : TransferSubject - - @Serializable - @SerialName("CH_QR_BILL") - @Description("Swiss QR bill transfer subject") - data class QrBill( - @Description("QR reference number for the bill") - val qr_reference_number: String, - @Description("Credit amount for the transfer") - val credit_amount: TalerAmount, - ) : TransferSubject -} - -@Serializable -@Description("Supported transfer subject format") -enum class SubjectFormat { - SIMPLE, - URI, - CH_QR_BILL -} - -@Serializable -@Description("Public key algorithm") -enum class PublicKeyAlg { - EdDSA -} - -@Serializable -@Description("Type of wire transfer") -enum class TransferType { - reserve, - kyc -} - -@Serializable -@Description("Request to generate a transfer subject") -data class SubjectRequest( - @Description("Payto URI of the credit account") - val credit_account: Payto, - @Description("Type of transfer") - val type: TransferType, - @Description("Whether subject is recurrent") - val recurrent: Boolean, - @Description("Credit amount for the transfer") - val credit_amount: TalerAmount, - @Description("Public key algorithm") - val alg: PublicKeyAlg, - @Description("Account public key") - val account_pub: EddsaPublicKey, - @Description("Authorization public key") - val authorization_pub: EddsaPublicKey, - @Description("Authorization signature") - val authorization_sig: EddsaSignature, -) : NBO { - /* Network bytes */ - override fun nbo(): ByteArray = ByteBuffer.allocate(104).apply { - order(ByteOrder.BIG_ENDIAN) - putInt(capacity()) - putInt(1224) - put(hashStringNbo(credit_account.toString())) - put(credit_amount.nbo()) - putInt(when (type) { - TransferType.reserve -> 1 - TransferType.kyc -> 2 - }) - putShort(if (recurrent) 2 else 1) - putShort(when (alg) { - PublicKeyAlg.EdDSA -> 1 - }) - put(account_pub.raw) - }.array() - - fun sign(priv: ByteArray): SubjectRequest = - this.copy(authorization_sig = this.signNbo(priv)) - - fun verify(): Boolean = - this.verifyNbo(this.authorization_sig, this.authorization_pub) -} - -@Serializable -@Description("Result of subject generation") -data class SubjectResult( - @Description("List of generated transfer subjects") - val subjects: List<TransferSubject>, - @Description("Expiration timestamp of the subjects") - val expiration: TalerTimestamp -) - -@Serializable -@Description("Request to unregister a subject") -data class Unregistration( - @Description("Timestamp of the unregistration") - val timestamp: TalerTimestamp, - @Description("Authorization public key") - val authorization_pub: EddsaPublicKey, - @Description("Authorization signature") - val authorization_sig: EddsaSignature -) : NBO { - /* Network bytes */ - override fun nbo(): ByteArray = ByteBuffer.allocate(16).apply { - order(ByteOrder.BIG_ENDIAN) - putInt(capacity()) - putInt(1225) - putLong(timestamp.instant.epochSecond * 1000 * 1000) - }.array() - - fun sign(priv: ByteArray): Unregistration = - this.copy(authorization_sig = this.signNbo(priv)) - - fun verify(): Boolean = - this.verifyNbo(this.authorization_sig, this.authorization_pub) -} - -/** Response GET /taler-revenue/config */ -@Serializable -@Description("Revenue API configuration response") -data class RevenueConfig( - @Description("Currency supported by the API") - val currency: String -) { - @Description("API name identifier") - val name: String = "taler-revenue" - @Description("API version string") - val version: String = REVENUE_API_VERSION -} - -/** Request GET /taler-revenue/history */ -@Serializable -@Description("History of revenue incoming transactions") -data class RevenueIncomingHistory( - @Description("List of incoming revenue transactions") - val incoming_transactions: List<RevenueIncomingBankTransaction>, - @Description("Payto URI of the credit account") - val credit_account: String -) - -/** Inner request GET /taler-revenue/history */ -@Serializable -@Description("Single revenue incoming bank transaction") -data class RevenueIncomingBankTransaction( - @Description("Database row identifier") - val row_id: Long, - @Description("Timestamp of the transaction") - val date: TalerTimestamp, - @Description("Transaction amount") - val amount: TalerAmount, - @Description("Optional credit fee") - val credit_fee: TalerAmount? = null, - @Description("Payto URI of the debit account") - val debit_account: String, - @Description("Transaction subject line") - val subject: String -) - -/** Response GET /taler-observability/config */ -@Serializable -@Description("Observability API configuration response") -class TalerObservabilityConfig() { - @Description("API name identifier") - val name: String = "taler-observability" - @Description("API version string") - val version: String = OBSERVABILITY_API_VERSION -} diff --git a/libeufin-common/src/main/kotlin/api/auth.kt b/libeufin-common/src/main/kotlin/api/auth.kt @@ -1,58 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2025 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.common.api - -import io.ktor.http.* -import io.ktor.server.response.* -import io.ktor.server.routing.* -import tech.libeufin.common.* -import tech.libeufin.common.api.intercept - -/** Apply authentication api configuration for a route */ -fun Route.apiAuth(auth: AuthMethod, callback: Route.() -> Unit): Route = - intercept("Auth", callback) { - if (auth != AuthMethod.None) { - val header = this.request.headers[HttpHeaders.Authorization] - val (expectedScheme, token) = when (auth) { - is AuthMethod.Basic -> "Basic" to auth.token - is AuthMethod.Bearer -> "Bearer" to auth.token - else -> throw UnsupportedOperationException() - } - - if (header == null) { - this.response.header(HttpHeaders.WWWAuthenticate, expectedScheme) - throw unauthorized( - "Authorization header not found", - TalerErrorCode.GENERIC_PARAMETER_MISSING - ) - } - val (scheme, content) = header.splitOnce(" ") ?: throw badRequest( - "Authorization is invalid", - TalerErrorCode.GENERIC_HTTP_HEADERS_MALFORMED - ) - if (scheme == expectedScheme) { - if (content != token) { - throw unauthorized("Unknown token", TalerErrorCode.GENERIC_TOKEN_UNKNOWN) - } - } else { - throw unauthorized("Expected scheme $expectedScheme got '$scheme'") - } - } - } -\ No newline at end of file diff --git a/libeufin-common/src/main/kotlin/api/route.kt b/libeufin-common/src/main/kotlin/api/route.kt @@ -1,41 +0,0 @@ -/* - * 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.common.api - -import io.ktor.server.application.* -import io.ktor.server.routing.* -import io.ktor.util.pipeline.* - -fun Route.intercept(name: String, build: Route.() -> Unit, lambda: suspend ApplicationCall.() -> Unit): Route { - val plugin = createRouteScopedPlugin(name) { - onCall { call -> call.lambda() } - } - val subRoute = createChild(TransparentRouteSelector()) - subRoute.install(plugin) - subRoute.build() - return subRoute -} - -private class TransparentRouteSelector : RouteSelector() { - override suspend fun evaluate(context: RoutingResolveContext, segmentIndex: Int): RouteSelectorEvaluation = - RouteSelectorEvaluation.Transparent - - override fun toString(): String = "" -} diff --git a/libeufin-common/src/main/kotlin/api/server.kt b/libeufin-common/src/main/kotlin/api/server.kt @@ -1,372 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2024, 2025, 2026 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.common.api - -import io.github.smiley4.ktoropenapi.OpenApi -import io.github.smiley4.ktoropenapi.OpenApiPlugin -import io.github.smiley4.ktoropenapi.config.* -import io.github.smiley4.ktoropenapi.openApi -import io.github.smiley4.schemakenerator.serialization.SerializationSteps.analyzeTypeUsingKotlinxSerialization -import io.github.smiley4.schemakenerator.swagger.SwaggerSteps.compileReferencingRoot -import io.github.smiley4.schemakenerator.swagger.SwaggerSteps.generateSwaggerSchema -import io.github.smiley4.schemakenerator.swagger.SwaggerSteps.withTitle -import io.github.smiley4.schemakenerator.swagger.SwaggerSteps.RequiredHandling -import io.github.smiley4.schemakenerator.core.CoreSteps.addDiscriminatorProperty -import io.github.smiley4.schemakenerator.core.CoreSteps.handleNameAnnotation -import io.github.smiley4.schemakenerator.swagger.data.* -import io.ktor.http.* -import io.ktor.serialization.kotlinx.json.* -import io.ktor.server.application.* -import io.ktor.server.engine.* -import io.ktor.server.cio.* -import io.ktor.server.plugins.* -import io.ktor.server.plugins.calllogging.* -import io.ktor.server.plugins.contentnegotiation.* -import io.ktor.server.plugins.forwardedheaders.* -import io.ktor.server.plugins.statuspages.* -import io.ktor.server.plugins.callid.* -import io.ktor.server.request.* -import io.ktor.server.response.* -import io.ktor.server.routing.* -import io.ktor.utils.io.* -import io.ktor.util.* -import io.ktor.util.pipeline.* -import io.ktor.http.content.* -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.json.Json -import org.postgresql.util.PSQLState -import org.slf4j.Logger -import org.slf4j.event.Level -import tech.libeufin.common.* -import tech.libeufin.common.db.SERIALIZATION_ERROR -import java.net.InetAddress -import java.sql.SQLException -import java.util.zip.DataFormatException -import java.util.zip.Inflater - -/** Used to store the raw body */ -private val RAW_BODY = AttributeKey<ByteArray>("RAW_BODY") - -/** Used to set custom body limit */ -val BODY_LIMIT = AttributeKey<Int>("BODY_LIMIT") - -/** Get call raw body */ -val ApplicationCall.rawBody: ByteArray get() = attributes.getOrNull(RAW_BODY) ?: ByteArray(0) - -/** - * This plugin apply Taler specific logic - * It checks for body length limit and inflates the requests that have "Content-Encoding: deflate" - * It logs incoming requests and their details - */ -fun talerPlugin(logger: Logger): ApplicationPlugin<Unit> { - return createApplicationPlugin("TalerPlugin") { - onCall { call -> - // Handle CORS - call.response.header(HttpHeaders.AccessControlAllowOrigin, "*") - // Handle CORS preflight - if (call.request.httpMethod == HttpMethod.Options) { - call.response.header(HttpHeaders.AccessControlAllowHeaders, "*") - call.response.header(HttpHeaders.AccessControlAllowMethods, "*") - call.respond(HttpStatusCode.NoContent) - return@onCall - } - - // Log incoming transaction - val requestCall = buildString { - val path = call.request.path() - append(call.request.httpMethod.value) - append(' ') - append(call.request.path()) - val query = call.request.queryString() - if (query.isNotEmpty()) { - append('?') - append(query) - } - } - logger.info(requestCall) - } - onCallReceive { call -> - val bodyLimit = call.attributes.getOrNull(BODY_LIMIT) ?: MAX_BODY_LENGTH - // Check content length if present and wellformed - val contentLenght = call.request.headers[HttpHeaders.ContentLength]?.toIntOrNull() - if (contentLenght != null && contentLenght > bodyLimit) - throw bodyOverflow("Body is suspiciously big > ${bodyLimit}B") - - // Else check while reading and decompressing the body - transformBody { body -> - val bytes = ByteArray(bodyLimit + 1) - var read = 0 - when (val encoding = call.request.headers[HttpHeaders.ContentEncoding]) { - "deflate" -> { - // Decompress and check decompressed length - val inflater = Inflater() - while (!body.isClosedForRead) { - body.read { buf -> - inflater.setInput(buf) - try { - read += inflater.inflate(bytes, read, bytes.size - read) - } catch (e: DataFormatException) { - logger.error("Deflated request failed to inflate: ${e.message}") - throw badRequest( - "Could not inflate request", - TalerErrorCode.GENERIC_COMPRESSION_INVALID - ) - } - } - if (read > bodyLimit) - throw bodyOverflow("Decompressed body is suspiciously big > ${bodyLimit}B") - } - } - null -> { - // Check body length - while (true) { - val new = body.readAvailable(bytes, read, bytes.size - read) - if (new == -1) break // Channel is closed - read += new - if (read > bodyLimit) - throw bodyOverflow("Body is suspiciously big > ${bodyLimit}B") - } - } - else -> throw unsupportedMediaType( - "Content encoding '$encoding' not supported, expected plain or deflate", - TalerErrorCode.GENERIC_COMPRESSION_INVALID - ) - } - logger.trace { - "request ${bytes.sliceArray(0 until read).asUtf8()}" - } - call.attributes.put(RAW_BODY, bytes) - ByteReadChannel(bytes, 0, read) - } - } - } -} - -data class OpenApiInfo( - val title: String, - val version: String, - val description: String? = null, - val securityConfig: SecurityConfig.() -> Unit -) - -/** Set up web server handlers for a Taler API */ -fun Application.talerApi(logger: Logger, openApiInfo: OpenApiInfo? = null, serveSpec: Boolean = false, routes: Routing.() -> Unit) { - if (openApiInfo != null) { - install(OpenApi) { - info { - title = openApiInfo.title - version = openApiInfo.version - description = openApiInfo.description - } - server { - url = "/" - description = "Same host" - } - security(openApiInfo.securityConfig) - outputFormat = OutputFormat.YAML - schemas { - generator = { type -> - type - .analyzeTypeUsingKotlinxSerialization() - .handleNameAnnotation() - .addDiscriminatorProperty("type") - .generateSwaggerSchema { - nullables = RequiredHandling.NON_REQUIRED - optionals = RequiredHandling.REQUIRED - } - .withTitle(TitleType.MINIMAL) - .compileReferencingRoot( - explicitNullTypes = false, - pathType = RefType.OPENAPI_MINIMAL - ) - } - } - } - } - install(CallId) { - generate(10, "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") - verify { true } - } - install(CallLogging) { - callIdMdc("call-id") - level = Level.INFO - this.logger = logger - format { call -> - val status = call.response.status() - val msg = call.logMsg() - if (msg != null) { - "${status?.value} ${call.processingTimeMillis()}ms: $msg" - } else { - "${status?.value} ${call.processingTimeMillis()}ms" - } - } - } - install(XForwardedHeaders) - install(talerPlugin(logger)) - install(IgnoreTrailingSlash) - install(ContentNegotiation) { - json(Json { - @OptIn(ExperimentalSerializationApi::class) - explicitNulls = false - encodeDefaults = true - ignoreUnknownKeys = true - }) - } - install(StatusPages) { - status(HttpStatusCode.NotFound) { call, status -> - call.err( - status, - "There is no endpoint defined for the URL provided by the client. Check if you used the correct URL and/or file a report with the developers of the client software.", - TalerErrorCode.GENERIC_ENDPOINT_UNKNOWN, - null - ) - } - status(HttpStatusCode.MethodNotAllowed) { call, status -> - call.err( - status, - "The HTTP method used is invalid for this endpoint. This is likely a bug in the client implementation. Check if you are using the latest available version and/or file a report with the developers.", - TalerErrorCode.GENERIC_METHOD_INVALID, - null - ) - } - exception<Exception> { call, cause -> - logger.debug("", cause) - when (cause) { - is ApiException -> call.err(cause, null) - is SQLException -> { - if (SERIALIZATION_ERROR.contains(cause.sqlState)) { - call.err( - HttpStatusCode.InternalServerError, - "Transaction serialization failure", - TalerErrorCode.BANK_SOFT_EXCEPTION, - cause - ) - } else { - call.err( - HttpStatusCode.InternalServerError, - "Unexpected sql error with state ${cause.sqlState}", - TalerErrorCode.BANK_UNMANAGED_EXCEPTION, - cause - ) - } - } - is BadRequestException -> { - /** - * NOTE: extracting the root cause helps with JSON error messages, - * because they mention the particular way they are invalid, but OTOH - * it loses (by getting null) other error messages, like for example - * the one from MissingRequestParameterException. Therefore, in order - * to get the most detailed message, we must consider BOTH sides: - * the 'cause' AND its root cause! - */ - var rootCause: Throwable? = cause.cause - while (rootCause?.cause != null) - rootCause = rootCause.cause - // Telling apart invalid JSON vs missing parameter vs invalid parameter. - val errorCode = when { - cause is MissingRequestParameterException -> - TalerErrorCode.GENERIC_PARAMETER_MISSING - cause is ParameterConversionException -> - TalerErrorCode.GENERIC_PARAMETER_MALFORMED - rootCause is CommonError -> when (rootCause) { - is CommonError.AmountFormat -> TalerErrorCode.BANK_BAD_FORMAT_AMOUNT - is CommonError.AmountNumberTooBig -> TalerErrorCode.BANK_NUMBER_TOO_BIG - is CommonError.Payto -> TalerErrorCode.GENERIC_JSON_INVALID - } - else -> TalerErrorCode.GENERIC_JSON_INVALID - } - call.err( - HttpStatusCode.BadRequest, - rootCause?.message, - errorCode, - null - ) - } - is CommonError -> { - val errorCode = when (cause) { - is CommonError.AmountFormat -> TalerErrorCode.BANK_BAD_FORMAT_AMOUNT - is CommonError.AmountNumberTooBig -> TalerErrorCode.BANK_NUMBER_TOO_BIG - is CommonError.Payto -> TalerErrorCode.GENERIC_JSON_INVALID - } - call.err( - HttpStatusCode.BadRequest, - cause.message, - errorCode, - null - ) - } - else -> { - call.err( - HttpStatusCode.InternalServerError, - cause.message, - TalerErrorCode.BANK_UNMANAGED_EXCEPTION, - cause - ) - } - } - } - } - val phase = PipelinePhase("phase") - sendPipeline.insertPhaseBefore(ApplicationSendPipeline.Engine, phase) - sendPipeline.intercept(phase) { response -> - if (logger.isTraceEnabled) { - if (response is OutgoingContent.ByteArrayContent) { - logger.trace("response ${String(response.bytes())}") - } - } - - } - routing { - routes() - if (serveSpec) { - route("openapi.yaml") { - openApi() - } - } - } -} - -// Dirty local variable to stop the server in test TODO remove this ugly hack -var engine: ApplicationEngine? = null - -fun serve(cfg: tech.libeufin.common.ServerConfig, logger: Logger, api: Application.() -> Unit) { - val server = embeddedServer(CIO, - configure = { - when (cfg) { - is ServerConfig.Tcp -> { - for (addr in InetAddress.getAllByName(cfg.addr)) { - logger.info("Listening on ${addr.hostAddress}:${cfg.port}") - connector { - port = cfg.port - host = addr.hostAddress - } - } - } - is ServerConfig.Unix -> { - logger.info("Listening on ${cfg.path}") - unixConnector(cfg.path.toString()) - } - } - }, - module = api - ) - engine = server.engine - server.start(wait = true) -} diff --git a/libeufin-common/src/main/kotlin/client.kt b/libeufin-common/src/main/kotlin/client.kt @@ -1,129 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2023-2025, 2026 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.common - -import io.ktor.client.request.* -import io.ktor.client.statement.* -import io.ktor.http.* -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonElement -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.encodeToJsonElement -import kotlinx.serialization.json.jsonObject -import kotlin.test.assertEquals - -/* ----- Json DSL ----- */ - -inline fun obj(from: JsonObject = JsonObject(emptyMap()), builderAction: JsonBuilder.() -> Unit): JsonObject { - val builder = JsonBuilder(from) - builder.apply(builderAction) - return JsonObject(builder.content) -} - -class JsonBuilder(from: JsonObject) { - val content: MutableMap<String, JsonElement> = from.toMutableMap() - - inline infix fun <reified T> String.to(v: T) { - val json = Json.encodeToJsonElement(kotlinx.serialization.serializer<T>(), v) - content[this] = json - } -} - -/* ----- Json body helper ----- */ - -inline fun <reified B> HttpRequestBuilder.json(b: B) { - val json = Json.encodeToString(kotlinx.serialization.serializer<B>(), b) - contentType(ContentType.Application.Json) - setBody(json) -} - -inline fun <reified B> HttpRequestBuilder.json( - from: B, - builderAction: JsonBuilder.() -> Unit -) { - json(obj(Json.encodeToJsonElement(kotlinx.serialization.serializer<B>(), from).jsonObject, builderAction)) -} - -inline fun HttpRequestBuilder.json( - from: JsonObject = JsonObject(emptyMap()), - builderAction: JsonBuilder.() -> Unit -) { - json(obj(from, builderAction)) -} - -suspend inline fun <reified B> HttpResponse.json(): B = - Json.decodeFromString(kotlinx.serialization.serializer<B>(), bodyAsText()) - -suspend inline fun <reified B> HttpResponse.assertOkJson(lambda: (B) -> Unit = {}): B { - assertOk() - val body = json<B>() - lambda(body) - return body -} - -suspend inline fun <reified B> HttpResponse.assertAcceptedJson(lambda: (B) -> Unit = {}): B { - assertAccepted() - val body = json<B>() - lambda(body) - return body -} - -/* ----- Assert ----- */ - -suspend fun HttpResponse.isStatus(status: HttpStatusCode, err: TalerErrorCode?): Boolean { - if (status != this.status) return false - if (err != null) { - val body = json<TalerError>() - return err.code == body.code - } - return true -} - -suspend fun HttpResponse.assertStatus(status: HttpStatusCode, err: TalerErrorCode?): HttpResponse { - assertEquals(status, this.status, if (err != null) "$err" else err) - if (err != null) { - val body = json<TalerError>() - assertEquals(err.code, body.code) - } - return this -} -suspend fun HttpResponse.assertOk(): HttpResponse - = assertStatus(HttpStatusCode.OK, null) -suspend fun HttpResponse.assertNoContent(err: TalerErrorCode? = null): HttpResponse - = assertStatus(HttpStatusCode.NoContent, err) -suspend fun HttpResponse.assertAccepted(): HttpResponse - = assertStatus(HttpStatusCode.Accepted, null) -suspend fun HttpResponse.assertNotFound(err: TalerErrorCode): HttpResponse - = assertStatus(HttpStatusCode.NotFound, err) -suspend fun HttpResponse.assertUnauthorized(err: TalerErrorCode = TalerErrorCode.GENERIC_UNAUTHORIZED): HttpResponse - = assertStatus(HttpStatusCode.Unauthorized, err) -suspend fun HttpResponse.assertConflict(err: TalerErrorCode): HttpResponse - = assertStatus(HttpStatusCode.Conflict, err) -suspend fun HttpResponse.assertBadRequest(err: TalerErrorCode = TalerErrorCode.GENERIC_JSON_INVALID): HttpResponse - = assertStatus(HttpStatusCode.BadRequest, err) -suspend fun HttpResponse.assertForbidden(err: TalerErrorCode = TalerErrorCode.GENERIC_FORBIDDEN): HttpResponse - = assertStatus(HttpStatusCode.Forbidden, err) -suspend fun HttpResponse.assertNotImplemented(err: TalerErrorCode = TalerErrorCode.END): HttpResponse - = assertStatus(HttpStatusCode.NotImplemented, err) -suspend fun HttpResponse.assertTooManyRequests(err: TalerErrorCode): HttpResponse - = assertStatus(HttpStatusCode.TooManyRequests, err) -suspend fun HttpResponse.assertPayloadTooLarge( - err: TalerErrorCode = TalerErrorCode.GENERIC_UPLOAD_EXCEEDS_LIMIT -): HttpResponse = assertStatus(HttpStatusCode.PayloadTooLarge, err) -\ No newline at end of file diff --git a/libeufin-common/src/main/kotlin/config.kt b/libeufin-common/src/main/kotlin/config.kt @@ -1,65 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2024, 2025, 2026 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.common - -import org.slf4j.Logger -import org.slf4j.LoggerFactory -import java.nio.file.Path - -private val logger: Logger = LoggerFactory.getLogger("libeufin-config") - -sealed interface ServerConfig { - data class Unix(val path: Path): ServerConfig - data class Tcp(val addr: String, val port: Int): ServerConfig -} - -fun TalerConfig.loadServerConfig(section: String): ServerConfig { - val sect = section(section) - return sect.mapLambda("serve", "server method", mapOf( - "tcp" to { ServerConfig.Tcp(sect.string("address").orNull() ?: sect.string("bind_to").require(), sect.number("port").require()) }, - "unix" to { ServerConfig.Unix(sect.path("unixpath").require()) } - )).require() -} - -fun TalerConfigSection.requireAuthMethod(): AuthMethod { - return mapLambda("auth_method", "auth method", mapOf( - "none" to { AuthMethod.None }, - "bearer-token" to { - logger.warn("Deprecated auth method option 'auth_method' used deprecated value 'bearer-token'") - val token = string("auth_bearer_token").require() - AuthMethod.Bearer(token) - }, - "bearer" to { - val token = string("token").require() - AuthMethod.Bearer(token) - }, - "basic" to { - val username = string("username").require() - val password = string("password").require() - AuthMethod.Basic("$username:$password".encodeBase64()) - } - )).require() -} - -sealed interface AuthMethod { - data object None: AuthMethod - data class Bearer(val token: String): AuthMethod - data class Basic(val token: String): AuthMethod -} -\ No newline at end of file diff --git a/libeufin-common/src/main/kotlin/crypto/CryptoUtil.kt b/libeufin-common/src/main/kotlin/crypto/CryptoUtil.kt @@ -1,342 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2024, 2026 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.common.crypto - -import org.bouncycastle.asn1.x500.X500Name -import org.bouncycastle.asn1.x509.BasicConstraints -import org.bouncycastle.asn1.x509.Extension -import org.bouncycastle.asn1.x509.KeyUsage -import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter -import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder -import org.bouncycastle.crypto.generators.Argon2BytesGenerator -import org.bouncycastle.crypto.generators.BCrypt -import org.bouncycastle.crypto.params.Argon2Parameters -import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters -import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters -import org.bouncycastle.crypto.signers.Ed25519Signer -import org.bouncycastle.jce.provider.BouncyCastleProvider -import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder -import tech.libeufin.common.* -import java.io.ByteArrayOutputStream -import java.io.InputStream -import java.math.BigInteger -import java.security.KeyFactory -import java.security.KeyPairGenerator -import java.security.MessageDigest -import java.security.Signature -import java.security.cert.CertificateFactory -import java.security.cert.X509Certificate -import java.security.interfaces.RSAPrivateCrtKey -import java.security.interfaces.RSAPublicKey -import java.security.spec.* -import java.util.* -import javax.crypto.* -import javax.crypto.spec.IvParameterSpec -import javax.crypto.spec.PBEKeySpec -import javax.crypto.spec.SecretKeySpec - -interface NBO { - fun nbo(): ByteArray - - fun signNbo(key: ByteArray): EddsaSignature = CryptoUtil.eddsaSign(this.nbo(), key) - - fun verifyNbo(signature: EddsaSignature, key: EddsaPublicKey): Boolean = CryptoUtil.checkEdssaSignature(this.nbo(), signature, key) -} - -/** Helpers for dealing with cryptographic operations in EBICS / LibEuFin */ -object CryptoUtil { - - private val provider = BouncyCastleProvider() - - /** Load an RSA private key from its binary PKCS#8 encoding */ - fun loadRSAPrivate(encodedPrivateKey: ByteArray): RSAPrivateCrtKey { - val spec = PKCS8EncodedKeySpec(encodedPrivateKey) - val priv = KeyFactory.getInstance("RSA").generatePrivate(spec) - return priv as RSAPrivateCrtKey - } - - /** Load an RSA public key from its binary X509 encoding */ - fun loadRSAPublic(encodedPublicKey: ByteArray): RSAPublicKey { - val spec = X509EncodedKeySpec(encodedPublicKey) - val pub = KeyFactory.getInstance("RSA").generatePublic(spec) - return pub as RSAPublicKey - } - - /** Create an RSA public key from its components: [modulus] and [exponent] */ - fun RSAPublicFromComponents(modulus: ByteArray, exponent: ByteArray): RSAPublicKey { - val modulusBigInt = BigInteger(1, modulus) - val exponentBigInt = BigInteger(1, exponent) - val spec = RSAPublicKeySpec(modulusBigInt, exponentBigInt) - return KeyFactory.getInstance("RSA").generatePublic(spec) as RSAPublicKey - } - - /** Extract an RSA public key from a [raw] X.509 certificate */ - fun RSAPublicFromCertificate(raw: ByteArray): RSAPublicKey { - val certificate = CertificateFactory.getInstance("X.509").generateCertificate(raw.inputStream()) - return certificate.publicKey as RSAPublicKey - } - - /** Generate an RSA public key from a [private] one */ - fun RSAPublicFromPrivate(private: RSAPrivateCrtKey): RSAPublicKey { - val spec = RSAPublicKeySpec(private.modulus, private.publicExponent) - return KeyFactory.getInstance("RSA").generatePublic(spec) as RSAPublicKey - } - - /** Generate a self-signed X.509 certificate from an RSA [private] key */ - fun X509CertificateFromRSAPrivate(private: RSAPrivateCrtKey, name: String): X509Certificate { - val start = Date() - val calendar = Calendar.getInstance() - calendar.time = start - calendar.add(Calendar.YEAR, 1_000) - val end = calendar.time - - val name = X500Name("CN=$name") - val builder = JcaX509v3CertificateBuilder( - name, - BigInteger(20, Random()), - start, - end, - name, - RSAPublicFromPrivate(private) - ) - - - builder.addExtension( - Extension.keyUsage, true, KeyUsage( - KeyUsage.digitalSignature - or KeyUsage.nonRepudiation - or KeyUsage.keyEncipherment - or KeyUsage.dataEncipherment - or KeyUsage.keyAgreement - or KeyUsage.keyCertSign - or KeyUsage.cRLSign - or KeyUsage.encipherOnly - or KeyUsage.decipherOnly - ) - ) - builder.addExtension(Extension.basicConstraints, true, BasicConstraints(true)) - - val certificate = JcaContentSignerBuilder("SHA256WithRSA").build(private) - return JcaX509CertificateConverter() - .setProvider(provider) - .getCertificate(builder.build(certificate)) - - } - - /** Generate an RSA key pair of [keysize] */ - fun genRSAPair(keysize: Int): Pair<RSAPrivateCrtKey, RSAPublicKey> { - val gen = KeyPairGenerator.getInstance("RSA") - gen.initialize(keysize) - val pair = gen.genKeyPair() - return Pair(pair.private as RSAPrivateCrtKey, pair.public as RSAPublicKey) - } - - /** Generate an RSA private key of [keysize] */ - fun genRSAPrivate(keysize: Int): RSAPrivateCrtKey = genRSAPair(keysize).first - - /** Generate an RSA public key of [keysize] */ - fun genRSAPublic(keysize: Int): RSAPublicKey = genRSAPair(keysize).second - - /** - * Hash an RSA public key according to the EBICS standard (EBICS 2.5: 4.4.1.2.3). - */ - fun getEbicsPublicKeyHash(publicKey: RSAPublicKey): ByteArray { - val keyBytes = ByteArrayOutputStream() - keyBytes.writeBytes(publicKey.publicExponent.encodeHex().trimStart('0').toByteArray()) - keyBytes.write(' '.code) - keyBytes.writeBytes(publicKey.modulus.encodeHex().trimStart('0').toByteArray()) - val digest = MessageDigest.getInstance("SHA-256") - return digest.digest(keyBytes.toByteArray()) - } - - fun genEbicsE002Key(encryptionPublicKey: RSAPublicKey): Pair<SecretKey, ByteArray> { - // Gen transaction key - val keygen = KeyGenerator.getInstance("AES", provider) - keygen.init(128) - val transactionKey = keygen.generateKey() - // Encrypt transaction keyA - val cipher = Cipher.getInstance( - "RSA/None/PKCS1Padding", - provider - ) - cipher.init(Cipher.ENCRYPT_MODE, encryptionPublicKey) - val encryptedTransactionKey = cipher.doFinal(transactionKey.encoded) - return Pair(transactionKey, encryptedTransactionKey) - } - - /** - * Encrypt data according to the EBICS E002 encryption process. - */ - fun encryptEbicsE002( - transactionKey: SecretKey, - data: InputStream - ): CipherInputStream { - val cipher = Cipher.getInstance( - "AES/CBC/X9.23Padding", - provider - ) - val ivParameterSpec = IvParameterSpec(ByteArray(16)) - cipher.init(Cipher.ENCRYPT_MODE, transactionKey, ivParameterSpec) - return CipherInputStream(data, cipher) - } - - fun decryptEbicsE002Key( - privateKey: RSAPrivateCrtKey, - encryptedTransactionKey: ByteArray - ): SecretKeySpec { - val cipher = Cipher.getInstance( - "RSA/None/PKCS1Padding", - provider - ) - cipher.init(Cipher.DECRYPT_MODE, privateKey) - val transactionKeyBytes = cipher.doFinal(encryptedTransactionKey) - return SecretKeySpec(transactionKeyBytes, "AES") - } - - fun decryptEbicsE002( - transactionKey: SecretKeySpec, - encryptedData: InputStream - ): CipherInputStream { - val cipher = Cipher.getInstance( - "AES/CBC/X9.23Padding", - provider - ) - val ivParameterSpec = IvParameterSpec(ByteArray(16)) - cipher.init(Cipher.DECRYPT_MODE, transactionKey, ivParameterSpec) - return CipherInputStream(encryptedData, cipher) - } - - /** - * Signing algorithm corresponding to the EBICS A006 signing process. - * - * Note that while [data] can be arbitrary-length data, in EBICS, the order - * data is *always* hashed *before* passing it to the signing algorithm, which again - * uses a hash internally. - */ - fun signEbicsA006(data: ByteArray, privateKey: RSAPrivateCrtKey): ByteArray { - val signature = Signature.getInstance("SHA256withRSA/PSS", provider) - signature.setParameter(PSSParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 32, 1)) - signature.initSign(privateKey) - signature.update(data) - return signature.sign() - } - - fun verifyEbicsA006(sig: ByteArray, data: ByteArray, publicKey: RSAPublicKey): Boolean { - val signature = Signature.getInstance("SHA256withRSA/PSS", provider) - signature.setParameter(PSSParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 32, 1)) - signature.initVerify(publicKey) - signature.update(data) - return signature.verify(sig) - } - - fun digestEbicsOrderA006(orderData: ByteArray): ByteArray { - val digest = MessageDigest.getInstance("SHA-256") - for (b in orderData) { - when (b) { - '\r'.code.toByte(), '\n'.code.toByte(), (26).toByte() -> Unit - else -> digest.update(b) - } - } - return digest.digest() - } - - fun decryptKey(data: EncryptedPrivateKeyInfo, passphrase: String): RSAPrivateCrtKey { - /* make key out of passphrase */ - val pbeKeySpec = PBEKeySpec(passphrase.toCharArray()) - val keyFactory = SecretKeyFactory.getInstance(data.algName) - val secretKey = keyFactory.generateSecret(pbeKeySpec) - /* Make a cipher */ - val cipher = Cipher.getInstance(data.algName) - cipher.init( - Cipher.DECRYPT_MODE, - secretKey, - data.algParameters // has hash count and salt - ) - /* Ready to decrypt */ - val decryptedKeySpec: PKCS8EncodedKeySpec = data.getKeySpec(cipher) - val priv = KeyFactory.getInstance("RSA").generatePrivate(decryptedKeySpec) - return priv as RSAPrivateCrtKey - } - - fun hashStringSHA256(input: String): ByteArray = - MessageDigest.getInstance("SHA-256").digest(input.toByteArray(Charsets.UTF_8)) - - fun hashStringNbo(input: String): ByteArray = - MessageDigest.getInstance("SHA-512").digest(input.toByteArray(Charsets.UTF_8) + byteArrayOf(0)).sliceArray(0..31) - - fun bcrypt(password: String, salt: ByteArray, cost: Int): ByteArray { - val pwBytes = BCrypt.passwordToByteArray(password.toCharArray()) - return BCrypt.generate(pwBytes, salt, cost) - } - - fun hashArgon2id(password: String, salt: ByteArray): ByteArray { - // OSWAP recommended config https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#argon2id - val builder = Argon2Parameters.Builder(Argon2Parameters.ARGON2_id) - .withIterations(1) - .withMemoryAsKB(47104) - .withParallelism(1) - .withSalt(salt) - - val gen = Argon2BytesGenerator() - gen.init(builder.build()) - - val result = ByteArray(32) - gen.generateBytes(password.toCharArray(), result, 0, result.size) - return result - } - - private fun mfaBodyHash(body: ByteArray, salt: Base32Crockford16B): Base32Crockford64B { - val digest = MessageDigest.getInstance("SHA-512") - digest.update(salt.raw) - val hash = digest.digest(body) - return Base32Crockford64B(hash) - } - - fun mfaBodyHashCheck(body: ByteArray, hash: Base32Crockford64B, salt: Base32Crockford16B): Boolean { - val check = mfaBodyHash(body, salt) - return check == hash - } - - fun mfaBodyHashCreate(body: ByteArray): Pair<Base32Crockford64B, Base32Crockford16B> { - val salt = Base32Crockford16B.secureRand() - val hash = mfaBodyHash(body, salt) - return Pair(hash, salt) - } - - fun checkEdssaSignature(data: ByteArray, signature: EddsaSignature, publicKey: EddsaPublicKey): Boolean { - val pubKey = Ed25519PublicKeyParameters(publicKey.raw, 0) - - val verifier = Ed25519Signer() - verifier.init(false, pubKey) - verifier.update(data, 0, data.size) - - return verifier.verifySignature(signature.raw) - } - - fun eddsaSign(data: ByteArray, privateKey: ByteArray): EddsaSignature { - val privateKey = Ed25519PrivateKeyParameters(privateKey, 0) - - val signer = Ed25519Signer() - signer.init(true, privateKey) - signer.update(data, 0, data.size) - - return EddsaSignature(signer.generateSignature()) - } -} -\ No newline at end of file diff --git a/libeufin-common/src/main/kotlin/crypto/PwCrypto.kt b/libeufin-common/src/main/kotlin/crypto/PwCrypto.kt @@ -1,106 +0,0 @@ -/* - * 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.common.crypto - -import kotlinx.serialization.Serializable -import tech.libeufin.common.* - -@JvmInline -value class Password(val pw: String) - -// NIST Password Guidelines 2024 -private const val PASSWORD_MIN_LEN = 8 -private const val PASSWORD_MAX_LEN = 64 - -/** Check if a string is a valid password */ -fun String.checkPw(checkQuality: Boolean): Password { - if (!checkQuality) return Password(this) - val len = this.length - return when { - len < PASSWORD_MIN_LEN -> throw conflict( - "Password is too short, expect at least ${PASSWORD_MIN_LEN} characters got ${len}", - TalerErrorCode.BANK_PASSWORD_TOO_SHORT - ) - len > PASSWORD_MAX_LEN -> throw conflict( - "Password is too long, expect at most ${PASSWORD_MAX_LEN} characters got ${len}", - TalerErrorCode.BANK_PASSWORD_TOO_LONG - ) - else -> Password(this) - } -} - -data class PasswordHashCheck( - val match: Boolean, - val outdated: Boolean -) - -/** Cryptographic operations for secure password storage and verification */ -sealed interface PwCrypto { - @Serializable - data class Bcrypt(val cost: Int = 8): PwCrypto - - /** Hash [pw] using [cfg] hashing method */ - fun hashpw(pw: String): String { - when (this) { - is Bcrypt -> { - val salt = ByteArray(16).secureRand() - val pwh = CryptoUtil.bcrypt(pw, salt, cost) - return "bcrypt\$$cost\$${salt.encodeBase64()}\$${pwh.encodeBase64()}" - } - /* TODO Argon2id - "argon2id" -> { - require(components.size == 3) { "bad password hash format" } - val salt = components[1].decodeBase64() - val hash = components[2] - val pwh = CryptoUtil.hashArgon2id(pw, salt).encodeBase64() - PasswordHashCheck(pwh == hash, false) - } */ - } - } - - /** Check whether [pw] match hashed [storedPwHash] and if it should be rehashed */ - fun checkpw(pw: String, storedPwHash: String): PasswordHashCheck { - val components = storedPwHash.split('$', limit = 5) - return when (val algo = components[0]) { - "sha256" -> { - require(components.size == 2) { "bad password hash format" } - val hash = components[1] - val pwh = CryptoUtil.hashStringSHA256(pw).encodeBase64() - PasswordHashCheck(pwh == hash, true) - } - "sha256-salted" -> { - require(components.size == 3) { "bad password hash format" } - val salt = components[1] - val hash = components[2] - val pwh = CryptoUtil.hashStringSHA256("$salt|$pw").encodeBase64() - PasswordHashCheck(pwh == hash, true) - } - "bcrypt" -> { - require(components.size == 4) { "bad password hash format" } - val cost = components[1].toInt() - val salt = components[2].decodeBase64() - val hash = components[3] - val pwh = CryptoUtil.bcrypt(pw, salt, cost).encodeBase64() - PasswordHashCheck(pwh == hash, !(this is Bcrypt && this.cost == cost)) - } - else -> throw Exception("unsupported hash algo: '$algo'") - } - } -} -\ No newline at end of file diff --git a/libeufin-common/src/main/kotlin/db/DbPool.kt b/libeufin-common/src/main/kotlin/db/DbPool.kt @@ -1,81 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2024-2025 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.common.db - -import com.zaxxer.hikari.HikariConfig -import com.zaxxer.hikari.HikariDataSource -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import org.postgresql.jdbc.PgConnection -import tech.libeufin.common.MIN_VERSION -import java.sql.PreparedStatement - -open class DbPool(cfg: DatabaseConfig, schema: String) : java.io.Closeable { - val pgSource = pgDataSource(cfg.dbConnStr) - private val pool: HikariDataSource - - init { - val config = HikariConfig() - config.dataSource = pgSource - config.schema = schema.replace("-", "_") - config.transactionIsolation = "TRANSACTION_SERIALIZABLE" - pool = HikariDataSource(config) - pool.connection.use { con -> - val meta = con.metaData - val majorVersion = meta.databaseMajorVersion - val minorVersion = meta.databaseMinorVersion - require(majorVersion >= MIN_VERSION) { - "postgres version must be at least $MIN_VERSION.0 got $majorVersion.$minorVersion" - } - checkMigrations(con.unwrap(PgConnection::class.java), cfg, schema) - } - } - - /** Executes a query with automatic retry on serialization errors */ - suspend fun <R> serializable(query: String, lambda: TalerStatement.() -> R): R = conn { conn -> - // We could explicitly tell Postgres when a request is read-only, - // but the performance improvement isn't obvious, it doesn't prevent - // stored procedures from modifying the database and it adds a - // round-trip during configuration - - conn.withStatement(query) { - retrySerializationError { lambda() } - } - } - - /** Executes a transaction with automatic retry on serialization errors */ - suspend fun <R> serializableTransaction(transaction: (PgConnection) -> R): R = conn { conn -> - retrySerializationError { - conn.transaction(transaction) - } - } - - /** Run db logic using a connection from the pool */ - suspend fun <R> conn(lambda: suspend (PgConnection) -> R): R { - // Use a coroutine dispatcher that we can block as JDBC API is blocking - return withContext(Dispatchers.IO) { - pool.connection.use { lambda(it.unwrap(PgConnection::class.java)) } - } - } - - override fun close() { - pool.close() - } -} -\ No newline at end of file diff --git a/libeufin-common/src/main/kotlin/db/config.kt b/libeufin-common/src/main/kotlin/db/config.kt @@ -1,86 +0,0 @@ -/* - * 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.common.db - -import io.ktor.http.* -import org.postgresql.ds.PGSimpleDataSource -import org.postgresql.jdbc.PgConnection -import java.net.URI -import java.nio.file.Path - -fun currentUser(): String = System.getProperty("user.name") - -/** - * This function converts postgresql:// URIs to JDBC URIs. - * - * URIs that are already jdbc: URIs are passed through. - * - * This avoids the user having to create complex JDBC URIs for postgres connections. - * They are especially complex when using unix domain sockets, as they're not really - * supported natively by JDBC. - */ -fun jdbcFromPg(pgConn: String): String { - // Pass through jdbc URIs. - if (pgConn.startsWith("jdbc:")) { - return pgConn - } - require(pgConn.startsWith("postgresql://") || pgConn.startsWith("postgres://")) { - "Not a Postgres connection string: $pgConn" - } - val uri = URI(pgConn) - val params = parseQueryString(uri.query ?: "", decode = false) - - val host = uri.host ?: params["host"] ?: System.getenv("PGHOST") - if (host == null || host.startsWith('/')) { - val port = (if (uri.port == -1) null else uri.port.toString()) ?: params["port"] ?: System.getenv("PGPORT") ?: "5432" - val user = params["user"] ?: currentUser() - val unixPath = (host ?:"/var/run/postgresql") + "/.s.PGSQL.$port" - return "jdbc:postgresql://localhost${uri.path}?user=$user&socketFactory=org.newsclub.net.unix." + - "AFUNIXSocketFactory\$FactoryArg&socketFactoryArg=$unixPath" - } - if (pgConn.startsWith("postgres://")) { - // The JDBC driver doesn't like postgres://, only postgresql://. - // For consistency with other components, we normalize the postgres:// URI - // into one that the JDBC driver likes. - return "jdbc:postgresql://" + pgConn.removePrefix("postgres://") - } - logger.info("connecting to database via JDBC string '$pgConn'") - return "jdbc:$pgConn" -} - -data class DatabaseConfig( - val dbConnStr: String, - val sqlDir: Path -) - -fun pgDataSource(dbConfig: String): PGSimpleDataSource { - val jdbcConnStr = jdbcFromPg(dbConfig) - logger.debug("connecting to database via JDBC string '$jdbcConnStr'") - val pgSource = PGSimpleDataSource() - pgSource.setUrl(jdbcConnStr) - pgSource.prepareThreshold = 1 - return pgSource -} - -fun PGSimpleDataSource.pgConnection(schema: String? = null): PgConnection { - val conn = connection.unwrap(PgConnection::class.java) - if (schema != null) conn.execSQLUpdate("SET search_path TO $schema") - return conn -} diff --git a/libeufin-common/src/main/kotlin/db/helpers.kt b/libeufin-common/src/main/kotlin/db/helpers.kt @@ -1,165 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2024-2025 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.common.db - -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch -import kotlinx.coroutines.withTimeoutOrNull -import tech.libeufin.common.HistoryParams -import tech.libeufin.common.PageParams -import java.sql.PreparedStatement -import java.sql.ResultSet -import kotlin.math.abs -import kotlin.math.min - -/** - * Hard upper bound on the number of records returned by a single - * query, regardless of the limit requested by the client. - */ -private const val MAX_RECORDS: Long = 50_000 - -/** Apply paging logic to a sql query */ -suspend fun <T> DbPool.page( - params: PageParams, - idName: String, - query: String, - args: TalerStatement.() -> Unit = {}, - map: (ResultSet) -> T -): List<T> { - val backward = params.limit < 0 - val pageQuery = """ - $query - $idName ${if (backward) '<' else '>'} ? - ORDER BY $idName ${if (backward) "DESC" else "ASC"} - LIMIT ? - """ - return serializable(pageQuery) { - args() - bind(params.offset) - // Widen before abs(): abs(Int.MIN_VALUE) is Int.MIN_VALUE, which would - // reach Postgres as a negative LIMIT. - bind(min(MAX_RECORDS, abs(params.limit.toLong()))) - all { map(it) } - } -} - -/** -* The following function returns the list of transactions, according -* to the history parameters and perform long polling when necessary -*/ -suspend fun <T> DbPool.poolHistory( - params: HistoryParams, - bankAccountId: Long, - listen: suspend (Long, suspend (Flow<Long>) -> List<T>) -> List<T>, - query: String, - accountColumn: String = "bank_account_id", - map: (ResultSet) -> T -): List<T> { - - suspend fun load(): List<T> = page( - params.page, - "bank_transaction_id", - "$query $accountColumn=? AND", - { - bind(bankAccountId) - }, - map - ) - - // When going backward there is always at least one transaction or none - return if (params.page.limit >= 0 && params.polling.timeout_ms > 0) { - listen(bankAccountId) { flow -> - coroutineScope { - // Start buffering notification before loading transactions to not miss any - val polling = launch { - withTimeoutOrNull(params.polling.timeout_ms) { - flow.first { it > params.page.offset } // Always forward so > - } - } - // Initial loading - val init = load() - // Long polling if we found no transactions - if (init.isEmpty()) { - if (polling.join() != null) { - load() - } else { - init - } - } else { - polling.cancel() - init - } - } - } - } else { - load() - } -} - -/** -* The following function returns the list of transactions, according -* to the history parameters and perform long polling when necessary -*/ -suspend fun <T> DbPool.poolHistoryGlobal( - params: HistoryParams, - listen: suspend (suspend (Flow<Long>) -> List<T>) -> List<T>, - query: String, - idColumnValue: String, - map: (ResultSet) -> T -): List<T> { - - suspend fun load(): List<T> = page( - params.page, - idColumnValue, - query, - map=map - ) - - // When going backward there is always at least one transaction or none - return if (params.page.limit >= 0 && params.polling.timeout_ms > 0) { - listen { flow -> - coroutineScope { - // Start buffering notification before loading transactions to not miss any - val polling = launch { - withTimeoutOrNull(params.polling.timeout_ms) { - flow.first { it > params.page.offset } // Always forward so > - } - } - // Initial loading - val init = load() - // Long polling if we found no transactions - if (init.isEmpty()) { - if (polling.join() != null) { - load() - } else { - init - } - } else { - polling.cancel() - init - } - } - } - } else { - load() - } -} -\ No newline at end of file diff --git a/libeufin-common/src/main/kotlin/db/notifications.kt b/libeufin-common/src/main/kotlin/db/notifications.kt @@ -1,101 +0,0 @@ -/* - * 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.common.db - -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow -import org.postgresql.ds.PGSimpleDataSource -import org.slf4j.Logger -import tech.libeufin.common.ExpoBackoffDecorr -import tech.libeufin.common.fmtLog -import java.io.Closeable -import java.util.concurrent.ConcurrentHashMap - -// SharedFlow that are manually counted for manual garbage collection -class CountedSharedFlow<T> { - val flow: MutableSharedFlow<T> = MutableSharedFlow() - var count: Int = 0 -} - -fun watchNotifications( - pgSource: PGSimpleDataSource, - schema: String, - logger: Logger, - listeners: Map<String, (suspend (String) -> Unit)> -): Closeable { - val backoff = ExpoBackoffDecorr() - // JDBC notification reads block, so keep them off the caller's dispatcher. - val job = CoroutineScope(Dispatchers.IO).launch { - while (isActive) { - try { - pgSource.pgConnection(schema).use { conn -> - - // Listen to all notifications channels - for (channel in listeners.keys) { - conn.execSQLUpdate("LISTEN $channel") - } - - backoff.reset() - - while (isActive) { - // Bound the blocking read so close() can cancel an idle watcher. - conn.getNotifications(1000) - .forEach { - // Dispatch - try { - listeners[it.name]!!(it.parameter) - } catch (e: Exception) { - ensureActive() - throw Exception("channel ${it.name} with input '${it.parameter}'", e) - } - } - } - } - } catch (e: Exception) { - ensureActive() - e.fmtLog(logger) - delay(backoff.next()) - } - } - } - return Closeable { runBlocking { job.cancelAndJoin() } } -} - -/** Listen to flow from [map] for [key] using [lambda]*/ -suspend fun <R, K, V> listen(map: ConcurrentHashMap<K, CountedSharedFlow<V>>, key: K, lambda: suspend (Flow<V>) -> R): R { - // Register listener, create a new flow if missing - val flow = map.compute(key) { _, v -> - val tmp = v ?: CountedSharedFlow() - tmp.count++ - tmp - }!!.flow - - try { - return lambda(flow) - } finally { - // Unregister listener, removing unused flow - map.compute(key) { _, v -> - v!! - v.count-- - if (v.count > 0) v else null - } - } -} diff --git a/libeufin-common/src/main/kotlin/db/schema.kt b/libeufin-common/src/main/kotlin/db/schema.kt @@ -1,116 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2024-2025 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.common.db - -import org.postgresql.ds.* -import org.postgresql.jdbc.PgConnection -import java.sql.Connection -import kotlin.io.path.Path -import kotlin.io.path.exists -import kotlin.io.path.readText - -/** - * Only runs versioning.sql if the _v schema is not found. - * - * @param conn database connection - * @param cfg database configuration - */ -private fun maybeApplyV(conn: PgConnection, cfg: DatabaseConfig) { - conn.transaction { - val checkVSchema = conn.talerStatement( - "SELECT schema_name FROM information_schema.schemata WHERE schema_name = '_v'" - ) - if (!checkVSchema.executeQueryCheck()) { - logger.debug("_v schema not found, applying versioning.sql") - val sqlVersioning = Path("${cfg.sqlDir}/versioning.sql").readText() - conn.execSQLUpdate(sqlVersioning) - } - } -} - -private fun migrationsPath(sqlFilePrefix: String): Sequence<String> = sequence { - for (n in 1..9999) { - val padded = n.toString().padStart(4, '0') - yield("$sqlFilePrefix-$padded") - } -} - -// sqlFilePrefix is, for example, "libeufin-bank" or "libeufin-nexus" (no trailing dash). -private fun initializeDatabaseTables(conn: PgConnection, cfg: DatabaseConfig, sqlFilePrefix: String) { - logger.info("doing DB initialization, sqldir ${cfg.sqlDir}") - maybeApplyV(conn, cfg) - conn.transaction { - val checkStmt = conn.talerStatement("SELECT EXISTS(SELECT FROM _v.patches where patch_name = ?)") - - for (patchName in migrationsPath(sqlFilePrefix)) { - checkStmt.bind(patchName) - val applied = checkStmt.one { it.getBoolean(1) } - if (applied) { - logger.debug("patch $patchName already applied") - continue - } - - val path = Path("${cfg.sqlDir}/$patchName.sql") - if (!path.exists()) { - logger.debug("path {} doesn't exist anymore, stopping", path) - break - } - logger.info("applying patch $path") - conn.execSQLUpdate(path.readText()) - } - val sqlProcedures = Path("${cfg.sqlDir}/$sqlFilePrefix-procedures.sql") - if (!sqlProcedures.exists()) { - logger.warn("no procedures.sql for the SQL collection: $sqlFilePrefix") - return@transaction - } - logger.info("run procedure.sql") - conn.execSQLUpdate(sqlProcedures.readText()) - } -} - -internal fun checkMigrations(conn: PgConnection, cfg: DatabaseConfig, sqlFilePrefix: String) { - val checkStmt = conn.talerStatement("SELECT EXISTS(SELECT FROM _v.patches where patch_name = ?)") - - for (patchName in migrationsPath(sqlFilePrefix)) { - checkStmt.bind(patchName) - val path = Path("${cfg.sqlDir}/$patchName.sql") - if (!path.exists()) break - val applied = checkStmt.one { it.getBoolean(1) } - if (!applied) { - throw Exception("patch $patchName not applied, run '$sqlFilePrefix dbinit'") - } - } -} - -// sqlFilePrefix is, for example, "libeufin-bank" or "libeufin-nexus" (no trailing dash). -private fun resetDatabaseTables(conn: PgConnection, cfg: DatabaseConfig, sqlFilePrefix: String) { - logger.info("reset DB, sqldir ${cfg.sqlDir}") - val sqlDrop = Path("${cfg.sqlDir}/$sqlFilePrefix-drop.sql").readText() - conn.execSQLUpdate(sqlDrop) -} - -fun PGSimpleDataSource.dbInit(cfg: DatabaseConfig, sqlFilePrefix: String, reset: Boolean) { - pgConnection().use { conn -> - if (reset) { - resetDatabaseTables(conn, cfg, sqlFilePrefix) - } - initializeDatabaseTables(conn, cfg, sqlFilePrefix) - } -} -\ No newline at end of file diff --git a/libeufin-common/src/main/kotlin/db/statement.kt b/libeufin-common/src/main/kotlin/db/statement.kt @@ -1,236 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2025 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.common.db - -import org.slf4j.Logger -import org.slf4j.LoggerFactory -import org.postgresql.util.PSQLState -import tech.libeufin.common.* -import java.sql.* -import java.time.* -import java.util.* - -internal val logger: Logger = LoggerFactory.getLogger("libeufin-db") - -class TalerStatement(internal val stmt: PreparedStatement): java.io.Closeable { - override fun close() { - // Close inner statement - stmt.close() - } - - private fun consume() { - // Log warnings - var current = stmt.getWarnings() - while (current != null) { - logger.warn(current.message) - current = current.getNextWarning() - } - - // Reset params - stmt.clearParameters() - idx=1 - } - - /* ----- Bindings helpers ----- */ - - private var idx = 1; - - fun bind(string: String?) { - stmt.setString(idx, string) - idx+=1; - } - - fun bind(bool: Boolean) { - stmt.setBoolean(idx, bool) - idx+=1; - } - - fun bind(nb: Long?) { - if (nb != null) { - stmt.setLong(idx, nb) - } else { - stmt.setNull(idx, Types.INTEGER) - } - idx+=1; - } - - fun bind(nb: Int) { - stmt.setInt(idx, nb) - idx+=1; - } - - fun bind(amount: TalerAmount?) { - bind(amount?.number()) - } - - fun bind(nb: DecimalNumber?) { - if (nb != null) { - stmt.setLong(idx, nb.value) - stmt.setInt(idx+1, nb.frac) - idx+=2 - } - } - - fun bind(timestamp: Instant) { - stmt.setLong(idx, timestamp.micros()) - idx+=1 - } - - fun bind(bytes: Base32Crockford64B?) { - stmt.setBytes(idx, bytes?.raw) - idx+=1 - } - - fun bind(bytes: Base32Crockford32B?) { - stmt.setBytes(idx, bytes?.raw) - idx+=1 - } - - fun bind(bytes: Base32Crockford16B?) { - stmt.setBytes(idx, bytes?.raw) - idx+=1 - } - - - fun bind(bytes: ByteArray?) { - stmt.setBytes(idx, bytes) - idx+=1 - } - - fun <T : kotlin.Enum<T>> bind(enum: T?) { - bind(enum?.name) - } - - fun bind(date: LocalDateTime) { - stmt.setObject(idx, date) - idx+=1 - } - - fun bind(uuid: UUID?) { - stmt.setObject(idx, uuid) - idx+=1 - } - - fun <T : Enum<T>> bind(array: Array<T>) { - val sqlArray = stmt.connection.createArrayOf("text", array) - stmt.setArray(idx, sqlArray) - idx+=1 - } - - fun bind(array: Array<String>) { - val sqlArray = stmt.connection.createArrayOf("text", array) - stmt.setArray(idx, sqlArray) - idx+=1 - } - - fun bind(array: Array<UUID>) { - val sqlArray = stmt.connection.createArrayOf("uuid", array) - stmt.setArray(idx, sqlArray) - idx+=1 - } - - /* ----- Transaction helpers ----- */ - - fun executeQuery(): ResultSet { - return try { - stmt.executeQuery() - } finally { - consume() - } - } - - fun executeUpdate(): Int { - return try { - stmt.executeUpdate() - } finally { - consume() - } - } - - /** Read one row or null if none */ - fun <T> oneOrNull(lambda: (ResultSet) -> T): T? { - return executeQuery().use { - if (it.next()) lambda(it) else null - } - } - - /** Read one row or throw if none */ - fun <T> one(lambda: (ResultSet) -> T): T = - requireNotNull(oneOrNull(lambda)) { "Missing result to database query" } - - /** Read one row or throw [err] in case or unique violation error */ - fun <T> oneUniqueViolation(err: T, lambda: (ResultSet) -> T): T { - return try { - one(lambda) - } catch (e: SQLException) { - if (e.sqlState == PSQLState.UNIQUE_VIOLATION.state) return err - throw e // rethrowing, not to hide other types of errors. - } - } - - /** Read all rows */ - fun <T> all(lambda: (ResultSet) -> T): List<T> { - return executeQuery().use { - val ret = mutableListOf<T>() - while (it.next()) { - ret.add(lambda(it)) - } - ret - } - } - - /** Execute a query checking it return a least one row */ - fun executeQueryCheck(): Boolean { - return executeQuery().use { - it.next() - } - } - - /** Execute an update checking it update at least one row */ - fun executeUpdateCheck(): Boolean { - executeUpdate() - return stmt.updateCount > 0 - } - - /** Execute an update checking if fail because of unique violation error */ - fun executeUpdateViolation(): Boolean { - return try { - executeUpdateCheck() - } catch (e: SQLException) { - logger.debug(e.message) - if (e.sqlState == PSQLState.UNIQUE_VIOLATION.state) return false - throw e // rethrowing, not to hide other types of errors. - } - } - - /** Execute an update checking if fail because of unique violation error and resetting state */ - fun executeProcedureViolation(): Boolean { - val savepoint = stmt.connection.setSavepoint() - return try { - executeUpdate() - stmt.connection.releaseSavepoint(savepoint) - true - } catch (e: SQLException) { - stmt.connection.rollback(savepoint) - if (e.sqlState == PSQLState.UNIQUE_VIOLATION.state) return false - throw e // rethrowing, not to hide other types of errors. - } - } -} -\ No newline at end of file diff --git a/libeufin-common/src/main/kotlin/db/transaction.kt b/libeufin-common/src/main/kotlin/db/transaction.kt @@ -1,84 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2025 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.common.db - -import org.postgresql.jdbc.PgConnection -import org.postgresql.util.PSQLState -import tech.libeufin.common.SERIALIZATION_RETRY -import java.sql.PreparedStatement -import java.sql.ResultSet -import java.sql.SQLException - -val SERIALIZATION_ERROR = setOf( - "40001", // serialization_failure - "40P01", // deadlock_detected - "55P03", // lock_not_available -) - -/** Executes db logic with automatic retry on serialization errors */ -suspend fun <R> retrySerializationError(lambda: suspend () -> R): R { - repeat(SERIALIZATION_RETRY) { - try { - return lambda() - } catch (e: SQLException) { - if (!SERIALIZATION_ERROR.contains(e.sqlState)) throw e - } - } - return lambda() -} - -fun PgConnection.talerStatement(query: String): TalerStatement = TalerStatement(prepareStatement(query)) - -/** Run a postgres query using a prepared statement */ -inline fun <R> PgConnection.withStatement(query: String, lambda: TalerStatement.() -> R): R = - talerStatement(query).use { it.lambda() } - -/** Run a postgres [transaction] */ -fun <R> PgConnection.transaction(transaction: (PgConnection) -> R): R { - try { - autoCommit = false - val result = transaction(this) - commit() - autoCommit = true - return result - } catch (e: Exception) { - rollback() - autoCommit = true - throw e - } -} - -/** - * Execute an update of [table] with a dynamic query generated at runtime. - * Every [fields] in each row matching [filter] are updated using values from [bind]. - **/ -fun PgConnection.dynamicUpdate( - table: String, - fields: Sequence<String>, - filter: String, - bind: TalerStatement.() -> Unit, -) { - val sql = fields.joinToString() - if (sql.isEmpty()) return - withStatement("UPDATE $table SET $sql $filter") { - bind() - executeUpdate() - } -} -\ No newline at end of file diff --git a/libeufin-common/src/main/kotlin/db/types.kt b/libeufin-common/src/main/kotlin/db/types.kt @@ -1,134 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2024, 2025, 2026 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.common.db - -import tech.libeufin.common.* -import java.sql.* -import java.time.* -import java.util.* - -fun optAmount(amount: TalerAmount?): String { - if (amount != null) { - return "(?,?)::taler_amount" - } else { - return "NULL" - } -} - -fun optDecimal(nb: DecimalNumber?): String { - if (nb != null) { - return "(?,?)::taler_amount" - } else { - return "NULL" - } -} - -inline fun <reified T : Enum<T>> ResultSet.getEnum(name: String): T - = java.lang.Enum.valueOf(T::class.java, getString(name)) -inline fun <reified T : Enum<T>> ResultSet.getEnum(idx: Int): T - = java.lang.Enum.valueOf(T::class.java, getString(idx)) - -inline fun <reified T : Enum<T>> ResultSet.getOptEnum(name: String): T? - = getString(name)?.run { java.lang.Enum.valueOf(T::class.java, this) } -inline fun <reified T : Enum<T>> ResultSet.getOptEnum(idx: Int): T? - = getString(idx)?.run { java.lang.Enum.valueOf(T::class.java, this) } - -inline fun <reified T : Enum<T>> ResultSet.getEnumSet(name: String): Set<T> { - val sqlArray: java.sql.Array = this.getArray(name) - val javaArray: Array<String> = (sqlArray.array as Array<String>) - val set: Set<T> = javaArray.map { java.lang.Enum.valueOf(T::class.java, it) }.toSet() - sqlArray.free() - return set -} - -inline fun <reified T> ResultSet.getOptObject(column: String): T? { - val value = this.getObject(column, T::class.java) - return if (this.wasNull()) null else value as T -} - -fun ResultSet.getOptKey(column: String): EddsaPublicKey? { - val bytes = this.getBytes(column) - return if (this.wasNull()) null else EddsaPublicKey(bytes) -} - -fun ResultSet.getOptSig(column: String): EddsaSignature? { - val bytes = this.getBytes(column) - return if (this.wasNull()) null else EddsaSignature(bytes) -} - -fun ResultSet.getOptLong(name: String): Long? { - val nb = getLong(name) - if (wasNull()) return null - return nb -} - -fun ResultSet.getAmount(name: String, currency: String): TalerAmount { - return TalerAmount( - getLong("${name}_val"), - getInt("${name}_frac"), - currency - ) -} - -fun ResultSet.getOptAmount(name: String, currency: String): TalerAmount? { - val amount = getAmount(name, currency) - if (wasNull()) return null - return amount -} - -fun ResultSet.getDecimal(name: String): DecimalNumber { - return DecimalNumber( - getLong("${name}_val"), - getInt("${name}_frac") - ) -} - -fun ResultSet.getOptDecimal(name: String): DecimalNumber? { - val amount = getDecimal(name) - if (wasNull()) return null - return amount -} - -fun ResultSet.getTalerTimestamp(name: String): TalerTimestamp{ - return TalerTimestamp(getLong(name).asInstant()) -} - -fun ResultSet.getBankPayto(payto: String, name: String?, ctx: BankPaytoCtx): String { - return Payto.parse(getString(payto)).bank( - name?.let { getString(it) } - , ctx) -} -fun ResultSet.getOptBankPayto(payto: String, name: String?, ctx: BankPaytoCtx): String? { - val payto = getString(payto) - if (payto == null) return null - return Payto.parse(payto).bank( - name?.let { getString(it) } - , ctx) -} - -fun ResultSet.getOptIbanPayto(payto: String): IbanPayto? { - val raw = getString(payto) - if (raw == null) return null - return Payto.parse(raw).expectIban() -} - -fun ResultSet.getIbanPayto(payto: String): IbanPayto { - return Payto.parse(getString(payto)).expectIban() -} -\ No newline at end of file diff --git a/libeufin-common/src/main/kotlin/helpers.kt b/libeufin-common/src/main/kotlin/helpers.kt @@ -1,199 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2024-2025 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.common - -import io.ktor.server.application.* -import org.slf4j.Logger -import java.io.ByteArrayOutputStream -import java.io.FilterInputStream -import java.io.InputStream -import java.math.BigInteger -import java.security.SecureRandom -import java.time.Instant -import java.time.LocalDate -import java.time.LocalDateTime -import java.time.ZoneOffset -import java.time.format.DateTimeFormatter -import java.util.* -import java.util.zip.DeflaterInputStream -import java.util.zip.InflaterInputStream -import java.util.zip.ZipInputStream -import kotlin.random.Random - -/* ----- String ----- */ - -/** Decode a base64 encoded string */ -fun String.decodeBase64(): ByteArray = Base64.getDecoder().decode(this) -/** Encode a string as base64 */ -fun String.encodeBase64(): String = toByteArray().encodeBase64() -/** Decode a hexadecimal uppercase encoded string */ -fun String.decodeUpHex(): ByteArray = HexFormat.of().withUpperCase().parseHex(this) - -fun String.splitOnce(pat: String): Pair<String, String>? { - val split = splitToSequence(pat, limit=2).iterator() - val first = split.next() - if (!split.hasNext()) return null - return Pair(first, split.next()) -} - -/** Format a string with a space every two characters */ -fun String.fmtChunkByTwo() = buildString { - this@fmtChunkByTwo.forEachIndexed { pos, c -> - if (pos != 0 && pos % 2 == 0) append(' ') - append(c) - } -} - -/* ----- Date & Time ----- */ - -/** Converting YYYY-MM-DD to Instant */ -fun dateToInstant(date: String): Instant = - LocalDate.parse(date, DateTimeFormatter.ISO_DATE).atStartOfDay().toInstant(ZoneOffset.UTC) - -/** Converting YYYY-MM-DDTHH:MM:SS to Instant */ -fun dateTimeToInstant(date: String): Instant = - LocalDateTime.parse(date, DateTimeFormatter.ISO_DATE_TIME).toInstant(ZoneOffset.UTC) - -private val DATE_TIME_PATH = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HHmmss") - -/** Converting Instant to YYYY-MM-DDTHHMMSS */ -fun Instant.toDateTimeFilePath(): String = - this.atOffset(ZoneOffset.UTC).format(DATE_TIME_PATH) - -/* ----- BigInteger -----*/ - -fun BigInteger.encodeHex(): String = this.toByteArray().encodeHex() -fun BigInteger.encodeBase64(): String = this.toByteArray().encodeBase64() - -/* ----- Random ----- */ - -/** Thread local cryptographically strong random number generator */ -val SECURE_RNG = ThreadLocal.withInitial { SecureRandom() } - -/* ----- ByteArray ----- */ - -fun ByteArray.rand(rng: Random = Random): ByteArray { - rng.nextBytes(this) - return this -} -fun ByteArray.secureRand(): ByteArray { - SECURE_RNG.get().nextBytes(this) - return this -} -fun ByteArray.encodeHex(): String = HexFormat.of().formatHex(this) -fun ByteArray.encodeUpHex(): String = HexFormat.of().withUpperCase().formatHex(this) -fun ByteArray.encodeBase64(): String = Base64.getEncoder().encodeToString(this) -fun ByteArray.asUtf8(): String = this.toString(Charsets.UTF_8) -fun ByteArrayOutputStream.asUtf8(): String = this.toString(Charsets.UTF_8) - -/* ----- InputStream ----- */ - -/** Unzip an input stream and run [lambda] over each entry */ -inline fun InputStream.unzipEach(lambda: (String, InputStream) -> Unit) { - ZipInputStream(this).use { zip -> - while (true) { - val entry = zip.getNextEntry() ?: break - val entryStream = object: FilterInputStream(zip) { - override fun close() { - zip.closeEntry() - } - } - lambda(entry.name, entryStream) - } - } -} - -/** Decode a base64 encoded input stream */ -fun InputStream.decodeBase64(): InputStream - = Base64.getDecoder().wrap(this) - -/** Encode an input stream as base64 */ -fun InputStream.encodeBase64(): String { - val w = ByteArrayOutputStream() - val encoded = Base64.getEncoder().wrap(w) - transferTo(encoded) - encoded.close() - return w.asUtf8() -} - -/** Deflate an input stream */ -fun InputStream.deflate(): DeflaterInputStream - = DeflaterInputStream(this) - -/** Inflate an input stream */ -fun InputStream.inflate(): InflaterInputStream - = InflaterInputStream(this) - -/** Read an input stream as UTF8 text */ -fun InputStream.readText(): String - = this.reader().readText() - -/* ----- Throwable ----- */ - -fun Throwable.fmt(): String = buildString { - append(message ?: this@fmt::class.simpleName) - var cause = cause - while (cause != null) { - append(": ") - append(cause.message ?: cause::class.simpleName) - cause = cause.cause - } -} - -fun Throwable.fmtLog(logger: Logger) { - logger.error(this.fmt()) - logger.trace("", this) -} - -/* ----- Logger ----- */ - -inline fun Logger.debug(lambda: () -> String) { - if (isDebugEnabled) debug(lambda()) -} - -inline fun Logger.trace(lambda: () -> String) { - if (isTraceEnabled) trace(lambda()) -} - -/* ----- KTOR ----- */ - -fun ApplicationCall.uuidPath(name: String): UUID { - val value = parameters[name]!! - try { - return UUID.fromString(value) - } catch (e: Exception) { - throw badRequest("UUID uri component malformed: ${e.message}", TalerErrorCode.GENERIC_PARAMETER_MALFORMED) // TODO better error ? - } -} - -fun ApplicationCall.longPath(name: String): Long { - val value = parameters[name]!! - try { - return value.toLong() - } catch (e: Exception) { - throw badRequest("Long uri component malformed: ${e.message}", TalerErrorCode.GENERIC_PARAMETER_MALFORMED) // TODO better error ? - } -} - -/* ----- Payto ----- */ - -fun ibanPayto(iban: String, name: String? = null): IbanPayto { - return Payto.parse(IbanPayto.build(iban, null, name)).expectIban() -} -\ No newline at end of file diff --git a/libeufin-common/src/main/kotlin/iban.kt b/libeufin-common/src/main/kotlin/iban.kt @@ -1,88 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2024-2025 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.common - -@JvmInline -value class IBAN private constructor(val value: String) { - override fun toString(): String = value - - companion object { - private val SEPARATOR = Regex("[\\ \\-]") - - fun checksum(iban: String): Int = - (iban.substring(4 until iban.length).asSequence() + iban.substring(0 until 4).asSequence()) - .fold(0) { acc, char -> - if (char.isDigit()) { - (acc * 10 + char.code - '0'.code) % 97 - } else { - (acc * 100 + char.code - 'A'.code + 10) % 97 - } - } - - - fun parse(raw: String): IBAN { - val iban: String = raw.uppercase().replace(SEPARATOR, "") - if (iban.length < 5) { - throw CommonError.Payto("malformed IBAN, string is too small only ${iban.length} char") - } - val countryCode = iban.substring(0 until 2) - for (c in countryCode) { - if (!c.isLetter()) throw CommonError.Payto("malformed IBAN, malformed country code") - } - for (c in iban.substring(2 until 4)) { - if (!c.isDigit()) throw CommonError.Payto("malformed IBAN, malformed check digit") - } - val country = try { - Country.valueOf(countryCode) - } catch (e: IllegalArgumentException) { - throw CommonError.Payto("malformed IBAN, unknown country $countryCode") - } - if (country == Country.DE && iban.length != 22) { - // This is allowed for retrocompatibility with libeufin-bank malformed DE IBAN - } else if (!country.bbanRegex.matches(iban.substring(4))) { - throw CommonError.Payto("malformed IBAN, invalid char") - } - val checksum = checksum(iban) - if (checksum != 1) throw CommonError.Payto("malformed IBAN, modulo is $checksum expected 1") - return IBAN(iban) - } - - fun rand(country: Country): IBAN { - val bban = buildString { - for ((repetition, c) in country.rules) { - val alphabet = when (c) { - IbanC.n -> "0123456789" - IbanC.a -> "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - IbanC.c -> "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" - } - repeat(repetition) { - append(alphabet.random()) - } - } - } - val checkDigits = 98 - checksum("${country.name}00$bban"); - return if (checkDigits < 10) { - IBAN("${country.name}0$checkDigits$bban") - } else { - IBAN("${country.name}$checkDigits$bban") - } - } - } -} -\ No newline at end of file diff --git a/libeufin-common/src/main/kotlin/log.kt b/libeufin-common/src/main/kotlin/log.kt @@ -1,114 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2025 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.common - -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.ConcurrentHashMap; -import org.slf4j.event.Level; -import org.slf4j.Logger; -import org.slf4j.Marker; -import org.slf4j.ILoggerFactory; -import org.slf4j.IMarkerFactory; -import org.slf4j.helpers.LegacyAbstractLogger; -import org.slf4j.helpers.MessageFormatter; -import org.slf4j.helpers.BasicMarkerFactory; -import org.slf4j.helpers.BasicMDCAdapter; -import org.slf4j.spi.MDCAdapter; -import org.slf4j.spi.SLF4JServiceProvider; -import java.time.LocalDateTime -import java.time.format.DateTimeFormatter -import java.io.PrintStream -import java.util.concurrent.CancellationException - - -class TalerLogger(private val loggerName: String): LegacyAbstractLogger() { - private fun isLevelEnabled(level: Level): Boolean = level.toInt() >= TalerServiceProvider.currentLevel.toInt() - override fun isTraceEnabled(): Boolean = isLevelEnabled(Level.TRACE) - override fun isDebugEnabled(): Boolean = isLevelEnabled(Level.DEBUG) - override fun isInfoEnabled(): Boolean = isLevelEnabled(Level.INFO) - override fun isWarnEnabled(): Boolean = isLevelEnabled(Level.WARN) - override fun isErrorEnabled(): Boolean = isLevelEnabled(Level.ERROR) - - override fun getFullyQualifiedCallerName(): String = loggerName - - override fun handleNormalizedLoggingCall(level: Level, marker: Marker?, messagePattern: String?, arguments: Array<Any>?, throwable: Throwable?) { - val name = fullyQualifiedCallerName; - if ( - !isLevelEnabled(level) || - (name.startsWith("io.ktor") && level.toInt() < Level.WARN.toInt()) || - // Ktor reports a disconnected client's cancelled request coroutine - // as an unhandled server error. Cancellation is normal control flow, - // in particular when a wire-gateway long poll is stopped. - (name.startsWith("io.ktor") && throwable is CancellationException) || - name.startsWith("com.zaxxer.hikari") - ) return - val callId = org.slf4j.MDC.get("call-id") - val logEntry = buildString { - if (timestampFmt != null) { - append(LocalDateTime.now().format(timestampFmt)) - append(' ') - } - if (callId != null) { - append(callId) - append(' ') - } - append(level.name.padEnd(5)) - append(' ') - append(name) - append(" - ") - append(MessageFormatter.basicArrayFormat(messagePattern, arguments)) - - throwable?.let { t -> - append("${t.javaClass.simpleName}: ${t.message}") - t.stackTrace.take(10).forEach { stackElement -> - append("\n\tat $stackElement") - } - } - } - - System.err.println(logEntry) - } - - companion object { - // We skip logging timestamp if systemd is used - private val skipTimestamp = System.getenv("JOURNAL_STREAM") != null - // A null timestamp formatter mean we should skip it - private val timestampFmt = if (skipTimestamp) null else DateTimeFormatter.ofPattern("dd-MMM-yyyy'T'HH:mm:ss.SSS") - } -} - -class TalerServiceProvider: SLF4JServiceProvider { - private val markerFactory = BasicMarkerFactory() - private val mdcAdapter = BasicMDCAdapter() - - override fun getLoggerFactory() = TalerServiceProvider - override fun getMarkerFactory() = markerFactory - override fun getMDCAdapter() = mdcAdapter - override fun getRequestedApiVersion() = "2.0.99" - override fun initialize() {} - - companion object: ILoggerFactory { - var currentLevel = Level.TRACE - private val loggerMap: ConcurrentMap<String, TalerLogger> = ConcurrentHashMap() - - override fun getLogger(name: String): Logger - = loggerMap.computeIfAbsent(name, ::TalerLogger) - } -} diff --git a/libeufin-common/src/main/kotlin/params.kt b/libeufin-common/src/main/kotlin/params.kt @@ -1,149 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2024-2025 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.common - -import io.ktor.http.* -import kotlin.math.min -import java.util.* - -fun Parameters.expect(name: String): String - = get(name) ?: throw badRequest("Missing '$name' parameter", TalerErrorCode.GENERIC_PARAMETER_MISSING) -fun Parameters.int(name: String): Int? - = get(name)?.run { toIntOrNull() ?: throw paramsMalformed("Param '$name' not a number") } -fun Parameters.expectInt(name: String): Int - = int(name) ?: throw badRequest("Missing '$name' number parameter", TalerErrorCode.GENERIC_PARAMETER_MISSING) -fun Parameters.long(name: String): Long? - = get(name)?.run { toLongOrNull() ?: throw paramsMalformed("Param '$name' not a number") } -fun Parameters.expectLong(name: String): Long - = long(name) ?: throw badRequest("Missing '$name' number parameter", TalerErrorCode.GENERIC_PARAMETER_MISSING) -fun Parameters.uuid(name: String): UUID? - = get(name)?.run { - try { - UUID.fromString(this) - } catch (e: Exception) { - throw paramsMalformed("Param '$name' not an UUID") - } - } -fun Parameters.expectUuid(name: String): UUID - = uuid(name) ?: throw badRequest("Missing '$name' UUID parameter", TalerErrorCode.GENERIC_PARAMETER_MISSING) -fun Parameters.payto(name: String): Payto? - = get(name)?.run { - try { - Payto.parse(this) - } catch (e: Exception) { - throw paramsMalformed("Param '$name' not a valid payto") - } - } -fun Parameters.expectPayto(name: String): Payto - = payto(name) ?: throw badRequest("Missing '$name' payto parameter", TalerErrorCode.GENERIC_PARAMETER_MISSING) -fun Parameters.amount(name: String): TalerAmount? - = get(name)?.run { - try { - TalerAmount(this) - } catch (e: Exception) { - throw paramsMalformed("Param '$name' not a taler amount") - } - } - -data class PageParams( - val limit: Int, val offset: Long -) { - companion object { - fun extract(params: Parameters): PageParams { - val legacy_limit_value = params.int("delta") - val new_limit_value = params.int("limit") - if (legacy_limit_value != null && new_limit_value != null && legacy_limit_value != new_limit_value) - throw paramsMalformed("Param 'limit' cannot be used with param 'delta'") - - val legacy_offset_value = params.long("start") - val new_offset_value = params.long("offset") - if (legacy_offset_value != null && new_offset_value != null && legacy_offset_value != new_offset_value) - throw paramsMalformed("Param 'offset' cannot be used with param 'start'") - - val limit: Int = new_limit_value ?: legacy_limit_value ?: -20 - if (limit == 0) throw paramsMalformed("Param 'limit' must be non-zero") - else if (limit > MAX_PAGE_SIZE) throw paramsMalformed("Param 'limit' must be <= ${MAX_PAGE_SIZE}") - // The sign of 'limit' only selects the direction, so the negative - // side needs the same bound. Note that abs(Int.MIN_VALUE) silently - // returns Int.MIN_VALUE, so an unbounded negative limit would reach - // SQL as a negative LIMIT. - else if (limit < -MAX_PAGE_SIZE) throw paramsMalformed("Param 'limit' must be >= ${-MAX_PAGE_SIZE}") - val offset: Long = new_offset_value ?: legacy_offset_value ?: if (limit >= 0) 0L else Long.MAX_VALUE - if (offset < 0) throw paramsMalformed("Param 'offset' must be a positive number") - - return PageParams(limit, offset) - } - } -} - -data class TransferParams( - val page: PageParams, val status: TransferStatusState? -) { - companion object { - private val names = TransferStatusState.entries.map { it.name } - private val names_fmt = names.joinToString() - fun extract(params: Parameters): TransferParams { - val status = params["status"]?.let { - if (!names.contains(it)) { - throw paramsMalformed("Param 'status' must be one of $names_fmt") - } - TransferStatusState.valueOf(it) - } - return TransferParams(PageParams.extract(params), status) - } - } -} - -data class PollingParams( - val timeout_ms: Long -) { - companion object { - fun extract(params: Parameters): PollingParams { - val legacy_value = params.long("long_poll_ms") - val new_value = params.long("timeout_ms") - if (legacy_value != null && new_value != null && legacy_value != new_value) - throw paramsMalformed("Param 'timeout_ms' cannot be used with param 'long_poll_ms'") - val timeout_ms: Long = min(new_value ?: legacy_value ?: 0, MAX_TIMEOUT_MS) - if (timeout_ms < 0) throw paramsMalformed("Param 'timeout_ms' must be a positive number") - return PollingParams(timeout_ms) - } - } -} - -data class HistoryParams( - val page: PageParams, val polling: PollingParams -) { - companion object { - fun extract(params: Parameters): HistoryParams { - return HistoryParams(PageParams.extract(params), PollingParams.extract(params)) - } - } -} - -data class AccountCheckParams( - val account: Payto -) { - companion object { - fun extract(params: Parameters): AccountCheckParams { - val account = params.expectPayto("account") - return AccountCheckParams(account) - } - } -} -\ No newline at end of file diff --git a/libeufin-common/src/main/kotlin/registry.kt b/libeufin-common/src/main/kotlin/registry.kt @@ -1,215 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2025 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.common - -/** IBAN ASCII characters rules */ -enum class IbanC { - /** Digits (0-9) */ - n, - /** Uppercase (A-Z) */ - a, - /** Digits or uppercase (0-9 & A-Z) */ - c, -} - -enum class Country(val ibanLen: Int, val rules: List<Pair<Int, IbanC>>, val bbanRegex: Regex) { - AD(24, listOf(Pair(8, IbanC.n),Pair(12, IbanC.c),), Regex("^[0-9]{8}[0-9A-Z]{12}$")), - AE(23, listOf(Pair(19, IbanC.n),), Regex("^[0-9]{19}$")), - AL(28, listOf(Pair(8, IbanC.n),Pair(16, IbanC.c),), Regex("^[0-9]{8}[0-9A-Z]{16}$")), - AT(20, listOf(Pair(16, IbanC.n),), Regex("^[0-9]{16}$")), - AZ(28, listOf(Pair(4, IbanC.a),Pair(20, IbanC.c),), Regex("^[A-Z]{4}[0-9A-Z]{20}$")), - BA(20, listOf(Pair(16, IbanC.n),), Regex("^[0-9]{16}$")), - BE(16, listOf(Pair(12, IbanC.n),), Regex("^[0-9]{12}$")), - BG(22, listOf(Pair(4, IbanC.a),Pair(6, IbanC.n),Pair(8, IbanC.c),), Regex("^[A-Z]{4}[0-9]{6}[0-9A-Z]{8}$")), - BH(22, listOf(Pair(4, IbanC.a),Pair(14, IbanC.c),), Regex("^[A-Z]{4}[0-9A-Z]{14}$")), - BI(27, listOf(Pair(23, IbanC.n),), Regex("^[0-9]{23}$")), - BR(29, listOf(Pair(23, IbanC.n),Pair(1, IbanC.a),Pair(1, IbanC.c),), Regex("^[0-9]{23}[A-Z]{1}[0-9A-Z]{1}$")), - BY(28, listOf(Pair(4, IbanC.c),Pair(4, IbanC.n),Pair(16, IbanC.c),), Regex("^[0-9A-Z]{4}[0-9]{4}[0-9A-Z]{16}$")), - CH(21, listOf(Pair(5, IbanC.n),Pair(12, IbanC.c),), Regex("^[0-9]{5}[0-9A-Z]{12}$")), - CR(22, listOf(Pair(18, IbanC.n),), Regex("^[0-9]{18}$")), - CY(28, listOf(Pair(8, IbanC.n),Pair(16, IbanC.c),), Regex("^[0-9]{8}[0-9A-Z]{16}$")), - CZ(24, listOf(Pair(20, IbanC.n),), Regex("^[0-9]{20}$")), - DE(22, listOf(Pair(18, IbanC.n),), Regex("^[0-9]{18}$")), - DJ(27, listOf(Pair(23, IbanC.n),), Regex("^[0-9]{23}$")), - DK(18, listOf(Pair(14, IbanC.n),), Regex("^[0-9]{14}$")), - DO(28, listOf(Pair(4, IbanC.c),Pair(20, IbanC.n),), Regex("^[0-9A-Z]{4}[0-9]{20}$")), - EE(20, listOf(Pair(16, IbanC.n),), Regex("^[0-9]{16}$")), - EG(29, listOf(Pair(25, IbanC.n),), Regex("^[0-9]{25}$")), - ES(24, listOf(Pair(20, IbanC.n),), Regex("^[0-9]{20}$")), - FI(18, listOf(Pair(14, IbanC.n),), Regex("^[0-9]{14}$")), - FK(18, listOf(Pair(2, IbanC.a),Pair(12, IbanC.n),), Regex("^[A-Z]{2}[0-9]{12}$")), - FO(18, listOf(Pair(14, IbanC.n),), Regex("^[0-9]{14}$")), - FR(27, listOf(Pair(10, IbanC.n),Pair(11, IbanC.c),Pair(2, IbanC.n),), Regex("^[0-9]{10}[0-9A-Z]{11}[0-9]{2}$")), - GB(22, listOf(Pair(4, IbanC.a),Pair(14, IbanC.n),), Regex("^[A-Z]{4}[0-9]{14}$")), - GE(22, listOf(Pair(2, IbanC.a),Pair(16, IbanC.n),), Regex("^[A-Z]{2}[0-9]{16}$")), - GI(23, listOf(Pair(4, IbanC.a),Pair(15, IbanC.c),), Regex("^[A-Z]{4}[0-9A-Z]{15}$")), - GL(18, listOf(Pair(14, IbanC.n),), Regex("^[0-9]{14}$")), - GR(27, listOf(Pair(7, IbanC.n),Pair(16, IbanC.c),), Regex("^[0-9]{7}[0-9A-Z]{16}$")), - GT(28, listOf(Pair(24, IbanC.c),), Regex("^[0-9A-Z]{24}$")), - HN(28, listOf(Pair(4, IbanC.a),Pair(20, IbanC.n),), Regex("^[A-Z]{4}[0-9]{20}$")), - HR(21, listOf(Pair(17, IbanC.n),), Regex("^[0-9]{17}$")), - HU(28, listOf(Pair(24, IbanC.n),), Regex("^[0-9]{24}$")), - IE(22, listOf(Pair(4, IbanC.a),Pair(14, IbanC.n),), Regex("^[A-Z]{4}[0-9]{14}$")), - IL(23, listOf(Pair(19, IbanC.n),), Regex("^[0-9]{19}$")), - IQ(23, listOf(Pair(4, IbanC.a),Pair(15, IbanC.n),), Regex("^[A-Z]{4}[0-9]{15}$")), - IS(26, listOf(Pair(22, IbanC.n),), Regex("^[0-9]{22}$")), - IT(27, listOf(Pair(1, IbanC.a),Pair(10, IbanC.n),Pair(12, IbanC.c),), Regex("^[A-Z]{1}[0-9]{10}[0-9A-Z]{12}$")), - JO(30, listOf(Pair(4, IbanC.a),Pair(4, IbanC.n),Pair(18, IbanC.c),), Regex("^[A-Z]{4}[0-9]{4}[0-9A-Z]{18}$")), - KW(30, listOf(Pair(4, IbanC.a),Pair(22, IbanC.c),), Regex("^[A-Z]{4}[0-9A-Z]{22}$")), - KZ(20, listOf(Pair(3, IbanC.n),Pair(13, IbanC.c),), Regex("^[0-9]{3}[0-9A-Z]{13}$")), - LB(28, listOf(Pair(4, IbanC.n),Pair(20, IbanC.c),), Regex("^[0-9]{4}[0-9A-Z]{20}$")), - LC(32, listOf(Pair(4, IbanC.a),Pair(24, IbanC.c),), Regex("^[A-Z]{4}[0-9A-Z]{24}$")), - LI(21, listOf(Pair(5, IbanC.n),Pair(12, IbanC.c),), Regex("^[0-9]{5}[0-9A-Z]{12}$")), - LT(20, listOf(Pair(16, IbanC.n),), Regex("^[0-9]{16}$")), - LU(20, listOf(Pair(3, IbanC.n),Pair(13, IbanC.c),), Regex("^[0-9]{3}[0-9A-Z]{13}$")), - LV(21, listOf(Pair(4, IbanC.a),Pair(13, IbanC.c),), Regex("^[A-Z]{4}[0-9A-Z]{13}$")), - LY(25, listOf(Pair(21, IbanC.n),), Regex("^[0-9]{21}$")), - MC(27, listOf(Pair(10, IbanC.n),Pair(11, IbanC.c),Pair(2, IbanC.n),), Regex("^[0-9]{10}[0-9A-Z]{11}[0-9]{2}$")), - MD(24, listOf(Pair(20, IbanC.c),), Regex("^[0-9A-Z]{20}$")), - ME(22, listOf(Pair(18, IbanC.n),), Regex("^[0-9]{18}$")), - MK(19, listOf(Pair(3, IbanC.n),Pair(10, IbanC.c),Pair(2, IbanC.n),), Regex("^[0-9]{3}[0-9A-Z]{10}[0-9]{2}$")), - MN(20, listOf(Pair(16, IbanC.n),), Regex("^[0-9]{16}$")), - MR(27, listOf(Pair(23, IbanC.n),), Regex("^[0-9]{23}$")), - MT(31, listOf(Pair(4, IbanC.a),Pair(5, IbanC.n),Pair(18, IbanC.c),), Regex("^[A-Z]{4}[0-9]{5}[0-9A-Z]{18}$")), - MU(30, listOf(Pair(4, IbanC.a),Pair(19, IbanC.n),Pair(3, IbanC.a),), Regex("^[A-Z]{4}[0-9]{19}[A-Z]{3}$")), - NI(28, listOf(Pair(4, IbanC.a),Pair(20, IbanC.n),), Regex("^[A-Z]{4}[0-9]{20}$")), - NL(18, listOf(Pair(4, IbanC.a),Pair(10, IbanC.n),), Regex("^[A-Z]{4}[0-9]{10}$")), - NO(15, listOf(Pair(11, IbanC.n),), Regex("^[0-9]{11}$")), - OM(23, listOf(Pair(3, IbanC.n),Pair(16, IbanC.c),), Regex("^[0-9]{3}[0-9A-Z]{16}$")), - PK(24, listOf(Pair(4, IbanC.a),Pair(16, IbanC.c),), Regex("^[A-Z]{4}[0-9A-Z]{16}$")), - PL(28, listOf(Pair(24, IbanC.n),), Regex("^[0-9]{24}$")), - PS(29, listOf(Pair(4, IbanC.a),Pair(21, IbanC.c),), Regex("^[A-Z]{4}[0-9A-Z]{21}$")), - PT(25, listOf(Pair(21, IbanC.n),), Regex("^[0-9]{21}$")), - QA(29, listOf(Pair(4, IbanC.a),Pair(21, IbanC.c),), Regex("^[A-Z]{4}[0-9A-Z]{21}$")), - RO(24, listOf(Pair(4, IbanC.a),Pair(16, IbanC.c),), Regex("^[A-Z]{4}[0-9A-Z]{16}$")), - RS(22, listOf(Pair(18, IbanC.n),), Regex("^[0-9]{18}$")), - RU(33, listOf(Pair(14, IbanC.n),Pair(15, IbanC.c),), Regex("^[0-9]{14}[0-9A-Z]{15}$")), - SA(24, listOf(Pair(2, IbanC.n),Pair(18, IbanC.c),), Regex("^[0-9]{2}[0-9A-Z]{18}$")), - SC(31, listOf(Pair(4, IbanC.a),Pair(20, IbanC.n),Pair(3, IbanC.a),), Regex("^[A-Z]{4}[0-9]{20}[A-Z]{3}$")), - SD(18, listOf(Pair(14, IbanC.n),), Regex("^[0-9]{14}$")), - SE(24, listOf(Pair(20, IbanC.n),), Regex("^[0-9]{20}$")), - SI(19, listOf(Pair(15, IbanC.n),), Regex("^[0-9]{15}$")), - SK(24, listOf(Pair(20, IbanC.n),), Regex("^[0-9]{20}$")), - SM(27, listOf(Pair(1, IbanC.a),Pair(10, IbanC.n),Pair(12, IbanC.c),), Regex("^[A-Z]{1}[0-9]{10}[0-9A-Z]{12}$")), - SO(23, listOf(Pair(19, IbanC.n),), Regex("^[0-9]{19}$")), - ST(25, listOf(Pair(21, IbanC.n),), Regex("^[0-9]{21}$")), - SV(28, listOf(Pair(4, IbanC.a),Pair(20, IbanC.n),), Regex("^[A-Z]{4}[0-9]{20}$")), - TL(23, listOf(Pair(19, IbanC.n),), Regex("^[0-9]{19}$")), - TN(24, listOf(Pair(20, IbanC.n),), Regex("^[0-9]{20}$")), - TR(26, listOf(Pair(6, IbanC.n),Pair(16, IbanC.c),), Regex("^[0-9]{6}[0-9A-Z]{16}$")), - UA(29, listOf(Pair(6, IbanC.n),Pair(19, IbanC.c),), Regex("^[0-9]{6}[0-9A-Z]{19}$")), - VA(22, listOf(Pair(18, IbanC.n),), Regex("^[0-9]{18}$")), - VG(24, listOf(Pair(4, IbanC.a),Pair(16, IbanC.n),), Regex("^[A-Z]{4}[0-9]{16}$")), - XK(20, listOf(Pair(16, IbanC.n),), Regex("^[0-9]{16}$")), - YE(30, listOf(Pair(4, IbanC.a),Pair(4, IbanC.n),Pair(18, IbanC.c),), Regex("^[A-Z]{4}[0-9]{4}[0-9A-Z]{18}$")), -; - val bbanLen get() = ibanLen - 4 -} -val VALID_IBAN = listOf( - Pair("AD1200012030200359100100", "00012030200359100100"), - Pair("AE070331234567890123456", "0331234567890123456"), - Pair("AL47212110090000000235698741", "212110090000000235698741"), - Pair("AT611904300234573201", "1904300234573201"), - Pair("AZ21NABZ00000000137010001944", "NABZ00000000137010001944"), - Pair("BA391290079401028494", "1290079401028494"), - Pair("BE68539007547034", "539007547034"), - Pair("BG80BNBG96611020345678", "BNBG96611020345678"), - Pair("BH67BMAG00001299123456", "BMAG00001299123456"), - Pair("BI4210000100010000332045181", "10000100010000332045181"), - Pair("BR1800360305000010009795493C1", "00360305000010009795493C1"), - Pair("BY13NBRB3600900000002Z00AB00", "NBRB 3600900000002Z00AB00"), - Pair("CH9300762011623852957", "00762011623852957"), - Pair("CR05015202001026284066", "015202001026284066"), - Pair("CY17002001280000001200527600", "002001280000001200527600"), - Pair("CZ6508000000192000145399", "08000000192000145399"), - Pair("DE89370400440532013000", "370400440532013000"), - Pair("DJ2100010000000154000100186", "00010000000154000100186"), - Pair("DK5000400440116243", "00400440116243"), - Pair("DO28BAGR00000001212453611324", "BAGR00000001212453611324"), - Pair("EE382200221020145685", "2200221020145685"), - Pair("EG380019000500000000263180002", "0019000500000000263180002"), - Pair("ES9121000418450200051332", "21000418450200051332"), - Pair("FI2112345600000785", null), - Pair("FK88SC123456789012", "SC123456789012"), - Pair("FO6264600001631634", "64600001631634"), - Pair("FR1420041010050500013M02606", "20041010050500013M02606"), - Pair("GB29NWBK60161331926819", "NWBK60161331926819"), - Pair("GE29NB0000000101904917", "NB0000000101904917"), - Pair("GI75NWBK000000007099453", "NWBK000000007099453"), - Pair("GL8964710001000206", "64710001000206"), - Pair("GR1601101250000000012300695", "01101250000000012300695"), - Pair("GT82TRAJ01020000001210029690", "TRAJ01020000001210029690"), - Pair("HN88CABF00000000000250005469", "CABF00000000000250005469"), - Pair("HR1210010051863000160", "10010051863000160"), - Pair("HU42117730161111101800000000", "117730161111101800000000"), - Pair("IE29AIBK93115212345678", "AIBK93115212345678"), - Pair("IL620108000000099999999", "0108000000099999999"), - Pair("IQ98NBIQ850123456789012", "NBIQ850123456789012"), - Pair("IS140159260076545510730339", "0159260076545510730339"), - Pair("IT60X0542811101000000123456", "X0542811101000000123456"), - Pair("JO94CBJO0010000000000131000302", "CBJO0010000000000131000302"), - Pair("KW81CBKU0000000000001234560101", "CBKU0000000000001234560101"), - Pair("KZ86125KZT5004100100", "125KZT5004100100"), - Pair("LB62099900000001001901229114", "0999 0000 0001 0019 0122 9114"), - Pair("LC55HEMM000100010012001200023015", "HEMM000100010012001200023015"), - Pair("LI21088100002324013AA", "088100002324013AA"), - Pair("LT121000011101001000", "1000011101001000"), - Pair("LU280019400644750000", "0019400644750000"), - Pair("LV80BANK0000435195001", "BANK0000435195001"), - Pair("LY83002048000020100120361", "002048000020100120361"), - Pair("MC5811222000010123456789030", "11222 00001 01234567890 30"), - Pair("MD24AG000225100013104168", "AG000225100013104168"), - Pair("ME25505000012345678951", "505000012345678951"), - Pair("MK07250120000058984", "250120000058984"), - Pair("MN121234123456789123", "1234123456789123"), - Pair("MR1300020001010000123456753", "00020001010000123456753"), - Pair("MT84MALT011000012345MTLCAST001S", "MALT011000012345MTLCAST001S"), - Pair("MU17BOMM0101101030300200000MUR", "BOMM0101101030300200000MUR"), - Pair("NI45BAPR00000013000003558124", "BAPR00000013000003558124"), - Pair("NL91ABNA0417164300", "ABNA0417164300"), - Pair("NO9386011117947", "86011117947"), - Pair("OM810180000001299123456", "0180000001299123456"), - Pair("PK36SCBL0000001123456702", "SCBL0000001123456702"), - Pair("PL61109010140000071219812874", "109010140000071219812874"), - Pair("PS92PALS000000000400123456702", "PALS000000000400123456702"), - Pair("PT50000201231234567890154", "000201231234567890154"), - Pair("QA58DOHB00001234567890ABCDEFG", "DOHB00001234567890ABCDEFG"), - Pair("RO49AAAA1B31007593840000", "AAAA1B31007593840000"), - Pair("RS35260005601001611379", "260005601001611379"), - Pair("RU0304452522540817810538091310419", "044525225 40817 810 5 3809 1310419"), - Pair("SA0380000000608010167519", "80000000608010167519"), - Pair("SC18SSCB11010000000000001497USD", "SSCB11010000000000001497USD"), - Pair("SD2129010501234001", "29010501234001"), - Pair("SE4550000000058398257466", "50000000058398257466"), - Pair("SI56263300012039086", "263300012039086"), - Pair("SK3112000000198742637541", "12000000198742637541"), - Pair("SM86U0322509800000000270100", "U0322509800000000270100"), - Pair("SO211000001001000100141", "1000001001000100141"), - Pair("ST23000100010051845310146", "000100010051845310146"), - Pair("SV62CENR00000000000000700025", "CENR00000000000000700025"), - Pair("TL380080012345678910157", "0080012345678910157"), - Pair("TN5910006035183598478831", "10006035183598478831"), - Pair("TR330006100519786457841326", "0006100519786457841326"), - Pair("UA213223130000026007233566001", "3223130000026007233566001"), - Pair("VA59001123000012345678", "001123000012345678"), - Pair("VG96VPVG0000012345678901", "VPVG0000012345678901"), - Pair("XK051212012345678906", "1212012345678906"), - Pair("YE15CBYE0001018861234567891234", "CBYE0001018861234567891234"), -); -\ No newline at end of file diff --git a/libeufin-common/src/main/kotlin/security.kt b/libeufin-common/src/main/kotlin/security.kt @@ -1,29 +0,0 @@ -/* - * 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.common - -import java.security.Security - -fun setupSecurityProperties() { - // Enable certificate revocation check - System.setProperty("com.sun.net.ssl.checkRevocation", "true"); - System.setProperty("com.sun.security.enableCRLDP", "true"); - Security.setProperty("ocsp.enable", "true"); -} -\ No newline at end of file diff --git a/libeufin-common/src/main/kotlin/test/bench.kt b/libeufin-common/src/main/kotlin/test/bench.kt @@ -1,100 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2024, 2025, 2026 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.common.test - -import org.postgresql.copy.* -import org.postgresql.jdbc.* -import tech.libeufin.common.* -import kotlin.math.pow -import kotlin.math.sqrt -import kotlin.time.DurationUnit -import kotlin.time.measureTime -import kotlin.time.toDuration - -fun PgConnection.genData(amount: Int, generators: Sequence<Pair<String, (Int) -> String>>) { - for ((table, generator) in generators) { - println("Gen rows for $table") - PGCopyOutputStream(this, "COPY $table FROM STDIN", 16 * 1024 * 1024).use { out -> - repeat(amount) { - val str = generator(it+1) - val bytes = str.toByteArray() - out.write(bytes) - } - } - } - - // Update database statistics for better perf - this.execSQLUpdate("VACUUM FULL ANALYZE") -} - -class Benchmark(private val iter: Int) { - private val WARN = 4.toDuration(DurationUnit.MILLISECONDS) - private val ERR = 50.toDuration(DurationUnit.MILLISECONDS) - internal val measures: MutableList<List<String>> = mutableListOf() - - internal fun fmtMeasures(times: LongArray): List<String> { - val min: Long = times.min() - val max: Long = times.max() - val mean: Long = times.average().toLong() - val variance = times.map { (it.toDouble() - mean).pow(2) }.average() - val stdVar: Long = sqrt(variance.toDouble()).toLong() - return sequenceOf(min, mean, max, stdVar).map { - val duration = it.toDuration(DurationUnit.MICROSECONDS) - val str = duration.toString() - if (duration > ERR) { - ANSI.red(str) - } else if (duration > WARN) { - ANSI.yellow(str) - } else { - ANSI.green(str) - } - - }.toList() - } - - suspend fun <R> measureAction(name: String, lambda: suspend (Int) -> R): List<R> { - println("Measure action $name") - val results = mutableListOf<R>() - val times = LongArray(iter) { idx -> - measureTime { - val result = lambda(idx) - results.add(result) - }.inWholeMicroseconds - } - measures.add(listOf(ANSI.magenta(name)) + fmtMeasures(times)) - return results - } -} - -fun bench(lambda: Benchmark.(Int) -> Unit) { - val ITER = System.getenv("BENCH_ITER")?.toIntOrNull() ?: 10 - val AMOUNT = System.getenv("BENCH_AMOUNT")?.toIntOrNull() ?: 100 - - println("Bench $ITER times with $AMOUNT rows") - - val bench = Benchmark(ITER) - bench.lambda(AMOUNT) - printTable( - listOf("benchmark", "min", "mean", "max", "std").map { ANSI.bold(it) }, - bench.measures, - ' ', - listOf(ColumnStyle.DEFAULT) + List(5) { ColumnStyle(false) } - ) -} -\ No newline at end of file diff --git a/libeufin-common/src/main/kotlin/test/cli.kt b/libeufin-common/src/main/kotlin/test/cli.kt @@ -1,33 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2025 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.common.test - -import com.github.ajalt.clikt.core.CliktCommand -import com.github.ajalt.clikt.testing.test - -fun CliktCommand.fail(cmd: String) { - val result = test(cmd) - require(result.statusCode != 0) { result.output } -} - -fun CliktCommand.succeed(cmd: String) { - val result = test(cmd) - require(result.statusCode == 0) { result.output } -} diff --git a/libeufin-common/src/main/kotlin/test/helpers.kt b/libeufin-common/src/main/kotlin/test/helpers.kt @@ -1,176 +0,0 @@ -/* - * 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.common.test - -import io.ktor.client.* -import io.ktor.client.request.* -import io.ktor.client.statement.* -import io.ktor.http.* -import io.ktor.server.testing.* -import tech.libeufin.common.* -import kotlin.test.assertEquals -import kotlinx.serialization.json.* - -/* ----- Assert ----- */ - -suspend fun assertTime(min: Int, max: Int, lambda: suspend () -> Unit) { - val start = System.currentTimeMillis() - lambda() - val end = System.currentTimeMillis() - val time = end - start - assert(time >= min) { "Expected to last at least $min ms, lasted $time" } - assert(time <= max) { "Expected to last at most $max ms, lasted $time" } -} - -suspend inline fun <reified B> HttpResponse.assertHistoryIds(size: Int, ids: (B) -> List<Long>): B { - assertOk() - val body = json<B>() - val history = ids(body) - val params = PageParams.extract(call.request.url.parameters) - - // testing the size is like expected. - assertEquals(size, history.size, "bad history length: $history") - if (params.limit < 0) { - // testing that the first id is at most the 'offset' query param. - assert(history[0] <= params.offset) { "bad history offset: $params $history" } - // testing that the id decreases. - if (history.size > 1) - assert(history.windowed(2).all { (a, b) -> a > b }) { "bad history order: $history" } - } else { - // testing that the first id is at least the 'offset' query param. - assert(history[0] >= params.offset) { "bad history offset: $params $history" } - // testing that the id increases. - if (history.size > 1) - assert(history.windowed(2).all { (a, b) -> a < b }) { "bad history order: $history" } - } - - return body -} - -/* ----- Auth ----- */ - -typealias RequestLambda = suspend HttpRequestBuilder.() -> Unit; - -/** Auto token auth GET request */ -suspend fun HttpClient.getA(url: String, builder: RequestLambda = {}): HttpResponse - = tokenAuthRequest(url, HttpMethod.Get, null, builder) -/** Auto token auth POST request */ -suspend fun HttpClient.postA(url: String, builder: RequestLambda = {}): HttpResponse - = tokenAuthRequest(url, HttpMethod.Post, null, builder) -/** Auto token auth PATCH request */ -suspend fun HttpClient.patchA(url: String, builder: RequestLambda = {}): HttpResponse - = tokenAuthRequest(url, HttpMethod.Patch, null, builder) -/** Auto token auth DELETE request */ -suspend fun HttpClient.deleteA(url: String, builder: RequestLambda = {}): HttpResponse - = tokenAuthRequest(url, HttpMethod.Delete, null, builder) - -/** Admin token auth GET request */ -suspend fun HttpClient.getAdmin(url: String, builder: RequestLambda = {}): HttpResponse - = tokenAuthRequest(url, HttpMethod.Get, "admin", builder) -/** Admin token auth PATCH request */ -suspend fun HttpClient.patchAdmin(url: String, builder: RequestLambda = {}): HttpResponse - = tokenAuthRequest(url, HttpMethod.Patch, "admin", builder) -/** Admin token auth POST request */ -suspend fun HttpClient.postAdmin(url: String, builder: RequestLambda = {}): HttpResponse - = tokenAuthRequest(url, HttpMethod.Post, "admin", builder) -/** Admin token auth DELETE request */ -suspend fun HttpClient.deleteAdmin(url: String, builder: RequestLambda = {}): HttpResponse - = tokenAuthRequest(url, HttpMethod.Delete, "admin", builder) - -/** Auto pw auth GET request */ -suspend fun HttpClient.getPw(url: String, builder: RequestLambda = {}): HttpResponse - = pwAuthRequest(url, HttpMethod.Get, null, builder) -/** Auto pw auth POST request */ -suspend fun HttpClient.postPw(url: String, builder: RequestLambda = {}): HttpResponse - = pwAuthRequest(url, HttpMethod.Post, null, builder) -/** Auto pw auth PATCH request */ -suspend fun HttpClient.patchPw(url: String, builder: RequestLambda = {}): HttpResponse - = pwAuthRequest(url, HttpMethod.Patch, null, builder) -/** Auto pw auth DELETE request */ -suspend fun HttpClient.deletePw(url: String, builder: RequestLambda = {}): HttpResponse - = pwAuthRequest(url, HttpMethod.Delete, null, builder) - -private suspend fun HttpClient.tokenAuthRequest( - url: String, - method: HttpMethod, - username: String?, - builder: RequestLambda = {} -): HttpResponse = request(url) { - this.method = method - tokenAuth(this@tokenAuthRequest, username) - builder(this) -} - -private suspend fun HttpClient.pwAuthRequest( - url: String, - method: HttpMethod, - username: String?, - builder: RequestLambda = {} -): HttpResponse = request(url) { - this.method = method - pwAuth(username) - builder(this) -} - -private fun HttpRequestBuilder.extractUsername(username: String? = null): String? - = when { - username != null -> username - url.pathSegments.contains("admin") -> "admin" - url.pathSegments[1] == "accounts" -> url.pathSegments[2] - else -> null - } - -/** Authenticate a request for [username] with basic auth */ -fun HttpRequestBuilder.pwAuth(username: String? = null) { - val username = extractUsername(username) ?: return - basicAuth("$username", "$username-password") -} - -val globalTestTokens = mutableMapOf<String, String>() - -/** Get cached token or create it */ -suspend fun HttpClient.cachedToken(username: String): String { - // Get cached token or create it - var token = globalTestTokens.get(username) - if (token == null) { - val response = this.post("/accounts/$username/token") { - pwAuth() - json { - "scope" to "readwrite" - "duration" to obj { - "d_us" to "forever" - } - } - }.assertOkJson<JsonObject>() - token = Json.decodeFromJsonElement<String>(response.get("access_token")!!) - globalTestTokens.set(username, token) - } - return token -} - -/** Authenticate a request for [username] with a bearer token */ -suspend fun HttpRequestBuilder.tokenAuth(client: HttpClient, username: String? = null) { - // Get username from arg or path - val username = extractUsername(username) ?: return - // Get cached token or create it - var token = client.cachedToken(username) - // Set authorization header - headers[HttpHeaders.Authorization] = "Bearer $token" -} -\ No newline at end of file diff --git a/libeufin-common/src/main/kotlin/test/routines.kt b/libeufin-common/src/main/kotlin/test/routines.kt @@ -1,140 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2024, 2026 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.common.test - -import io.ktor.client.statement.* -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import tech.libeufin.common.assertNoContent -import tech.libeufin.common.assertOkJson - -suspend inline fun <reified B> abstractHistoryRoutine( - crossinline ids: (B) -> List<Long>, - registered: List<suspend () -> Unit>, - ignored: List<suspend () -> Unit> = listOf(), - polling: Boolean = true, - crossinline history: suspend (String) -> HttpResponse, -) { - // Check history is following specs - val assertHistory: suspend HttpResponse.(Int) -> Unit = { size: Int -> - assertHistoryIds<B>(size, ids) - } - // Get latest registered id - val latestId: suspend () -> Long = { - history("limit=-1").assertOkJson<B>().run { ids(this)[0] } - } - - // Check error when no transactions - history("limit=7").assertNoContent() - - // Run interleaved registered and ignore transactions - val registeredIter = registered.iterator() - val ignoredIter = ignored.iterator() - while (registeredIter.hasNext() || ignoredIter.hasNext()) { - if (registeredIter.hasNext()) registeredIter.next()() - if (ignoredIter.hasNext()) ignoredIter.next()() - } - - val nbRegistered = registered.size - val nbIgnored = ignored.size - val nbTotal = nbRegistered + nbIgnored - - // Check ignored - history("limit=$nbTotal").assertHistory(nbRegistered) - // Check skip ignored - history("limit=$nbRegistered").assertHistory(nbRegistered) - - if (polling) { - // Check no polling when we cannot have more transactions - assertTime(0, 500) { - history("limit=-${nbRegistered+1}&timeout_ms=1000") - .assertHistory(nbRegistered) - } - // Check no polling when already find transactions even if less than delta - assertTime(0, 500) { - history("limit=${nbRegistered+1}&timeout_ms=1000") - .assertHistory(nbRegistered) - } - - // Check polling - coroutineScope { - val id = latestId() - launch { // Check polling succeed - assertTime(100, 500) { - history("limit=2&offset=$id&timeout_ms=1000") - .assertHistory(1) - } - } - launch { // Check polling timeout - assertTime(200, 500) { - history("limit=1&offset=${id+nbTotal*3}&timeout_ms=200") - .assertNoContent() - } - } - delay(100) - registered[0]() - } - - // Test triggers - for (register in registered) { - coroutineScope { - val id = latestId() - launch { - assertTime(100, 500) { - history("limit=7&offset=$id&timeout_ms=1000") - .assertHistory(1) - } - } - delay(100) - register() - } - } - - // Test doesn't trigger - coroutineScope { - val id = latestId() - launch { - assertTime(200, 500) { - history("limit=7&offset=$id&timeout_ms=200") - .assertNoContent() - } - } - delay(100) - for (ignore in ignored) { - ignore() - } - } - } - - // Testing ranges. - repeat(20) { - registered[0]() - } - val id = latestId() - // Default - history("").assertHistory(20) - // forward range: - history("limit=10").assertHistory(10) - history("limit=10&offset=4").assertHistory(10) - // backward range: - history("limit=-10").assertHistory(10) - history("limit=-10&offset=${id-4}").assertHistory(10) -} -\ No newline at end of file diff --git a/libeufin-common/src/main/kotlin/time.kt b/libeufin-common/src/main/kotlin/time.kt @@ -1,59 +0,0 @@ -/* - * 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.common - -import org.slf4j.Logger -import org.slf4j.LoggerFactory -import java.time.Instant -import java.time.temporal.ChronoUnit - -private val logger: Logger = LoggerFactory.getLogger("libeufin-common") - -/** - * Convert Instant to microseconds since the epoch. - * - * Returns Long.MAX_VALUE if instant is Instant.MAX - **/ -fun Instant.micros(): Long { - if (this == Instant.MAX) - return Long.MAX_VALUE - try { - val micros = ChronoUnit.MICROS.between(Instant.EPOCH, this) - if (micros == Long.MAX_VALUE) throw ArithmeticException() - return micros - } catch (e: ArithmeticException) { - throw Exception("$this is too big to be converted to micros resolution", e) - } -} - -/** - * Convert microsecons to Instant. - * - * Returns Instant.MAX if microseconds is Long.MAX_VALUE - */ -fun Long.asInstant(): Instant { - if (this == Long.MAX_VALUE) - return Instant.MAX - return try { - Instant.EPOCH.plus(this, ChronoUnit.MICROS) - } catch (e: ArithmeticException ) { - throw Exception("$this is too big to be converted to Instant", e) - } -} -\ No newline at end of file diff --git a/libeufin-common/src/main/resources/META-INF/services/org.slf4j.spi.SLF4JServiceProvider b/libeufin-common/src/main/resources/META-INF/services/org.slf4j.spi.SLF4JServiceProvider @@ -1 +0,0 @@ -tech.libeufin.common.TalerServiceProvider -\ No newline at end of file diff --git a/libeufin-common/src/main/resources/xsd/camt.052.001.02.xsd b/libeufin-common/src/main/resources/xsd/camt.052.001.02.xsd @@ -1,1299 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<!-- Mit XMLSpy v2008 rel. 2 (http://www.altova.com) von Wenzel (SIZ Bonn) bearbeitet --> -<!--Generated by SWIFTStandards Workstation (build:R6.1.0.2) on 2009 Jan 08 17:30:53--> -<xs:schema xmlns="urn:iso:std:iso:20022:tech:xsd:camt.052.001.02" xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="urn:iso:std:iso:20022:tech:xsd:camt.052.001.02" elementFormDefault="qualified"> - <xs:element name="Document" type="Document"/> - <xs:complexType name="AccountIdentification4Choice"> - <xs:sequence> - <xs:choice> - <xs:element name="IBAN" type="IBAN2007Identifier"/> - <xs:element name="Othr" type="GenericAccountIdentification1"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:complexType name="AccountInterest2"> - <xs:sequence> - <xs:element name="Tp" type="InterestType1Choice" minOccurs="0"/> - <xs:element name="Rate" type="Rate3" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="FrToDt" type="DateTimePeriodDetails" minOccurs="0"/> - <xs:element name="Rsn" type="Max35Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="AccountReport11"> - <xs:sequence> - <xs:element name="Id" type="Max35Text"/> - <xs:element name="ElctrncSeqNb" type="Number" minOccurs="0"/> - <xs:element name="LglSeqNb" type="Number" minOccurs="0"/> - <xs:element name="CreDtTm" type="ISODateTime"/> - <xs:element name="FrToDt" type="DateTimePeriodDetails" minOccurs="0"/> - <xs:element name="CpyDplctInd" type="CopyDuplicate1Code" minOccurs="0"/> - <xs:element name="RptgSrc" type="ReportingSource1Choice" minOccurs="0"/> - <xs:element name="Acct" type="CashAccount20"/> - <xs:element name="RltdAcct" type="CashAccount16" minOccurs="0"/> - <xs:element name="Intrst" type="AccountInterest2" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="Bal" type="CashBalance3" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="TxsSummry" type="TotalTransactions2" minOccurs="0"/> - <xs:element name="Ntry" type="ReportEntry2" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="AddtlRptInf" type="Max500Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="AccountSchemeName1Choice"> - <xs:sequence> - <xs:choice> - <xs:element name="Cd" type="ExternalAccountIdentification1Code"/> - <xs:element name="Prtry" type="Max35Text"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="ActiveOrHistoricCurrencyAndAmount_SimpleType"> - <xs:restriction base="xs:decimal"> - <xs:minInclusive value="0"/> - <xs:fractionDigits value="5"/> - <xs:totalDigits value="18"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="ActiveOrHistoricCurrencyAndAmount"> - <xs:simpleContent> - <xs:extension base="ActiveOrHistoricCurrencyAndAmount_SimpleType"> - <xs:attribute name="Ccy" type="ActiveOrHistoricCurrencyCode" use="required"/> - </xs:extension> - </xs:simpleContent> - </xs:complexType> - <xs:simpleType name="ActiveOrHistoricCurrencyCode"> - <xs:restriction base="xs:string"> - <xs:pattern value="[A-Z]{3,13}"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="AddressType2Code"> - <xs:restriction base="xs:string"> - <xs:enumeration value="ADDR"/> - <xs:enumeration value="PBOX"/> - <xs:enumeration value="HOME"/> - <xs:enumeration value="BIZZ"/> - <xs:enumeration value="MLTO"/> - <xs:enumeration value="DLVY"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="AlternateSecurityIdentification2"> - <xs:sequence> - <xs:element name="Tp" type="Max35Text"/> - <xs:element name="Id" type="Max35Text"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="AmountAndCurrencyExchange3"> - <xs:sequence> - <xs:element name="InstdAmt" type="AmountAndCurrencyExchangeDetails3" minOccurs="0"/> - <xs:element name="TxAmt" type="AmountAndCurrencyExchangeDetails3" minOccurs="0"/> - <xs:element name="CntrValAmt" type="AmountAndCurrencyExchangeDetails3" minOccurs="0"/> - <xs:element name="AnncdPstngAmt" type="AmountAndCurrencyExchangeDetails3" minOccurs="0"/> - <xs:element name="PrtryAmt" type="AmountAndCurrencyExchangeDetails4" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="AmountAndCurrencyExchangeDetails3"> - <xs:sequence> - <xs:element name="Amt" type="ActiveOrHistoricCurrencyAndAmount"/> - <xs:element name="CcyXchg" type="CurrencyExchange5" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="AmountAndCurrencyExchangeDetails4"> - <xs:sequence> - <xs:element name="Tp" type="Max35Text"/> - <xs:element name="Amt" type="ActiveOrHistoricCurrencyAndAmount"/> - <xs:element name="CcyXchg" type="CurrencyExchange5" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="AmountRangeBoundary1"> - <xs:sequence> - <xs:element name="BdryAmt" type="ImpliedCurrencyAndAmount"/> - <xs:element name="Incl" type="YesNoIndicator"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="AnyBICIdentifier"> - <xs:restriction base="xs:string"> - <xs:pattern value="[A-Z]{6,6}[A-Z2-9][A-NP-Z0-9]([A-Z0-9]{3,3}){0,1}"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="BICIdentifier"> - <xs:restriction base="xs:string"> - <xs:pattern value="[A-Z]{6,6}[A-Z2-9][A-NP-Z0-9]([A-Z0-9]{3,3}){0,1}"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="BalanceSubType1Choice"> - <xs:sequence> - <xs:choice> - <xs:element name="Cd" type="ExternalBalanceSubType1Code"/> - <xs:element name="Prtry" type="Max35Text"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:complexType name="BalanceType12"> - <xs:sequence> - <xs:element name="CdOrPrtry" type="BalanceType5Choice"/> - <xs:element name="SubTp" type="BalanceSubType1Choice" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="BalanceType12Code"> - <xs:restriction base="xs:string"> - <xs:enumeration value="XPCD"/> - <xs:enumeration value="OPAV"/> - <xs:enumeration value="ITAV"/> - <xs:enumeration value="CLAV"/> - <xs:enumeration value="FWAV"/> - <xs:enumeration value="CLBD"/> - <xs:enumeration value="ITBD"/> - <xs:enumeration value="OPBD"/> - <xs:enumeration value="PRCD"/> - <xs:enumeration value="INFO"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="BalanceType5Choice"> - <xs:sequence> - <xs:choice> - <xs:element name="Cd" type="BalanceType12Code"/> - <xs:element name="Prtry" type="Max35Text"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:complexType name="BankToCustomerAccountReportV02"> - <xs:sequence> - <xs:element name="GrpHdr" type="GroupHeader42"/> - <xs:element name="Rpt" type="AccountReport11" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="BankTransactionCodeStructure4"> - <xs:sequence> - <xs:element name="Domn" type="BankTransactionCodeStructure5" minOccurs="0"/> - <xs:element name="Prtry" type="ProprietaryBankTransactionCodeStructure1" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="BankTransactionCodeStructure5"> - <xs:sequence> - <xs:element name="Cd" type="ExternalBankTransactionDomain1Code"/> - <xs:element name="Fmly" type="BankTransactionCodeStructure6"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="BankTransactionCodeStructure6"> - <xs:sequence> - <xs:element name="Cd" type="ExternalBankTransactionFamily1Code"/> - <xs:element name="SubFmlyCd" type="ExternalBankTransactionSubFamily1Code"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="BaseOneRate"> - <xs:restriction base="xs:decimal"> - <xs:fractionDigits value="10"/> - <xs:totalDigits value="11"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="BatchInformation2"> - <xs:sequence> - <xs:element name="MsgId" type="Max35Text" minOccurs="0"/> - <xs:element name="PmtInfId" type="Max35Text" minOccurs="0"/> - <xs:element name="NbOfTxs" type="Max15NumericText" minOccurs="0"/> - <xs:element name="TtlAmt" type="ActiveOrHistoricCurrencyAndAmount" minOccurs="0"/> - <xs:element name="CdtDbtInd" type="CreditDebitCode" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="BranchAndFinancialInstitutionIdentification4"> - <xs:sequence> - <xs:element name="FinInstnId" type="FinancialInstitutionIdentification7"/> - <xs:element name="BrnchId" type="BranchData2" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="BranchData2"> - <xs:sequence> - <xs:element name="Id" type="Max35Text" minOccurs="0"/> - <xs:element name="Nm" type="Max140Text" minOccurs="0"/> - <xs:element name="PstlAdr" type="PostalAddress6" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CashAccount16"> - <xs:sequence> - <xs:element name="Id" type="AccountIdentification4Choice"/> - <xs:element name="Tp" type="CashAccountType2" minOccurs="0"/> - <xs:element name="Ccy" type="ActiveOrHistoricCurrencyCode" minOccurs="0"/> - <xs:element name="Nm" type="Max70Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CashAccount20"> - <xs:sequence> - <xs:element name="Id" type="AccountIdentification4Choice"/> - <xs:element name="Tp" type="CashAccountType2" minOccurs="0"/> - <xs:element name="Ccy" type="ActiveOrHistoricCurrencyCode" minOccurs="0"/> - <xs:element name="Nm" type="Max70Text" minOccurs="0"/> - <xs:element name="Ownr" type="PartyIdentification32" minOccurs="0"/> - <xs:element name="Svcr" type="BranchAndFinancialInstitutionIdentification4" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CashAccountType2"> - <xs:sequence> - <xs:choice> - <xs:element name="Cd" type="CashAccountType4Code"/> - <xs:element name="Prtry" type="Max35Text"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="CashAccountType4Code"> - <xs:restriction base="xs:string"> - <xs:enumeration value="CASH"/> - <xs:enumeration value="CHAR"/> - <xs:enumeration value="COMM"/> - <xs:enumeration value="TAXE"/> - <xs:enumeration value="CISH"/> - <xs:enumeration value="TRAS"/> - <xs:enumeration value="SACC"/> - <xs:enumeration value="CACC"/> - <xs:enumeration value="SVGS"/> - <xs:enumeration value="ONDP"/> - <xs:enumeration value="MGLD"/> - <xs:enumeration value="NREX"/> - <xs:enumeration value="MOMA"/> - <xs:enumeration value="LOAN"/> - <xs:enumeration value="SLRY"/> - <xs:enumeration value="ODFT"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="CashBalance3"> - <xs:sequence> - <xs:element name="Tp" type="BalanceType12"/> - <xs:element name="CdtLine" type="CreditLine2" minOccurs="0"/> - <xs:element name="Amt" type="ActiveOrHistoricCurrencyAndAmount"/> - <xs:element name="CdtDbtInd" type="CreditDebitCode"/> - <xs:element name="Dt" type="DateAndDateTimeChoice"/> - <xs:element name="Avlbty" type="CashBalanceAvailability2" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CashBalanceAvailability2"> - <xs:sequence> - <xs:element name="Dt" type="CashBalanceAvailabilityDate1"/> - <xs:element name="Amt" type="ActiveOrHistoricCurrencyAndAmount"/> - <xs:element name="CdtDbtInd" type="CreditDebitCode"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CashBalanceAvailabilityDate1"> - <xs:sequence> - <xs:choice> - <xs:element name="NbOfDays" type="Max15PlusSignedNumericText"/> - <xs:element name="ActlDt" type="ISODate"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="ChargeBearerType1Code"> - <xs:restriction base="xs:string"> - <xs:enumeration value="DEBT"/> - <xs:enumeration value="CRED"/> - <xs:enumeration value="SHAR"/> - <xs:enumeration value="SLEV"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ChargeType1Code"> - <xs:restriction base="xs:string"> - <xs:enumeration value="BRKF"/> - <xs:enumeration value="COMM"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="ChargeType2Choice"> - <xs:sequence> - <xs:choice> - <xs:element name="Cd" type="ChargeType1Code"/> - <xs:element name="Prtry" type="GenericIdentification3"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ChargesInformation6"> - <xs:sequence> - <xs:element name="TtlChrgsAndTaxAmt" type="ActiveOrHistoricCurrencyAndAmount" minOccurs="0"/> - <xs:element name="Amt" type="ActiveOrHistoricCurrencyAndAmount"/> - <xs:element name="CdtDbtInd" type="CreditDebitCode" minOccurs="0"/> - <xs:element name="Tp" type="ChargeType2Choice" minOccurs="0"/> - <xs:element name="Rate" type="PercentageRate" minOccurs="0"/> - <xs:element name="Br" type="ChargeBearerType1Code" minOccurs="0"/> - <xs:element name="Pty" type="BranchAndFinancialInstitutionIdentification4" minOccurs="0"/> - <xs:element name="Tax" type="TaxCharges2" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ClearingSystemIdentification2Choice"> - <xs:sequence> - <xs:choice> - <xs:element name="Cd" type="ExternalClearingSystemIdentification1Code"/> - <xs:element name="Prtry" type="Max35Text"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ClearingSystemMemberIdentification2"> - <xs:sequence> - <xs:element name="ClrSysId" type="ClearingSystemIdentification2Choice" minOccurs="0"/> - <xs:element name="MmbId" type="Max35Text"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ContactDetails2"> - <xs:sequence> - <xs:element name="NmPrfx" type="NamePrefix1Code" minOccurs="0"/> - <xs:element name="Nm" type="Max140Text" minOccurs="0"/> - <xs:element name="PhneNb" type="PhoneNumber" minOccurs="0"/> - <xs:element name="MobNb" type="PhoneNumber" minOccurs="0"/> - <xs:element name="FaxNb" type="PhoneNumber" minOccurs="0"/> - <xs:element name="EmailAdr" type="Max2048Text" minOccurs="0"/> - <xs:element name="Othr" type="Max35Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="CopyDuplicate1Code"> - <xs:restriction base="xs:string"> - <xs:enumeration value="CODU"/> - <xs:enumeration value="COPY"/> - <xs:enumeration value="DUPL"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="CorporateAction1"> - <xs:sequence> - <xs:element name="Cd" type="Max35Text" minOccurs="0"/> - <xs:element name="Nb" type="Max35Text" minOccurs="0"/> - <xs:element name="Prtry" type="Max35Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="CountryCode"> - <xs:restriction base="xs:string"> - <xs:pattern value="[A-Z]{2,2}"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="CreditDebitCode"> - <xs:restriction base="xs:string"> - <xs:enumeration value="CRDT"/> - <xs:enumeration value="DBIT"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="CreditLine2"> - <xs:sequence> - <xs:element name="Incl" type="TrueFalseIndicator"/> - <xs:element name="Amt" type="ActiveOrHistoricCurrencyAndAmount" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CreditorReferenceInformation2"> - <xs:sequence> - <xs:element name="Tp" type="CreditorReferenceType2" minOccurs="0"/> - <xs:element name="Ref" type="Max35Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CreditorReferenceType1Choice"> - <xs:sequence> - <xs:choice> - <xs:element name="Cd" type="DocumentType3Code"/> - <xs:element name="Prtry" type="Max35Text"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CreditorReferenceType2"> - <xs:sequence> - <xs:element name="CdOrPrtry" type="CreditorReferenceType1Choice"/> - <xs:element name="Issr" type="Max35Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CurrencyAndAmountRange2"> - <xs:sequence> - <xs:element name="Amt" type="ImpliedCurrencyAmountRangeChoice"/> - <xs:element name="CdtDbtInd" type="CreditDebitCode" minOccurs="0"/> - <xs:element name="Ccy" type="ActiveOrHistoricCurrencyCode"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CurrencyExchange5"> - <xs:sequence> - <xs:element name="SrcCcy" type="ActiveOrHistoricCurrencyCode"/> - <xs:element name="TrgtCcy" type="ActiveOrHistoricCurrencyCode" minOccurs="0"/> - <xs:element name="UnitCcy" type="ActiveOrHistoricCurrencyCode" minOccurs="0"/> - <xs:element name="XchgRate" type="BaseOneRate"/> - <xs:element name="CtrctId" type="Max35Text" minOccurs="0"/> - <xs:element name="QtnDt" type="ISODateTime" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="DateAndDateTimeChoice"> - <xs:sequence> - <xs:choice> - <xs:element name="Dt" type="ISODate"/> - <xs:element name="DtTm" type="ISODateTime"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:complexType name="DateAndPlaceOfBirth"> - <xs:sequence> - <xs:element name="BirthDt" type="ISODate"/> - <xs:element name="PrvcOfBirth" type="Max35Text" minOccurs="0"/> - <xs:element name="CityOfBirth" type="Max35Text"/> - <xs:element name="CtryOfBirth" type="CountryCode"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="DatePeriodDetails"> - <xs:sequence> - <xs:element name="FrDt" type="ISODate"/> - <xs:element name="ToDt" type="ISODate"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="DateTimePeriodDetails"> - <xs:sequence> - <xs:element name="FrDtTm" type="ISODateTime"/> - <xs:element name="ToDtTm" type="ISODateTime"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="DecimalNumber"> - <xs:restriction base="xs:decimal"> - <xs:fractionDigits value="17"/> - <xs:totalDigits value="18"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="Document"> - <xs:sequence> - <xs:element name="BkToCstmrAcctRpt" type="BankToCustomerAccountReportV02"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="DocumentAdjustment1"> - <xs:sequence> - <xs:element name="Amt" type="ActiveOrHistoricCurrencyAndAmount"/> - <xs:element name="CdtDbtInd" type="CreditDebitCode" minOccurs="0"/> - <xs:element name="Rsn" type="Max4Text" minOccurs="0"/> - <xs:element name="AddtlInf" type="Max140Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="DocumentType3Code"> - <xs:restriction base="xs:string"> - <xs:enumeration value="RADM"/> - <xs:enumeration value="RPIN"/> - <xs:enumeration value="FXDR"/> - <xs:enumeration value="DISP"/> - <xs:enumeration value="PUOR"/> - <xs:enumeration value="SCOR"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="DocumentType5Code"> - <xs:restriction base="xs:string"> - <xs:enumeration value="MSIN"/> - <xs:enumeration value="CNFA"/> - <xs:enumeration value="DNFA"/> - <xs:enumeration value="CINV"/> - <xs:enumeration value="CREN"/> - <xs:enumeration value="DEBN"/> - <xs:enumeration value="HIRI"/> - <xs:enumeration value="SBIN"/> - <xs:enumeration value="CMCN"/> - <xs:enumeration value="SOAC"/> - <xs:enumeration value="DISP"/> - <xs:enumeration value="BOLD"/> - <xs:enumeration value="VCHR"/> - <xs:enumeration value="AROI"/> - <xs:enumeration value="TSUT"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="EntryDetails1"> - <xs:sequence> - <xs:element name="Btch" type="BatchInformation2" minOccurs="0"/> - <xs:element name="TxDtls" type="EntryTransaction2" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="EntryStatus2Code"> - <xs:restriction base="xs:string"> - <xs:enumeration value="BOOK"/> - <xs:enumeration value="PDNG"/> - <xs:enumeration value="INFO"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="EntryTransaction2"> - <xs:sequence> - <xs:element name="Refs" type="TransactionReferences2" minOccurs="0"/> - <xs:element name="AmtDtls" type="AmountAndCurrencyExchange3" minOccurs="0"/> - <xs:element name="Avlbty" type="CashBalanceAvailability2" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="BkTxCd" type="BankTransactionCodeStructure4" minOccurs="0"/> - <xs:element name="Chrgs" type="ChargesInformation6" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="Intrst" type="TransactionInterest2" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="RltdPties" type="TransactionParty2" minOccurs="0"/> - <xs:element name="RltdAgts" type="TransactionAgents2" minOccurs="0"/> - <xs:element name="Purp" type="Purpose2Choice" minOccurs="0"/> - <xs:element name="RltdRmtInf" type="RemittanceLocation2" minOccurs="0" maxOccurs="10"/> - <xs:element name="RmtInf" type="RemittanceInformation5" minOccurs="0"/> - <xs:element name="RltdDts" type="TransactionDates2" minOccurs="0"/> - <xs:element name="RltdPric" type="TransactionPrice2Choice" minOccurs="0"/> - <xs:element name="RltdQties" type="TransactionQuantities1Choice" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="FinInstrmId" type="SecurityIdentification4Choice" minOccurs="0"/> - <xs:element name="Tax" type="TaxInformation3" minOccurs="0"/> - <xs:element name="RtrInf" type="ReturnReasonInformation10" minOccurs="0"/> - <xs:element name="CorpActn" type="CorporateAction1" minOccurs="0"/> - <xs:element name="SfkpgAcct" type="CashAccount16" minOccurs="0"/> - <xs:element name="AddtlTxInf" type="Max500Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="ExternalAccountIdentification1Code"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="4"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ExternalBalanceSubType1Code"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="4"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ExternalBankTransactionDomain1Code"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="4"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ExternalBankTransactionFamily1Code"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="4"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ExternalBankTransactionSubFamily1Code"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="4"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ExternalClearingSystemIdentification1Code"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="5"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ExternalFinancialInstitutionIdentification1Code"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="4"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ExternalOrganisationIdentification1Code"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="4"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ExternalPersonIdentification1Code"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="4"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ExternalPurpose1Code"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="4"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ExternalReportingSource1Code"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="4"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ExternalReturnReason1Code"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="4"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ExternalTechnicalInputChannel1Code"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="4"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="FinancialIdentificationSchemeName1Choice"> - <xs:sequence> - <xs:choice> - <xs:element name="Cd" type="ExternalFinancialInstitutionIdentification1Code"/> - <xs:element name="Prtry" type="Max35Text"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:complexType name="FinancialInstitutionIdentification7"> - <xs:sequence> - <xs:element name="BIC" type="BICIdentifier" minOccurs="0"/> - <xs:element name="ClrSysMmbId" type="ClearingSystemMemberIdentification2" minOccurs="0"/> - <xs:element name="Nm" type="Max140Text" minOccurs="0"/> - <xs:element name="PstlAdr" type="PostalAddress6" minOccurs="0"/> - <xs:element name="Othr" type="GenericFinancialIdentification1" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="FinancialInstrumentQuantityChoice"> - <xs:sequence> - <xs:choice> - <xs:element name="Unit" type="DecimalNumber"/> - <xs:element name="FaceAmt" type="ImpliedCurrencyAndAmount"/> - <xs:element name="AmtsdVal" type="ImpliedCurrencyAndAmount"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:complexType name="FromToAmountRange"> - <xs:sequence> - <xs:element name="FrAmt" type="AmountRangeBoundary1"/> - <xs:element name="ToAmt" type="AmountRangeBoundary1"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="GenericAccountIdentification1"> - <xs:sequence> - <xs:element name="Id" type="Max34Text"/> - <xs:element name="SchmeNm" type="AccountSchemeName1Choice" minOccurs="0"/> - <xs:element name="Issr" type="Max35Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="GenericFinancialIdentification1"> - <xs:sequence> - <xs:element name="Id" type="Max35Text"/> - <xs:element name="SchmeNm" type="FinancialIdentificationSchemeName1Choice" minOccurs="0"/> - <xs:element name="Issr" type="Max35Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="GenericIdentification3"> - <xs:sequence> - <xs:element name="Id" type="Max35Text"/> - <xs:element name="Issr" type="Max35Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="GenericOrganisationIdentification1"> - <xs:sequence> - <xs:element name="Id" type="Max35Text"/> - <xs:element name="SchmeNm" type="OrganisationIdentificationSchemeName1Choice" minOccurs="0"/> - <xs:element name="Issr" type="Max35Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="GenericPersonIdentification1"> - <xs:sequence> - <xs:element name="Id" type="Max35Text"/> - <xs:element name="SchmeNm" type="PersonIdentificationSchemeName1Choice" minOccurs="0"/> - <xs:element name="Issr" type="Max35Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="GroupHeader42"> - <xs:sequence> - <xs:element name="MsgId" type="Max35Text"/> - <xs:element name="CreDtTm" type="ISODateTime"/> - <xs:element name="MsgRcpt" type="PartyIdentification32" minOccurs="0"/> - <xs:element name="MsgPgntn" type="Pagination" minOccurs="0"/> - <xs:element name="AddtlInf" type="Max500Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="IBAN2007Identifier"> - <xs:restriction base="xs:string"> - <xs:pattern value="[A-Z]{2,2}[0-9]{2,2}[a-zA-Z0-9]{1,30}"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ISINIdentifier"> - <xs:restriction base="xs:string"> - <xs:pattern value="[A-Z0-9]{12,12}"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ISODate"> - <xs:restriction base="xs:date"/> - </xs:simpleType> - <xs:simpleType name="ISODateTime"> - <xs:restriction base="xs:dateTime"/> - </xs:simpleType> - <xs:complexType name="ImpliedCurrencyAmountRangeChoice"> - <xs:sequence> - <xs:choice> - <xs:element name="FrAmt" type="AmountRangeBoundary1"/> - <xs:element name="ToAmt" type="AmountRangeBoundary1"/> - <xs:element name="FrToAmt" type="FromToAmountRange"/> - <xs:element name="EQAmt" type="ImpliedCurrencyAndAmount"/> - <xs:element name="NEQAmt" type="ImpliedCurrencyAndAmount"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="ImpliedCurrencyAndAmount"> - <xs:restriction base="xs:decimal"> - <xs:minInclusive value="0"/> - <xs:fractionDigits value="5"/> - <xs:totalDigits value="18"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="InterestType1Choice"> - <xs:sequence> - <xs:choice> - <xs:element name="Cd" type="InterestType1Code"/> - <xs:element name="Prtry" type="Max35Text"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="InterestType1Code"> - <xs:restriction base="xs:string"> - <xs:enumeration value="INDY"/> - <xs:enumeration value="OVRN"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="Max105Text"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="105"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="Max140Text"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="140"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="Max15NumericText"> - <xs:restriction base="xs:string"> - <xs:pattern value="[0-9]{1,15}"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="Max15PlusSignedNumericText"> - <xs:restriction base="xs:string"> - <xs:pattern value="[+]{0,1}[0-9]{1,15}"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="Max16Text"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="16"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="Max2048Text"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="2048"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="Max34Text"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="34"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="Max35Text"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="35"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="Max4Text"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="4"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="Max500Text"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="500"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="Max5NumericText"> - <xs:restriction base="xs:string"> - <xs:pattern value="[0-9]{1,5}"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="Max70Text"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="70"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="MessageIdentification2"> - <xs:sequence> - <xs:element name="MsgNmId" type="Max35Text" minOccurs="0"/> - <xs:element name="MsgId" type="Max35Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="NameAndAddress10"> - <xs:sequence> - <xs:element name="Nm" type="Max140Text"/> - <xs:element name="Adr" type="PostalAddress6"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="NamePrefix1Code"> - <xs:restriction base="xs:string"> - <xs:enumeration value="DOCT"/> - <xs:enumeration value="MIST"/> - <xs:enumeration value="MISS"/> - <xs:enumeration value="MADM"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="Number"> - <xs:restriction base="xs:decimal"> - <xs:fractionDigits value="0"/> - <xs:totalDigits value="18"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="NumberAndSumOfTransactions1"> - <xs:sequence> - <xs:element name="NbOfNtries" type="Max15NumericText" minOccurs="0"/> - <xs:element name="Sum" type="DecimalNumber" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="NumberAndSumOfTransactions2"> - <xs:sequence> - <xs:element name="NbOfNtries" type="Max15NumericText" minOccurs="0"/> - <xs:element name="Sum" type="DecimalNumber" minOccurs="0"/> - <xs:element name="TtlNetNtryAmt" type="DecimalNumber" minOccurs="0"/> - <xs:element name="CdtDbtInd" type="CreditDebitCode" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="OrganisationIdentification4"> - <xs:sequence> - <xs:element name="BICOrBEI" type="AnyBICIdentifier" minOccurs="0"/> - <xs:element name="Othr" type="GenericOrganisationIdentification1" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="OrganisationIdentificationSchemeName1Choice"> - <xs:sequence> - <xs:choice> - <xs:element name="Cd" type="ExternalOrganisationIdentification1Code"/> - <xs:element name="Prtry" type="Max35Text"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:complexType name="Pagination"> - <xs:sequence> - <xs:element name="PgNb" type="Max5NumericText"/> - <xs:element name="LastPgInd" type="YesNoIndicator"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="Party6Choice"> - <xs:sequence> - <xs:choice> - <xs:element name="OrgId" type="OrganisationIdentification4"/> - <xs:element name="PrvtId" type="PersonIdentification5"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:complexType name="PartyIdentification32"> - <xs:sequence> - <xs:element name="Nm" type="Max140Text" minOccurs="0"/> - <xs:element name="PstlAdr" type="PostalAddress6" minOccurs="0"/> - <xs:element name="Id" type="Party6Choice" minOccurs="0"/> - <xs:element name="CtryOfRes" type="CountryCode" minOccurs="0"/> - <xs:element name="CtctDtls" type="ContactDetails2" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="PercentageRate"> - <xs:restriction base="xs:decimal"> - <xs:fractionDigits value="10"/> - <xs:totalDigits value="11"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="PersonIdentification5"> - <xs:sequence> - <xs:element name="DtAndPlcOfBirth" type="DateAndPlaceOfBirth" minOccurs="0"/> - <xs:element name="Othr" type="GenericPersonIdentification1" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="PersonIdentificationSchemeName1Choice"> - <xs:sequence> - <xs:choice> - <xs:element name="Cd" type="ExternalPersonIdentification1Code"/> - <xs:element name="Prtry" type="Max35Text"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="PhoneNumber"> - <xs:restriction base="xs:string"> - <xs:pattern value="\+[0-9]{1,3}-[0-9()+\-]{1,30}"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="PostalAddress6"> - <xs:sequence> - <xs:element name="AdrTp" type="AddressType2Code" minOccurs="0"/> - <xs:element name="Dept" type="Max70Text" minOccurs="0"/> - <xs:element name="SubDept" type="Max70Text" minOccurs="0"/> - <xs:element name="StrtNm" type="Max70Text" minOccurs="0"/> - <xs:element name="BldgNb" type="Max16Text" minOccurs="0"/> - <xs:element name="PstCd" type="Max16Text" minOccurs="0"/> - <xs:element name="TwnNm" type="Max35Text" minOccurs="0"/> - <xs:element name="CtrySubDvsn" type="Max35Text" minOccurs="0"/> - <xs:element name="Ctry" type="CountryCode" minOccurs="0"/> - <xs:element name="AdrLine" type="Max70Text" minOccurs="0" maxOccurs="7"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ProprietaryAgent2"> - <xs:sequence> - <xs:element name="Tp" type="Max35Text"/> - <xs:element name="Agt" type="BranchAndFinancialInstitutionIdentification4"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ProprietaryBankTransactionCodeStructure1"> - <xs:sequence> - <xs:element name="Cd" type="Max35Text"/> - <xs:element name="Issr" type="Max35Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ProprietaryDate2"> - <xs:sequence> - <xs:element name="Tp" type="Max35Text"/> - <xs:element name="Dt" type="DateAndDateTimeChoice"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ProprietaryParty2"> - <xs:sequence> - <xs:element name="Tp" type="Max35Text"/> - <xs:element name="Pty" type="PartyIdentification32"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ProprietaryPrice2"> - <xs:sequence> - <xs:element name="Tp" type="Max35Text"/> - <xs:element name="Pric" type="ActiveOrHistoricCurrencyAndAmount"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ProprietaryQuantity1"> - <xs:sequence> - <xs:element name="Tp" type="Max35Text"/> - <xs:element name="Qty" type="Max35Text"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ProprietaryReference1"> - <xs:sequence> - <xs:element name="Tp" type="Max35Text"/> - <xs:element name="Ref" type="Max35Text"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="Purpose2Choice"> - <xs:sequence> - <xs:choice> - <xs:element name="Cd" type="ExternalPurpose1Code"/> - <xs:element name="Prtry" type="Max35Text"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:complexType name="Rate3"> - <xs:sequence> - <xs:element name="Tp" type="RateType4Choice"/> - <xs:element name="VldtyRg" type="CurrencyAndAmountRange2" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="RateType4Choice"> - <xs:sequence> - <xs:choice> - <xs:element name="Pctg" type="PercentageRate"/> - <xs:element name="Othr" type="Max35Text"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ReferredDocumentInformation3"> - <xs:sequence> - <xs:element name="Tp" type="ReferredDocumentType2" minOccurs="0"/> - <xs:element name="Nb" type="Max35Text" minOccurs="0"/> - <xs:element name="RltdDt" type="ISODate" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ReferredDocumentType1Choice"> - <xs:sequence> - <xs:choice> - <xs:element name="Cd" type="DocumentType5Code"/> - <xs:element name="Prtry" type="Max35Text"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ReferredDocumentType2"> - <xs:sequence> - <xs:element name="CdOrPrtry" type="ReferredDocumentType1Choice"/> - <xs:element name="Issr" type="Max35Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="RemittanceAmount1"> - <xs:sequence> - <xs:element name="DuePyblAmt" type="ActiveOrHistoricCurrencyAndAmount" minOccurs="0"/> - <xs:element name="DscntApldAmt" type="ActiveOrHistoricCurrencyAndAmount" minOccurs="0"/> - <xs:element name="CdtNoteAmt" type="ActiveOrHistoricCurrencyAndAmount" minOccurs="0"/> - <xs:element name="TaxAmt" type="ActiveOrHistoricCurrencyAndAmount" minOccurs="0"/> - <xs:element name="AdjstmntAmtAndRsn" type="DocumentAdjustment1" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="RmtdAmt" type="ActiveOrHistoricCurrencyAndAmount" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="RemittanceInformation5"> - <xs:sequence> - <xs:element name="Ustrd" type="Max140Text" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="Strd" type="StructuredRemittanceInformation7" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="RemittanceLocation2"> - <xs:sequence> - <xs:element name="RmtId" type="Max35Text" minOccurs="0"/> - <xs:element name="RmtLctnMtd" type="RemittanceLocationMethod2Code" minOccurs="0"/> - <xs:element name="RmtLctnElctrncAdr" type="Max2048Text" minOccurs="0"/> - <xs:element name="RmtLctnPstlAdr" type="NameAndAddress10" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="RemittanceLocationMethod2Code"> - <xs:restriction base="xs:string"> - <xs:enumeration value="FAXI"/> - <xs:enumeration value="EDIC"/> - <xs:enumeration value="URID"/> - <xs:enumeration value="EMAL"/> - <xs:enumeration value="POST"/> - <xs:enumeration value="SMSM"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="ReportEntry2"> - <xs:sequence> - <xs:element name="NtryRef" type="Max35Text" minOccurs="0"/> - <xs:element name="Amt" type="ActiveOrHistoricCurrencyAndAmount"/> - <xs:element name="CdtDbtInd" type="CreditDebitCode"/> - <xs:element name="RvslInd" type="TrueFalseIndicator" minOccurs="0"/> - <xs:element name="Sts" type="EntryStatus2Code"/> - <xs:element name="BookgDt" type="DateAndDateTimeChoice" minOccurs="0"/> - <xs:element name="ValDt" type="DateAndDateTimeChoice" minOccurs="0"/> - <xs:element name="AcctSvcrRef" type="Max35Text" minOccurs="0"/> - <xs:element name="Avlbty" type="CashBalanceAvailability2" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="BkTxCd" type="BankTransactionCodeStructure4"/> - <xs:element name="ComssnWvrInd" type="YesNoIndicator" minOccurs="0"/> - <xs:element name="AddtlInfInd" type="MessageIdentification2" minOccurs="0"/> - <xs:element name="AmtDtls" type="AmountAndCurrencyExchange3" minOccurs="0"/> - <xs:element name="Chrgs" type="ChargesInformation6" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="TechInptChanl" type="TechnicalInputChannel1Choice" minOccurs="0"/> - <xs:element name="Intrst" type="TransactionInterest2" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="NtryDtls" type="EntryDetails1" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="AddtlNtryInf" type="Max500Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ReportingSource1Choice"> - <xs:sequence> - <xs:choice> - <xs:element name="Cd" type="ExternalReportingSource1Code"/> - <xs:element name="Prtry" type="Max35Text"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ReturnReason5Choice"> - <xs:sequence> - <xs:choice> - <xs:element name="Cd" type="ExternalReturnReason1Code"/> - <xs:element name="Prtry" type="Max35Text"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ReturnReasonInformation10"> - <xs:sequence> - <xs:element name="OrgnlBkTxCd" type="BankTransactionCodeStructure4" minOccurs="0"/> - <xs:element name="Orgtr" type="PartyIdentification32" minOccurs="0"/> - <xs:element name="Rsn" type="ReturnReason5Choice" minOccurs="0"/> - <xs:element name="AddtlInf" type="Max105Text" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="SecurityIdentification4Choice"> - <xs:sequence> - <xs:choice> - <xs:element name="ISIN" type="ISINIdentifier"/> - <xs:element name="Prtry" type="AlternateSecurityIdentification2"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:complexType name="StructuredRemittanceInformation7"> - <xs:sequence> - <xs:element name="RfrdDocInf" type="ReferredDocumentInformation3" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="RfrdDocAmt" type="RemittanceAmount1" minOccurs="0"/> - <xs:element name="CdtrRefInf" type="CreditorReferenceInformation2" minOccurs="0"/> - <xs:element name="Invcr" type="PartyIdentification32" minOccurs="0"/> - <xs:element name="Invcee" type="PartyIdentification32" minOccurs="0"/> - <xs:element name="AddtlRmtInf" type="Max140Text" minOccurs="0" maxOccurs="3"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TaxAmount1"> - <xs:sequence> - <xs:element name="Rate" type="PercentageRate" minOccurs="0"/> - <xs:element name="TaxblBaseAmt" type="ActiveOrHistoricCurrencyAndAmount" minOccurs="0"/> - <xs:element name="TtlAmt" type="ActiveOrHistoricCurrencyAndAmount" minOccurs="0"/> - <xs:element name="Dtls" type="TaxRecordDetails1" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TaxAuthorisation1"> - <xs:sequence> - <xs:element name="Titl" type="Max35Text" minOccurs="0"/> - <xs:element name="Nm" type="Max140Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TaxCharges2"> - <xs:sequence> - <xs:element name="Id" type="Max35Text" minOccurs="0"/> - <xs:element name="Rate" type="PercentageRate" minOccurs="0"/> - <xs:element name="Amt" type="ActiveOrHistoricCurrencyAndAmount" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TaxInformation3"> - <xs:sequence> - <xs:element name="Cdtr" type="TaxParty1" minOccurs="0"/> - <xs:element name="Dbtr" type="TaxParty2" minOccurs="0"/> - <xs:element name="AdmstnZn" type="Max35Text" minOccurs="0"/> - <xs:element name="RefNb" type="Max140Text" minOccurs="0"/> - <xs:element name="Mtd" type="Max35Text" minOccurs="0"/> - <xs:element name="TtlTaxblBaseAmt" type="ActiveOrHistoricCurrencyAndAmount" minOccurs="0"/> - <xs:element name="TtlTaxAmt" type="ActiveOrHistoricCurrencyAndAmount" minOccurs="0"/> - <xs:element name="Dt" type="ISODate" minOccurs="0"/> - <xs:element name="SeqNb" type="Number" minOccurs="0"/> - <xs:element name="Rcrd" type="TaxRecord1" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TaxParty1"> - <xs:sequence> - <xs:element name="TaxId" type="Max35Text" minOccurs="0"/> - <xs:element name="RegnId" type="Max35Text" minOccurs="0"/> - <xs:element name="TaxTp" type="Max35Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TaxParty2"> - <xs:sequence> - <xs:element name="TaxId" type="Max35Text" minOccurs="0"/> - <xs:element name="RegnId" type="Max35Text" minOccurs="0"/> - <xs:element name="TaxTp" type="Max35Text" minOccurs="0"/> - <xs:element name="Authstn" type="TaxAuthorisation1" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TaxPeriod1"> - <xs:sequence> - <xs:element name="Yr" type="ISODate" minOccurs="0"/> - <xs:element name="Tp" type="TaxRecordPeriod1Code" minOccurs="0"/> - <xs:element name="FrToDt" type="DatePeriodDetails" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TaxRecord1"> - <xs:sequence> - <xs:element name="Tp" type="Max35Text" minOccurs="0"/> - <xs:element name="Ctgy" type="Max35Text" minOccurs="0"/> - <xs:element name="CtgyDtls" type="Max35Text" minOccurs="0"/> - <xs:element name="DbtrSts" type="Max35Text" minOccurs="0"/> - <xs:element name="CertId" type="Max35Text" minOccurs="0"/> - <xs:element name="FrmsCd" type="Max35Text" minOccurs="0"/> - <xs:element name="Prd" type="TaxPeriod1" minOccurs="0"/> - <xs:element name="TaxAmt" type="TaxAmount1" minOccurs="0"/> - <xs:element name="AddtlInf" type="Max140Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TaxRecordDetails1"> - <xs:sequence> - <xs:element name="Prd" type="TaxPeriod1" minOccurs="0"/> - <xs:element name="Amt" type="ActiveOrHistoricCurrencyAndAmount"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="TaxRecordPeriod1Code"> - <xs:restriction base="xs:string"> - <xs:enumeration value="MM01"/> - <xs:enumeration value="MM02"/> - <xs:enumeration value="MM03"/> - <xs:enumeration value="MM04"/> - <xs:enumeration value="MM05"/> - <xs:enumeration value="MM06"/> - <xs:enumeration value="MM07"/> - <xs:enumeration value="MM08"/> - <xs:enumeration value="MM09"/> - <xs:enumeration value="MM10"/> - <xs:enumeration value="MM11"/> - <xs:enumeration value="MM12"/> - <xs:enumeration value="QTR1"/> - <xs:enumeration value="QTR2"/> - <xs:enumeration value="QTR3"/> - <xs:enumeration value="QTR4"/> - <xs:enumeration value="HLF1"/> - <xs:enumeration value="HLF2"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="TechnicalInputChannel1Choice"> - <xs:sequence> - <xs:choice> - <xs:element name="Cd" type="ExternalTechnicalInputChannel1Code"/> - <xs:element name="Prtry" type="Max35Text"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TotalTransactions2"> - <xs:sequence> - <xs:element name="TtlNtries" type="NumberAndSumOfTransactions2" minOccurs="0"/> - <xs:element name="TtlCdtNtries" type="NumberAndSumOfTransactions1" minOccurs="0"/> - <xs:element name="TtlDbtNtries" type="NumberAndSumOfTransactions1" minOccurs="0"/> - <xs:element name="TtlNtriesPerBkTxCd" type="TotalsPerBankTransactionCode2" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TotalsPerBankTransactionCode2"> - <xs:sequence> - <xs:element name="NbOfNtries" type="Max15NumericText" minOccurs="0"/> - <xs:element name="Sum" type="DecimalNumber" minOccurs="0"/> - <xs:element name="TtlNetNtryAmt" type="DecimalNumber" minOccurs="0"/> - <xs:element name="CdtDbtInd" type="CreditDebitCode" minOccurs="0"/> - <xs:element name="FcstInd" type="TrueFalseIndicator" minOccurs="0"/> - <xs:element name="BkTxCd" type="BankTransactionCodeStructure4"/> - <xs:element name="Avlbty" type="CashBalanceAvailability2" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TransactionAgents2"> - <xs:sequence> - <xs:element name="DbtrAgt" type="BranchAndFinancialInstitutionIdentification4" minOccurs="0"/> - <xs:element name="CdtrAgt" type="BranchAndFinancialInstitutionIdentification4" minOccurs="0"/> - <xs:element name="IntrmyAgt1" type="BranchAndFinancialInstitutionIdentification4" minOccurs="0"/> - <xs:element name="IntrmyAgt2" type="BranchAndFinancialInstitutionIdentification4" minOccurs="0"/> - <xs:element name="IntrmyAgt3" type="BranchAndFinancialInstitutionIdentification4" minOccurs="0"/> - <xs:element name="RcvgAgt" type="BranchAndFinancialInstitutionIdentification4" minOccurs="0"/> - <xs:element name="DlvrgAgt" type="BranchAndFinancialInstitutionIdentification4" minOccurs="0"/> - <xs:element name="IssgAgt" type="BranchAndFinancialInstitutionIdentification4" minOccurs="0"/> - <xs:element name="SttlmPlc" type="BranchAndFinancialInstitutionIdentification4" minOccurs="0"/> - <xs:element name="Prtry" type="ProprietaryAgent2" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TransactionDates2"> - <xs:sequence> - <xs:element name="AccptncDtTm" type="ISODateTime" minOccurs="0"/> - <xs:element name="TradActvtyCtrctlSttlmDt" type="ISODate" minOccurs="0"/> - <xs:element name="TradDt" type="ISODate" minOccurs="0"/> - <xs:element name="IntrBkSttlmDt" type="ISODate" minOccurs="0"/> - <xs:element name="StartDt" type="ISODate" minOccurs="0"/> - <xs:element name="EndDt" type="ISODate" minOccurs="0"/> - <xs:element name="TxDtTm" type="ISODateTime" minOccurs="0"/> - <xs:element name="Prtry" type="ProprietaryDate2" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TransactionInterest2"> - <xs:sequence> - <xs:element name="Amt" type="ActiveOrHistoricCurrencyAndAmount"/> - <xs:element name="CdtDbtInd" type="CreditDebitCode"/> - <xs:element name="Tp" type="InterestType1Choice" minOccurs="0"/> - <xs:element name="Rate" type="Rate3" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="FrToDt" type="DateTimePeriodDetails" minOccurs="0"/> - <xs:element name="Rsn" type="Max35Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TransactionParty2"> - <xs:sequence> - <xs:element name="InitgPty" type="PartyIdentification32" minOccurs="0"/> - <xs:element name="Dbtr" type="PartyIdentification32" minOccurs="0"/> - <xs:element name="DbtrAcct" type="CashAccount16" minOccurs="0"/> - <xs:element name="UltmtDbtr" type="PartyIdentification32" minOccurs="0"/> - <xs:element name="Cdtr" type="PartyIdentification32" minOccurs="0"/> - <xs:element name="CdtrAcct" type="CashAccount16" minOccurs="0"/> - <xs:element name="UltmtCdtr" type="PartyIdentification32" minOccurs="0"/> - <xs:element name="TradgPty" type="PartyIdentification32" minOccurs="0"/> - <xs:element name="Prtry" type="ProprietaryParty2" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TransactionPrice2Choice"> - <xs:sequence> - <xs:choice> - <xs:element name="DealPric" type="ActiveOrHistoricCurrencyAndAmount"/> - <xs:element name="Prtry" type="ProprietaryPrice2" maxOccurs="unbounded"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TransactionQuantities1Choice"> - <xs:sequence> - <xs:choice> - <xs:element name="Qty" type="FinancialInstrumentQuantityChoice"/> - <xs:element name="Prtry" type="ProprietaryQuantity1"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TransactionReferences2"> - <xs:sequence> - <xs:element name="MsgId" type="Max35Text" minOccurs="0"/> - <xs:element name="AcctSvcrRef" type="Max35Text" minOccurs="0"/> - <xs:element name="PmtInfId" type="Max35Text" minOccurs="0"/> - <xs:element name="InstrId" type="Max35Text" minOccurs="0"/> - <xs:element name="EndToEndId" type="Max35Text" minOccurs="0"/> - <xs:element name="TxId" type="Max35Text" minOccurs="0"/> - <xs:element name="MndtId" type="Max35Text" minOccurs="0"/> - <xs:element name="ChqNb" type="Max35Text" minOccurs="0"/> - <xs:element name="ClrSysRef" type="Max35Text" minOccurs="0"/> - <xs:element name="Prtry" type="ProprietaryReference1" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="TrueFalseIndicator"> - <xs:restriction base="xs:boolean"/> - </xs:simpleType> - <xs:simpleType name="YesNoIndicator"> - <xs:restriction base="xs:boolean"/> - </xs:simpleType> -</xs:schema> diff --git a/libeufin-common/src/main/resources/xsd/camt.053.001.02.xsd b/libeufin-common/src/main/resources/xsd/camt.053.001.02.xsd @@ -1,1299 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<!-- Mit XMLSpy v2008 rel. 2 (http://www.altova.com) von Wenzel (SIZ Bonn) bearbeitet --> -<!--Generated by SWIFTStandards Workstation (build:R6.1.0.2) on 2009 Jan 08 17:30:53--> -<xs:schema xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02" xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02" elementFormDefault="qualified"> - <xs:element name="Document" type="Document"/> - <xs:complexType name="AccountIdentification4Choice"> - <xs:sequence> - <xs:choice> - <xs:element name="IBAN" type="IBAN2007Identifier"/> - <xs:element name="Othr" type="GenericAccountIdentification1"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:complexType name="AccountInterest2"> - <xs:sequence> - <xs:element name="Tp" type="InterestType1Choice" minOccurs="0"/> - <xs:element name="Rate" type="Rate3" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="FrToDt" type="DateTimePeriodDetails" minOccurs="0"/> - <xs:element name="Rsn" type="Max35Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="AccountSchemeName1Choice"> - <xs:sequence> - <xs:choice> - <xs:element name="Cd" type="ExternalAccountIdentification1Code"/> - <xs:element name="Prtry" type="Max35Text"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:complexType name="AccountStatement2"> - <xs:sequence> - <xs:element name="Id" type="Max35Text"/> - <xs:element name="ElctrncSeqNb" type="Number" minOccurs="0"/> - <xs:element name="LglSeqNb" type="Number" minOccurs="0"/> - <xs:element name="CreDtTm" type="ISODateTime"/> - <xs:element name="FrToDt" type="DateTimePeriodDetails" minOccurs="0"/> - <xs:element name="CpyDplctInd" type="CopyDuplicate1Code" minOccurs="0"/> - <xs:element name="RptgSrc" type="ReportingSource1Choice" minOccurs="0"/> - <xs:element name="Acct" type="CashAccount20"/> - <xs:element name="RltdAcct" type="CashAccount16" minOccurs="0"/> - <xs:element name="Intrst" type="AccountInterest2" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="Bal" type="CashBalance3" maxOccurs="unbounded"/> - <xs:element name="TxsSummry" type="TotalTransactions2" minOccurs="0"/> - <xs:element name="Ntry" type="ReportEntry2" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="AddtlStmtInf" type="Max500Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="ActiveOrHistoricCurrencyAndAmount_SimpleType"> - <xs:restriction base="xs:decimal"> - <xs:minInclusive value="0"/> - <xs:fractionDigits value="5"/> - <xs:totalDigits value="18"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="ActiveOrHistoricCurrencyAndAmount"> - <xs:simpleContent> - <xs:extension base="ActiveOrHistoricCurrencyAndAmount_SimpleType"> - <xs:attribute name="Ccy" type="ActiveOrHistoricCurrencyCode" use="required"/> - </xs:extension> - </xs:simpleContent> - </xs:complexType> - <xs:simpleType name="ActiveOrHistoricCurrencyCode"> - <xs:restriction base="xs:string"> - <xs:pattern value="[A-Z]{3,13}"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="AddressType2Code"> - <xs:restriction base="xs:string"> - <xs:enumeration value="ADDR"/> - <xs:enumeration value="PBOX"/> - <xs:enumeration value="HOME"/> - <xs:enumeration value="BIZZ"/> - <xs:enumeration value="MLTO"/> - <xs:enumeration value="DLVY"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="AlternateSecurityIdentification2"> - <xs:sequence> - <xs:element name="Tp" type="Max35Text"/> - <xs:element name="Id" type="Max35Text"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="AmountAndCurrencyExchange3"> - <xs:sequence> - <xs:element name="InstdAmt" type="AmountAndCurrencyExchangeDetails3" minOccurs="0"/> - <xs:element name="TxAmt" type="AmountAndCurrencyExchangeDetails3" minOccurs="0"/> - <xs:element name="CntrValAmt" type="AmountAndCurrencyExchangeDetails3" minOccurs="0"/> - <xs:element name="AnncdPstngAmt" type="AmountAndCurrencyExchangeDetails3" minOccurs="0"/> - <xs:element name="PrtryAmt" type="AmountAndCurrencyExchangeDetails4" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="AmountAndCurrencyExchangeDetails3"> - <xs:sequence> - <xs:element name="Amt" type="ActiveOrHistoricCurrencyAndAmount"/> - <xs:element name="CcyXchg" type="CurrencyExchange5" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="AmountAndCurrencyExchangeDetails4"> - <xs:sequence> - <xs:element name="Tp" type="Max35Text"/> - <xs:element name="Amt" type="ActiveOrHistoricCurrencyAndAmount"/> - <xs:element name="CcyXchg" type="CurrencyExchange5" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="AmountRangeBoundary1"> - <xs:sequence> - <xs:element name="BdryAmt" type="ImpliedCurrencyAndAmount"/> - <xs:element name="Incl" type="YesNoIndicator"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="AnyBICIdentifier"> - <xs:restriction base="xs:string"> - <xs:pattern value="[A-Z]{6,6}[A-Z2-9][A-NP-Z0-9]([A-Z0-9]{3,3}){0,1}"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="BICIdentifier"> - <xs:restriction base="xs:string"> - <xs:pattern value="[A-Z]{6,6}[A-Z2-9][A-NP-Z0-9]([A-Z0-9]{3,3}){0,1}"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="BalanceSubType1Choice"> - <xs:sequence> - <xs:choice> - <xs:element name="Cd" type="ExternalBalanceSubType1Code"/> - <xs:element name="Prtry" type="Max35Text"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:complexType name="BalanceType12"> - <xs:sequence> - <xs:element name="CdOrPrtry" type="BalanceType5Choice"/> - <xs:element name="SubTp" type="BalanceSubType1Choice" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="BalanceType12Code"> - <xs:restriction base="xs:string"> - <xs:enumeration value="XPCD"/> - <xs:enumeration value="OPAV"/> - <xs:enumeration value="ITAV"/> - <xs:enumeration value="CLAV"/> - <xs:enumeration value="FWAV"/> - <xs:enumeration value="CLBD"/> - <xs:enumeration value="ITBD"/> - <xs:enumeration value="OPBD"/> - <xs:enumeration value="PRCD"/> - <xs:enumeration value="INFO"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="BalanceType5Choice"> - <xs:sequence> - <xs:choice> - <xs:element name="Cd" type="BalanceType12Code"/> - <xs:element name="Prtry" type="Max35Text"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:complexType name="BankToCustomerStatementV02"> - <xs:sequence> - <xs:element name="GrpHdr" type="GroupHeader42"/> - <xs:element name="Stmt" type="AccountStatement2" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="BankTransactionCodeStructure4"> - <xs:sequence> - <xs:element name="Domn" type="BankTransactionCodeStructure5" minOccurs="0"/> - <xs:element name="Prtry" type="ProprietaryBankTransactionCodeStructure1" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="BankTransactionCodeStructure5"> - <xs:sequence> - <xs:element name="Cd" type="ExternalBankTransactionDomain1Code"/> - <xs:element name="Fmly" type="BankTransactionCodeStructure6"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="BankTransactionCodeStructure6"> - <xs:sequence> - <xs:element name="Cd" type="ExternalBankTransactionFamily1Code"/> - <xs:element name="SubFmlyCd" type="ExternalBankTransactionSubFamily1Code"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="BaseOneRate"> - <xs:restriction base="xs:decimal"> - <xs:fractionDigits value="10"/> - <xs:totalDigits value="11"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="BatchInformation2"> - <xs:sequence> - <xs:element name="MsgId" type="Max35Text" minOccurs="0"/> - <xs:element name="PmtInfId" type="Max35Text" minOccurs="0"/> - <xs:element name="NbOfTxs" type="Max15NumericText" minOccurs="0"/> - <xs:element name="TtlAmt" type="ActiveOrHistoricCurrencyAndAmount" minOccurs="0"/> - <xs:element name="CdtDbtInd" type="CreditDebitCode" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="BranchAndFinancialInstitutionIdentification4"> - <xs:sequence> - <xs:element name="FinInstnId" type="FinancialInstitutionIdentification7"/> - <xs:element name="BrnchId" type="BranchData2" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="BranchData2"> - <xs:sequence> - <xs:element name="Id" type="Max35Text" minOccurs="0"/> - <xs:element name="Nm" type="Max140Text" minOccurs="0"/> - <xs:element name="PstlAdr" type="PostalAddress6" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CashAccount16"> - <xs:sequence> - <xs:element name="Id" type="AccountIdentification4Choice"/> - <xs:element name="Tp" type="CashAccountType2" minOccurs="0"/> - <xs:element name="Ccy" type="ActiveOrHistoricCurrencyCode" minOccurs="0"/> - <xs:element name="Nm" type="Max70Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CashAccount20"> - <xs:sequence> - <xs:element name="Id" type="AccountIdentification4Choice"/> - <xs:element name="Tp" type="CashAccountType2" minOccurs="0"/> - <xs:element name="Ccy" type="ActiveOrHistoricCurrencyCode" minOccurs="0"/> - <xs:element name="Nm" type="Max70Text" minOccurs="0"/> - <xs:element name="Ownr" type="PartyIdentification32" minOccurs="0"/> - <xs:element name="Svcr" type="BranchAndFinancialInstitutionIdentification4" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CashAccountType2"> - <xs:sequence> - <xs:choice> - <xs:element name="Cd" type="CashAccountType4Code"/> - <xs:element name="Prtry" type="Max35Text"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="CashAccountType4Code"> - <xs:restriction base="xs:string"> - <xs:enumeration value="CASH"/> - <xs:enumeration value="CHAR"/> - <xs:enumeration value="COMM"/> - <xs:enumeration value="TAXE"/> - <xs:enumeration value="CISH"/> - <xs:enumeration value="TRAS"/> - <xs:enumeration value="SACC"/> - <xs:enumeration value="CACC"/> - <xs:enumeration value="SVGS"/> - <xs:enumeration value="ONDP"/> - <xs:enumeration value="MGLD"/> - <xs:enumeration value="NREX"/> - <xs:enumeration value="MOMA"/> - <xs:enumeration value="LOAN"/> - <xs:enumeration value="SLRY"/> - <xs:enumeration value="ODFT"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="CashBalance3"> - <xs:sequence> - <xs:element name="Tp" type="BalanceType12"/> - <xs:element name="CdtLine" type="CreditLine2" minOccurs="0"/> - <xs:element name="Amt" type="ActiveOrHistoricCurrencyAndAmount"/> - <xs:element name="CdtDbtInd" type="CreditDebitCode"/> - <xs:element name="Dt" type="DateAndDateTimeChoice"/> - <xs:element name="Avlbty" type="CashBalanceAvailability2" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CashBalanceAvailability2"> - <xs:sequence> - <xs:element name="Dt" type="CashBalanceAvailabilityDate1"/> - <xs:element name="Amt" type="ActiveOrHistoricCurrencyAndAmount"/> - <xs:element name="CdtDbtInd" type="CreditDebitCode"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CashBalanceAvailabilityDate1"> - <xs:sequence> - <xs:choice> - <xs:element name="NbOfDays" type="Max15PlusSignedNumericText"/> - <xs:element name="ActlDt" type="ISODate"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="ChargeBearerType1Code"> - <xs:restriction base="xs:string"> - <xs:enumeration value="DEBT"/> - <xs:enumeration value="CRED"/> - <xs:enumeration value="SHAR"/> - <xs:enumeration value="SLEV"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ChargeType1Code"> - <xs:restriction base="xs:string"> - <xs:enumeration value="BRKF"/> - <xs:enumeration value="COMM"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="ChargeType2Choice"> - <xs:sequence> - <xs:choice> - <xs:element name="Cd" type="ChargeType1Code"/> - <xs:element name="Prtry" type="GenericIdentification3"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ChargesInformation6"> - <xs:sequence> - <xs:element name="TtlChrgsAndTaxAmt" type="ActiveOrHistoricCurrencyAndAmount" minOccurs="0"/> - <xs:element name="Amt" type="ActiveOrHistoricCurrencyAndAmount"/> - <xs:element name="CdtDbtInd" type="CreditDebitCode" minOccurs="0"/> - <xs:element name="Tp" type="ChargeType2Choice" minOccurs="0"/> - <xs:element name="Rate" type="PercentageRate" minOccurs="0"/> - <xs:element name="Br" type="ChargeBearerType1Code" minOccurs="0"/> - <xs:element name="Pty" type="BranchAndFinancialInstitutionIdentification4" minOccurs="0"/> - <xs:element name="Tax" type="TaxCharges2" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ClearingSystemIdentification2Choice"> - <xs:sequence> - <xs:choice> - <xs:element name="Cd" type="ExternalClearingSystemIdentification1Code"/> - <xs:element name="Prtry" type="Max35Text"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ClearingSystemMemberIdentification2"> - <xs:sequence> - <xs:element name="ClrSysId" type="ClearingSystemIdentification2Choice" minOccurs="0"/> - <xs:element name="MmbId" type="Max35Text"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ContactDetails2"> - <xs:sequence> - <xs:element name="NmPrfx" type="NamePrefix1Code" minOccurs="0"/> - <xs:element name="Nm" type="Max140Text" minOccurs="0"/> - <xs:element name="PhneNb" type="PhoneNumber" minOccurs="0"/> - <xs:element name="MobNb" type="PhoneNumber" minOccurs="0"/> - <xs:element name="FaxNb" type="PhoneNumber" minOccurs="0"/> - <xs:element name="EmailAdr" type="Max2048Text" minOccurs="0"/> - <xs:element name="Othr" type="Max35Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="CopyDuplicate1Code"> - <xs:restriction base="xs:string"> - <xs:enumeration value="CODU"/> - <xs:enumeration value="COPY"/> - <xs:enumeration value="DUPL"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="CorporateAction1"> - <xs:sequence> - <xs:element name="Cd" type="Max35Text" minOccurs="0"/> - <xs:element name="Nb" type="Max35Text" minOccurs="0"/> - <xs:element name="Prtry" type="Max35Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="CountryCode"> - <xs:restriction base="xs:string"> - <xs:pattern value="[A-Z]{2,2}"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="CreditDebitCode"> - <xs:restriction base="xs:string"> - <xs:enumeration value="CRDT"/> - <xs:enumeration value="DBIT"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="CreditLine2"> - <xs:sequence> - <xs:element name="Incl" type="TrueFalseIndicator"/> - <xs:element name="Amt" type="ActiveOrHistoricCurrencyAndAmount" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CreditorReferenceInformation2"> - <xs:sequence> - <xs:element name="Tp" type="CreditorReferenceType2" minOccurs="0"/> - <xs:element name="Ref" type="Max35Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CreditorReferenceType1Choice"> - <xs:sequence> - <xs:choice> - <xs:element name="Cd" type="DocumentType3Code"/> - <xs:element name="Prtry" type="Max35Text"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CreditorReferenceType2"> - <xs:sequence> - <xs:element name="CdOrPrtry" type="CreditorReferenceType1Choice"/> - <xs:element name="Issr" type="Max35Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CurrencyAndAmountRange2"> - <xs:sequence> - <xs:element name="Amt" type="ImpliedCurrencyAmountRangeChoice"/> - <xs:element name="CdtDbtInd" type="CreditDebitCode" minOccurs="0"/> - <xs:element name="Ccy" type="ActiveOrHistoricCurrencyCode"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CurrencyExchange5"> - <xs:sequence> - <xs:element name="SrcCcy" type="ActiveOrHistoricCurrencyCode"/> - <xs:element name="TrgtCcy" type="ActiveOrHistoricCurrencyCode" minOccurs="0"/> - <xs:element name="UnitCcy" type="ActiveOrHistoricCurrencyCode" minOccurs="0"/> - <xs:element name="XchgRate" type="BaseOneRate"/> - <xs:element name="CtrctId" type="Max35Text" minOccurs="0"/> - <xs:element name="QtnDt" type="ISODateTime" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="DateAndDateTimeChoice"> - <xs:sequence> - <xs:choice> - <xs:element name="Dt" type="ISODate"/> - <xs:element name="DtTm" type="ISODateTime"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:complexType name="DateAndPlaceOfBirth"> - <xs:sequence> - <xs:element name="BirthDt" type="ISODate"/> - <xs:element name="PrvcOfBirth" type="Max35Text" minOccurs="0"/> - <xs:element name="CityOfBirth" type="Max35Text"/> - <xs:element name="CtryOfBirth" type="CountryCode"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="DatePeriodDetails"> - <xs:sequence> - <xs:element name="FrDt" type="ISODate"/> - <xs:element name="ToDt" type="ISODate"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="DateTimePeriodDetails"> - <xs:sequence> - <xs:element name="FrDtTm" type="ISODateTime"/> - <xs:element name="ToDtTm" type="ISODateTime"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="DecimalNumber"> - <xs:restriction base="xs:decimal"> - <xs:fractionDigits value="17"/> - <xs:totalDigits value="18"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="Document"> - <xs:sequence> - <xs:element name="BkToCstmrStmt" type="BankToCustomerStatementV02"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="DocumentAdjustment1"> - <xs:sequence> - <xs:element name="Amt" type="ActiveOrHistoricCurrencyAndAmount"/> - <xs:element name="CdtDbtInd" type="CreditDebitCode" minOccurs="0"/> - <xs:element name="Rsn" type="Max4Text" minOccurs="0"/> - <xs:element name="AddtlInf" type="Max140Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="DocumentType3Code"> - <xs:restriction base="xs:string"> - <xs:enumeration value="RADM"/> - <xs:enumeration value="RPIN"/> - <xs:enumeration value="FXDR"/> - <xs:enumeration value="DISP"/> - <xs:enumeration value="PUOR"/> - <xs:enumeration value="SCOR"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="DocumentType5Code"> - <xs:restriction base="xs:string"> - <xs:enumeration value="MSIN"/> - <xs:enumeration value="CNFA"/> - <xs:enumeration value="DNFA"/> - <xs:enumeration value="CINV"/> - <xs:enumeration value="CREN"/> - <xs:enumeration value="DEBN"/> - <xs:enumeration value="HIRI"/> - <xs:enumeration value="SBIN"/> - <xs:enumeration value="CMCN"/> - <xs:enumeration value="SOAC"/> - <xs:enumeration value="DISP"/> - <xs:enumeration value="BOLD"/> - <xs:enumeration value="VCHR"/> - <xs:enumeration value="AROI"/> - <xs:enumeration value="TSUT"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="EntryDetails1"> - <xs:sequence> - <xs:element name="Btch" type="BatchInformation2" minOccurs="0"/> - <xs:element name="TxDtls" type="EntryTransaction2" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="EntryStatus2Code"> - <xs:restriction base="xs:string"> - <xs:enumeration value="BOOK"/> - <xs:enumeration value="PDNG"/> - <xs:enumeration value="INFO"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="EntryTransaction2"> - <xs:sequence> - <xs:element name="Refs" type="TransactionReferences2" minOccurs="0"/> - <xs:element name="AmtDtls" type="AmountAndCurrencyExchange3" minOccurs="0"/> - <xs:element name="Avlbty" type="CashBalanceAvailability2" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="BkTxCd" type="BankTransactionCodeStructure4" minOccurs="0"/> - <xs:element name="Chrgs" type="ChargesInformation6" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="Intrst" type="TransactionInterest2" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="RltdPties" type="TransactionParty2" minOccurs="0"/> - <xs:element name="RltdAgts" type="TransactionAgents2" minOccurs="0"/> - <xs:element name="Purp" type="Purpose2Choice" minOccurs="0"/> - <xs:element name="RltdRmtInf" type="RemittanceLocation2" minOccurs="0" maxOccurs="10"/> - <xs:element name="RmtInf" type="RemittanceInformation5" minOccurs="0"/> - <xs:element name="RltdDts" type="TransactionDates2" minOccurs="0"/> - <xs:element name="RltdPric" type="TransactionPrice2Choice" minOccurs="0"/> - <xs:element name="RltdQties" type="TransactionQuantities1Choice" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="FinInstrmId" type="SecurityIdentification4Choice" minOccurs="0"/> - <xs:element name="Tax" type="TaxInformation3" minOccurs="0"/> - <xs:element name="RtrInf" type="ReturnReasonInformation10" minOccurs="0"/> - <xs:element name="CorpActn" type="CorporateAction1" minOccurs="0"/> - <xs:element name="SfkpgAcct" type="CashAccount16" minOccurs="0"/> - <xs:element name="AddtlTxInf" type="Max500Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="ExternalAccountIdentification1Code"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="4"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ExternalBalanceSubType1Code"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="4"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ExternalBankTransactionDomain1Code"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="4"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ExternalBankTransactionFamily1Code"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="4"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ExternalBankTransactionSubFamily1Code"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="4"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ExternalClearingSystemIdentification1Code"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="5"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ExternalFinancialInstitutionIdentification1Code"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="4"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ExternalOrganisationIdentification1Code"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="4"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ExternalPersonIdentification1Code"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="4"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ExternalPurpose1Code"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="4"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ExternalReportingSource1Code"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="4"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ExternalReturnReason1Code"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="4"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ExternalTechnicalInputChannel1Code"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="4"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="FinancialIdentificationSchemeName1Choice"> - <xs:sequence> - <xs:choice> - <xs:element name="Cd" type="ExternalFinancialInstitutionIdentification1Code"/> - <xs:element name="Prtry" type="Max35Text"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:complexType name="FinancialInstitutionIdentification7"> - <xs:sequence> - <xs:element name="BIC" type="BICIdentifier" minOccurs="0"/> - <xs:element name="ClrSysMmbId" type="ClearingSystemMemberIdentification2" minOccurs="0"/> - <xs:element name="Nm" type="Max140Text" minOccurs="0"/> - <xs:element name="PstlAdr" type="PostalAddress6" minOccurs="0"/> - <xs:element name="Othr" type="GenericFinancialIdentification1" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="FinancialInstrumentQuantityChoice"> - <xs:sequence> - <xs:choice> - <xs:element name="Unit" type="DecimalNumber"/> - <xs:element name="FaceAmt" type="ImpliedCurrencyAndAmount"/> - <xs:element name="AmtsdVal" type="ImpliedCurrencyAndAmount"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:complexType name="FromToAmountRange"> - <xs:sequence> - <xs:element name="FrAmt" type="AmountRangeBoundary1"/> - <xs:element name="ToAmt" type="AmountRangeBoundary1"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="GenericAccountIdentification1"> - <xs:sequence> - <xs:element name="Id" type="Max34Text"/> - <xs:element name="SchmeNm" type="AccountSchemeName1Choice" minOccurs="0"/> - <xs:element name="Issr" type="Max35Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="GenericFinancialIdentification1"> - <xs:sequence> - <xs:element name="Id" type="Max35Text"/> - <xs:element name="SchmeNm" type="FinancialIdentificationSchemeName1Choice" minOccurs="0"/> - <xs:element name="Issr" type="Max35Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="GenericIdentification3"> - <xs:sequence> - <xs:element name="Id" type="Max35Text"/> - <xs:element name="Issr" type="Max35Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="GenericOrganisationIdentification1"> - <xs:sequence> - <xs:element name="Id" type="Max35Text"/> - <xs:element name="SchmeNm" type="OrganisationIdentificationSchemeName1Choice" minOccurs="0"/> - <xs:element name="Issr" type="Max35Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="GenericPersonIdentification1"> - <xs:sequence> - <xs:element name="Id" type="Max35Text"/> - <xs:element name="SchmeNm" type="PersonIdentificationSchemeName1Choice" minOccurs="0"/> - <xs:element name="Issr" type="Max35Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="GroupHeader42"> - <xs:sequence> - <xs:element name="MsgId" type="Max35Text"/> - <xs:element name="CreDtTm" type="ISODateTime"/> - <xs:element name="MsgRcpt" type="PartyIdentification32" minOccurs="0"/> - <xs:element name="MsgPgntn" type="Pagination" minOccurs="0"/> - <xs:element name="AddtlInf" type="Max500Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="IBAN2007Identifier"> - <xs:restriction base="xs:string"> - <xs:pattern value="[A-Z]{2,2}[0-9]{2,2}[a-zA-Z0-9]{1,30}"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ISINIdentifier"> - <xs:restriction base="xs:string"> - <xs:pattern value="[A-Z0-9]{12,12}"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ISODate"> - <xs:restriction base="xs:date"/> - </xs:simpleType> - <xs:simpleType name="ISODateTime"> - <xs:restriction base="xs:dateTime"/> - </xs:simpleType> - <xs:complexType name="ImpliedCurrencyAmountRangeChoice"> - <xs:sequence> - <xs:choice> - <xs:element name="FrAmt" type="AmountRangeBoundary1"/> - <xs:element name="ToAmt" type="AmountRangeBoundary1"/> - <xs:element name="FrToAmt" type="FromToAmountRange"/> - <xs:element name="EQAmt" type="ImpliedCurrencyAndAmount"/> - <xs:element name="NEQAmt" type="ImpliedCurrencyAndAmount"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="ImpliedCurrencyAndAmount"> - <xs:restriction base="xs:decimal"> - <xs:minInclusive value="0"/> - <xs:fractionDigits value="5"/> - <xs:totalDigits value="18"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="InterestType1Choice"> - <xs:sequence> - <xs:choice> - <xs:element name="Cd" type="InterestType1Code"/> - <xs:element name="Prtry" type="Max35Text"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="InterestType1Code"> - <xs:restriction base="xs:string"> - <xs:enumeration value="INDY"/> - <xs:enumeration value="OVRN"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="Max105Text"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="105"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="Max140Text"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="140"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="Max15NumericText"> - <xs:restriction base="xs:string"> - <xs:pattern value="[0-9]{1,15}"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="Max15PlusSignedNumericText"> - <xs:restriction base="xs:string"> - <xs:pattern value="[+]{0,1}[0-9]{1,15}"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="Max16Text"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="16"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="Max2048Text"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="2048"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="Max34Text"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="34"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="Max35Text"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="35"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="Max4Text"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="4"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="Max500Text"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="500"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="Max5NumericText"> - <xs:restriction base="xs:string"> - <xs:pattern value="[0-9]{1,5}"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="Max70Text"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="70"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="MessageIdentification2"> - <xs:sequence> - <xs:element name="MsgNmId" type="Max35Text" minOccurs="0"/> - <xs:element name="MsgId" type="Max35Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="NameAndAddress10"> - <xs:sequence> - <xs:element name="Nm" type="Max140Text"/> - <xs:element name="Adr" type="PostalAddress6"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="NamePrefix1Code"> - <xs:restriction base="xs:string"> - <xs:enumeration value="DOCT"/> - <xs:enumeration value="MIST"/> - <xs:enumeration value="MISS"/> - <xs:enumeration value="MADM"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="Number"> - <xs:restriction base="xs:decimal"> - <xs:fractionDigits value="0"/> - <xs:totalDigits value="18"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="NumberAndSumOfTransactions1"> - <xs:sequence> - <xs:element name="NbOfNtries" type="Max15NumericText" minOccurs="0"/> - <xs:element name="Sum" type="DecimalNumber" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="NumberAndSumOfTransactions2"> - <xs:sequence> - <xs:element name="NbOfNtries" type="Max15NumericText" minOccurs="0"/> - <xs:element name="Sum" type="DecimalNumber" minOccurs="0"/> - <xs:element name="TtlNetNtryAmt" type="DecimalNumber" minOccurs="0"/> - <xs:element name="CdtDbtInd" type="CreditDebitCode" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="OrganisationIdentification4"> - <xs:sequence> - <xs:element name="BICOrBEI" type="AnyBICIdentifier" minOccurs="0"/> - <xs:element name="Othr" type="GenericOrganisationIdentification1" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="OrganisationIdentificationSchemeName1Choice"> - <xs:sequence> - <xs:choice> - <xs:element name="Cd" type="ExternalOrganisationIdentification1Code"/> - <xs:element name="Prtry" type="Max35Text"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:complexType name="Pagination"> - <xs:sequence> - <xs:element name="PgNb" type="Max5NumericText"/> - <xs:element name="LastPgInd" type="YesNoIndicator"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="Party6Choice"> - <xs:sequence> - <xs:choice> - <xs:element name="OrgId" type="OrganisationIdentification4"/> - <xs:element name="PrvtId" type="PersonIdentification5"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:complexType name="PartyIdentification32"> - <xs:sequence> - <xs:element name="Nm" type="Max140Text" minOccurs="0"/> - <xs:element name="PstlAdr" type="PostalAddress6" minOccurs="0"/> - <xs:element name="Id" type="Party6Choice" minOccurs="0"/> - <xs:element name="CtryOfRes" type="CountryCode" minOccurs="0"/> - <xs:element name="CtctDtls" type="ContactDetails2" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="PercentageRate"> - <xs:restriction base="xs:decimal"> - <xs:fractionDigits value="10"/> - <xs:totalDigits value="11"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="PersonIdentification5"> - <xs:sequence> - <xs:element name="DtAndPlcOfBirth" type="DateAndPlaceOfBirth" minOccurs="0"/> - <xs:element name="Othr" type="GenericPersonIdentification1" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="PersonIdentificationSchemeName1Choice"> - <xs:sequence> - <xs:choice> - <xs:element name="Cd" type="ExternalPersonIdentification1Code"/> - <xs:element name="Prtry" type="Max35Text"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="PhoneNumber"> - <xs:restriction base="xs:string"> - <xs:pattern value="\+[0-9]{1,3}-[0-9()+\-]{1,30}"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="PostalAddress6"> - <xs:sequence> - <xs:element name="AdrTp" type="AddressType2Code" minOccurs="0"/> - <xs:element name="Dept" type="Max70Text" minOccurs="0"/> - <xs:element name="SubDept" type="Max70Text" minOccurs="0"/> - <xs:element name="StrtNm" type="Max70Text" minOccurs="0"/> - <xs:element name="BldgNb" type="Max16Text" minOccurs="0"/> - <xs:element name="PstCd" type="Max16Text" minOccurs="0"/> - <xs:element name="TwnNm" type="Max35Text" minOccurs="0"/> - <xs:element name="CtrySubDvsn" type="Max35Text" minOccurs="0"/> - <xs:element name="Ctry" type="CountryCode" minOccurs="0"/> - <xs:element name="AdrLine" type="Max70Text" minOccurs="0" maxOccurs="7"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ProprietaryAgent2"> - <xs:sequence> - <xs:element name="Tp" type="Max35Text"/> - <xs:element name="Agt" type="BranchAndFinancialInstitutionIdentification4"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ProprietaryBankTransactionCodeStructure1"> - <xs:sequence> - <xs:element name="Cd" type="Max35Text"/> - <xs:element name="Issr" type="Max35Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ProprietaryDate2"> - <xs:sequence> - <xs:element name="Tp" type="Max35Text"/> - <xs:element name="Dt" type="DateAndDateTimeChoice"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ProprietaryParty2"> - <xs:sequence> - <xs:element name="Tp" type="Max35Text"/> - <xs:element name="Pty" type="PartyIdentification32"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ProprietaryPrice2"> - <xs:sequence> - <xs:element name="Tp" type="Max35Text"/> - <xs:element name="Pric" type="ActiveOrHistoricCurrencyAndAmount"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ProprietaryQuantity1"> - <xs:sequence> - <xs:element name="Tp" type="Max35Text"/> - <xs:element name="Qty" type="Max35Text"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ProprietaryReference1"> - <xs:sequence> - <xs:element name="Tp" type="Max35Text"/> - <xs:element name="Ref" type="Max35Text"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="Purpose2Choice"> - <xs:sequence> - <xs:choice> - <xs:element name="Cd" type="ExternalPurpose1Code"/> - <xs:element name="Prtry" type="Max35Text"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:complexType name="Rate3"> - <xs:sequence> - <xs:element name="Tp" type="RateType4Choice"/> - <xs:element name="VldtyRg" type="CurrencyAndAmountRange2" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="RateType4Choice"> - <xs:sequence> - <xs:choice> - <xs:element name="Pctg" type="PercentageRate"/> - <xs:element name="Othr" type="Max35Text"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ReferredDocumentInformation3"> - <xs:sequence> - <xs:element name="Tp" type="ReferredDocumentType2" minOccurs="0"/> - <xs:element name="Nb" type="Max35Text" minOccurs="0"/> - <xs:element name="RltdDt" type="ISODate" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ReferredDocumentType1Choice"> - <xs:sequence> - <xs:choice> - <xs:element name="Cd" type="DocumentType5Code"/> - <xs:element name="Prtry" type="Max35Text"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ReferredDocumentType2"> - <xs:sequence> - <xs:element name="CdOrPrtry" type="ReferredDocumentType1Choice"/> - <xs:element name="Issr" type="Max35Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="RemittanceAmount1"> - <xs:sequence> - <xs:element name="DuePyblAmt" type="ActiveOrHistoricCurrencyAndAmount" minOccurs="0"/> - <xs:element name="DscntApldAmt" type="ActiveOrHistoricCurrencyAndAmount" minOccurs="0"/> - <xs:element name="CdtNoteAmt" type="ActiveOrHistoricCurrencyAndAmount" minOccurs="0"/> - <xs:element name="TaxAmt" type="ActiveOrHistoricCurrencyAndAmount" minOccurs="0"/> - <xs:element name="AdjstmntAmtAndRsn" type="DocumentAdjustment1" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="RmtdAmt" type="ActiveOrHistoricCurrencyAndAmount" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="RemittanceInformation5"> - <xs:sequence> - <xs:element name="Ustrd" type="Max140Text" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="Strd" type="StructuredRemittanceInformation7" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="RemittanceLocation2"> - <xs:sequence> - <xs:element name="RmtId" type="Max35Text" minOccurs="0"/> - <xs:element name="RmtLctnMtd" type="RemittanceLocationMethod2Code" minOccurs="0"/> - <xs:element name="RmtLctnElctrncAdr" type="Max2048Text" minOccurs="0"/> - <xs:element name="RmtLctnPstlAdr" type="NameAndAddress10" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="RemittanceLocationMethod2Code"> - <xs:restriction base="xs:string"> - <xs:enumeration value="FAXI"/> - <xs:enumeration value="EDIC"/> - <xs:enumeration value="URID"/> - <xs:enumeration value="EMAL"/> - <xs:enumeration value="POST"/> - <xs:enumeration value="SMSM"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="ReportEntry2"> - <xs:sequence> - <xs:element name="NtryRef" type="Max35Text" minOccurs="0"/> - <xs:element name="Amt" type="ActiveOrHistoricCurrencyAndAmount"/> - <xs:element name="CdtDbtInd" type="CreditDebitCode"/> - <xs:element name="RvslInd" type="TrueFalseIndicator" minOccurs="0"/> - <xs:element name="Sts" type="EntryStatus2Code"/> - <xs:element name="BookgDt" type="DateAndDateTimeChoice" minOccurs="0"/> - <xs:element name="ValDt" type="DateAndDateTimeChoice" minOccurs="0"/> - <xs:element name="AcctSvcrRef" type="Max35Text" minOccurs="0"/> - <xs:element name="Avlbty" type="CashBalanceAvailability2" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="BkTxCd" type="BankTransactionCodeStructure4"/> - <xs:element name="ComssnWvrInd" type="YesNoIndicator" minOccurs="0"/> - <xs:element name="AddtlInfInd" type="MessageIdentification2" minOccurs="0"/> - <xs:element name="AmtDtls" type="AmountAndCurrencyExchange3" minOccurs="0"/> - <xs:element name="Chrgs" type="ChargesInformation6" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="TechInptChanl" type="TechnicalInputChannel1Choice" minOccurs="0"/> - <xs:element name="Intrst" type="TransactionInterest2" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="NtryDtls" type="EntryDetails1" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="AddtlNtryInf" type="Max500Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ReportingSource1Choice"> - <xs:sequence> - <xs:choice> - <xs:element name="Cd" type="ExternalReportingSource1Code"/> - <xs:element name="Prtry" type="Max35Text"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ReturnReason5Choice"> - <xs:sequence> - <xs:choice> - <xs:element name="Cd" type="ExternalReturnReason1Code"/> - <xs:element name="Prtry" type="Max35Text"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ReturnReasonInformation10"> - <xs:sequence> - <xs:element name="OrgnlBkTxCd" type="BankTransactionCodeStructure4" minOccurs="0"/> - <xs:element name="Orgtr" type="PartyIdentification32" minOccurs="0"/> - <xs:element name="Rsn" type="ReturnReason5Choice" minOccurs="0"/> - <xs:element name="AddtlInf" type="Max105Text" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="SecurityIdentification4Choice"> - <xs:sequence> - <xs:choice> - <xs:element name="ISIN" type="ISINIdentifier"/> - <xs:element name="Prtry" type="AlternateSecurityIdentification2"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:complexType name="StructuredRemittanceInformation7"> - <xs:sequence> - <xs:element name="RfrdDocInf" type="ReferredDocumentInformation3" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="RfrdDocAmt" type="RemittanceAmount1" minOccurs="0"/> - <xs:element name="CdtrRefInf" type="CreditorReferenceInformation2" minOccurs="0"/> - <xs:element name="Invcr" type="PartyIdentification32" minOccurs="0"/> - <xs:element name="Invcee" type="PartyIdentification32" minOccurs="0"/> - <xs:element name="AddtlRmtInf" type="Max140Text" minOccurs="0" maxOccurs="3"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TaxAmount1"> - <xs:sequence> - <xs:element name="Rate" type="PercentageRate" minOccurs="0"/> - <xs:element name="TaxblBaseAmt" type="ActiveOrHistoricCurrencyAndAmount" minOccurs="0"/> - <xs:element name="TtlAmt" type="ActiveOrHistoricCurrencyAndAmount" minOccurs="0"/> - <xs:element name="Dtls" type="TaxRecordDetails1" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TaxAuthorisation1"> - <xs:sequence> - <xs:element name="Titl" type="Max35Text" minOccurs="0"/> - <xs:element name="Nm" type="Max140Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TaxCharges2"> - <xs:sequence> - <xs:element name="Id" type="Max35Text" minOccurs="0"/> - <xs:element name="Rate" type="PercentageRate" minOccurs="0"/> - <xs:element name="Amt" type="ActiveOrHistoricCurrencyAndAmount" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TaxInformation3"> - <xs:sequence> - <xs:element name="Cdtr" type="TaxParty1" minOccurs="0"/> - <xs:element name="Dbtr" type="TaxParty2" minOccurs="0"/> - <xs:element name="AdmstnZn" type="Max35Text" minOccurs="0"/> - <xs:element name="RefNb" type="Max140Text" minOccurs="0"/> - <xs:element name="Mtd" type="Max35Text" minOccurs="0"/> - <xs:element name="TtlTaxblBaseAmt" type="ActiveOrHistoricCurrencyAndAmount" minOccurs="0"/> - <xs:element name="TtlTaxAmt" type="ActiveOrHistoricCurrencyAndAmount" minOccurs="0"/> - <xs:element name="Dt" type="ISODate" minOccurs="0"/> - <xs:element name="SeqNb" type="Number" minOccurs="0"/> - <xs:element name="Rcrd" type="TaxRecord1" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TaxParty1"> - <xs:sequence> - <xs:element name="TaxId" type="Max35Text" minOccurs="0"/> - <xs:element name="RegnId" type="Max35Text" minOccurs="0"/> - <xs:element name="TaxTp" type="Max35Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TaxParty2"> - <xs:sequence> - <xs:element name="TaxId" type="Max35Text" minOccurs="0"/> - <xs:element name="RegnId" type="Max35Text" minOccurs="0"/> - <xs:element name="TaxTp" type="Max35Text" minOccurs="0"/> - <xs:element name="Authstn" type="TaxAuthorisation1" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TaxPeriod1"> - <xs:sequence> - <xs:element name="Yr" type="ISODate" minOccurs="0"/> - <xs:element name="Tp" type="TaxRecordPeriod1Code" minOccurs="0"/> - <xs:element name="FrToDt" type="DatePeriodDetails" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TaxRecord1"> - <xs:sequence> - <xs:element name="Tp" type="Max35Text" minOccurs="0"/> - <xs:element name="Ctgy" type="Max35Text" minOccurs="0"/> - <xs:element name="CtgyDtls" type="Max35Text" minOccurs="0"/> - <xs:element name="DbtrSts" type="Max35Text" minOccurs="0"/> - <xs:element name="CertId" type="Max35Text" minOccurs="0"/> - <xs:element name="FrmsCd" type="Max35Text" minOccurs="0"/> - <xs:element name="Prd" type="TaxPeriod1" minOccurs="0"/> - <xs:element name="TaxAmt" type="TaxAmount1" minOccurs="0"/> - <xs:element name="AddtlInf" type="Max140Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TaxRecordDetails1"> - <xs:sequence> - <xs:element name="Prd" type="TaxPeriod1" minOccurs="0"/> - <xs:element name="Amt" type="ActiveOrHistoricCurrencyAndAmount"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="TaxRecordPeriod1Code"> - <xs:restriction base="xs:string"> - <xs:enumeration value="MM01"/> - <xs:enumeration value="MM02"/> - <xs:enumeration value="MM03"/> - <xs:enumeration value="MM04"/> - <xs:enumeration value="MM05"/> - <xs:enumeration value="MM06"/> - <xs:enumeration value="MM07"/> - <xs:enumeration value="MM08"/> - <xs:enumeration value="MM09"/> - <xs:enumeration value="MM10"/> - <xs:enumeration value="MM11"/> - <xs:enumeration value="MM12"/> - <xs:enumeration value="QTR1"/> - <xs:enumeration value="QTR2"/> - <xs:enumeration value="QTR3"/> - <xs:enumeration value="QTR4"/> - <xs:enumeration value="HLF1"/> - <xs:enumeration value="HLF2"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="TechnicalInputChannel1Choice"> - <xs:sequence> - <xs:choice> - <xs:element name="Cd" type="ExternalTechnicalInputChannel1Code"/> - <xs:element name="Prtry" type="Max35Text"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TotalTransactions2"> - <xs:sequence> - <xs:element name="TtlNtries" type="NumberAndSumOfTransactions2" minOccurs="0"/> - <xs:element name="TtlCdtNtries" type="NumberAndSumOfTransactions1" minOccurs="0"/> - <xs:element name="TtlDbtNtries" type="NumberAndSumOfTransactions1" minOccurs="0"/> - <xs:element name="TtlNtriesPerBkTxCd" type="TotalsPerBankTransactionCode2" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TotalsPerBankTransactionCode2"> - <xs:sequence> - <xs:element name="NbOfNtries" type="Max15NumericText" minOccurs="0"/> - <xs:element name="Sum" type="DecimalNumber" minOccurs="0"/> - <xs:element name="TtlNetNtryAmt" type="DecimalNumber" minOccurs="0"/> - <xs:element name="CdtDbtInd" type="CreditDebitCode" minOccurs="0"/> - <xs:element name="FcstInd" type="TrueFalseIndicator" minOccurs="0"/> - <xs:element name="BkTxCd" type="BankTransactionCodeStructure4"/> - <xs:element name="Avlbty" type="CashBalanceAvailability2" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TransactionAgents2"> - <xs:sequence> - <xs:element name="DbtrAgt" type="BranchAndFinancialInstitutionIdentification4" minOccurs="0"/> - <xs:element name="CdtrAgt" type="BranchAndFinancialInstitutionIdentification4" minOccurs="0"/> - <xs:element name="IntrmyAgt1" type="BranchAndFinancialInstitutionIdentification4" minOccurs="0"/> - <xs:element name="IntrmyAgt2" type="BranchAndFinancialInstitutionIdentification4" minOccurs="0"/> - <xs:element name="IntrmyAgt3" type="BranchAndFinancialInstitutionIdentification4" minOccurs="0"/> - <xs:element name="RcvgAgt" type="BranchAndFinancialInstitutionIdentification4" minOccurs="0"/> - <xs:element name="DlvrgAgt" type="BranchAndFinancialInstitutionIdentification4" minOccurs="0"/> - <xs:element name="IssgAgt" type="BranchAndFinancialInstitutionIdentification4" minOccurs="0"/> - <xs:element name="SttlmPlc" type="BranchAndFinancialInstitutionIdentification4" minOccurs="0"/> - <xs:element name="Prtry" type="ProprietaryAgent2" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TransactionDates2"> - <xs:sequence> - <xs:element name="AccptncDtTm" type="ISODateTime" minOccurs="0"/> - <xs:element name="TradActvtyCtrctlSttlmDt" type="ISODate" minOccurs="0"/> - <xs:element name="TradDt" type="ISODate" minOccurs="0"/> - <xs:element name="IntrBkSttlmDt" type="ISODate" minOccurs="0"/> - <xs:element name="StartDt" type="ISODate" minOccurs="0"/> - <xs:element name="EndDt" type="ISODate" minOccurs="0"/> - <xs:element name="TxDtTm" type="ISODateTime" minOccurs="0"/> - <xs:element name="Prtry" type="ProprietaryDate2" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TransactionInterest2"> - <xs:sequence> - <xs:element name="Amt" type="ActiveOrHistoricCurrencyAndAmount"/> - <xs:element name="CdtDbtInd" type="CreditDebitCode"/> - <xs:element name="Tp" type="InterestType1Choice" minOccurs="0"/> - <xs:element name="Rate" type="Rate3" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="FrToDt" type="DateTimePeriodDetails" minOccurs="0"/> - <xs:element name="Rsn" type="Max35Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TransactionParty2"> - <xs:sequence> - <xs:element name="InitgPty" type="PartyIdentification32" minOccurs="0"/> - <xs:element name="Dbtr" type="PartyIdentification32" minOccurs="0"/> - <xs:element name="DbtrAcct" type="CashAccount16" minOccurs="0"/> - <xs:element name="UltmtDbtr" type="PartyIdentification32" minOccurs="0"/> - <xs:element name="Cdtr" type="PartyIdentification32" minOccurs="0"/> - <xs:element name="CdtrAcct" type="CashAccount16" minOccurs="0"/> - <xs:element name="UltmtCdtr" type="PartyIdentification32" minOccurs="0"/> - <xs:element name="TradgPty" type="PartyIdentification32" minOccurs="0"/> - <xs:element name="Prtry" type="ProprietaryParty2" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TransactionPrice2Choice"> - <xs:sequence> - <xs:choice> - <xs:element name="DealPric" type="ActiveOrHistoricCurrencyAndAmount"/> - <xs:element name="Prtry" type="ProprietaryPrice2" maxOccurs="unbounded"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TransactionQuantities1Choice"> - <xs:sequence> - <xs:choice> - <xs:element name="Qty" type="FinancialInstrumentQuantityChoice"/> - <xs:element name="Prtry" type="ProprietaryQuantity1"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TransactionReferences2"> - <xs:sequence> - <xs:element name="MsgId" type="Max35Text" minOccurs="0"/> - <xs:element name="AcctSvcrRef" type="Max35Text" minOccurs="0"/> - <xs:element name="PmtInfId" type="Max35Text" minOccurs="0"/> - <xs:element name="InstrId" type="Max35Text" minOccurs="0"/> - <xs:element name="EndToEndId" type="Max35Text" minOccurs="0"/> - <xs:element name="TxId" type="Max35Text" minOccurs="0"/> - <xs:element name="MndtId" type="Max35Text" minOccurs="0"/> - <xs:element name="ChqNb" type="Max35Text" minOccurs="0"/> - <xs:element name="ClrSysRef" type="Max35Text" minOccurs="0"/> - <xs:element name="Prtry" type="ProprietaryReference1" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="TrueFalseIndicator"> - <xs:restriction base="xs:boolean"/> - </xs:simpleType> - <xs:simpleType name="YesNoIndicator"> - <xs:restriction base="xs:boolean"/> - </xs:simpleType> -</xs:schema> diff --git a/libeufin-common/src/main/resources/xsd/camt.054.001.02.xsd b/libeufin-common/src/main/resources/xsd/camt.054.001.02.xsd @@ -1,1240 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<!-- Mit XMLSpy v2008 rel. 2 (http://www.altova.com) von Wenzel (SIZ Bonn) bearbeitet --> -<!--Generated by SWIFTStandards Workstation (build:R6.1.0.2) on 2009 Jan 08 17:30:53--> -<xs:schema xmlns="urn:iso:std:iso:20022:tech:xsd:camt.054.001.02" xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="urn:iso:std:iso:20022:tech:xsd:camt.054.001.02" elementFormDefault="qualified"> - <xs:element name="Document" type="Document"/> - <xs:complexType name="AccountIdentification4Choice"> - <xs:sequence> - <xs:choice> - <xs:element name="IBAN" type="IBAN2007Identifier"/> - <xs:element name="Othr" type="GenericAccountIdentification1"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:complexType name="AccountInterest2"> - <xs:sequence> - <xs:element name="Tp" type="InterestType1Choice" minOccurs="0"/> - <xs:element name="Rate" type="Rate3" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="FrToDt" type="DateTimePeriodDetails" minOccurs="0"/> - <xs:element name="Rsn" type="Max35Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="AccountNotification2"> - <xs:sequence> - <xs:element name="Id" type="Max35Text"/> - <xs:element name="ElctrncSeqNb" type="Number" minOccurs="0"/> - <xs:element name="LglSeqNb" type="Number" minOccurs="0"/> - <xs:element name="CreDtTm" type="ISODateTime"/> - <xs:element name="FrToDt" type="DateTimePeriodDetails" minOccurs="0"/> - <xs:element name="CpyDplctInd" type="CopyDuplicate1Code" minOccurs="0"/> - <xs:element name="RptgSrc" type="ReportingSource1Choice" minOccurs="0"/> - <xs:element name="Acct" type="CashAccount20"/> - <xs:element name="RltdAcct" type="CashAccount16" minOccurs="0"/> - <xs:element name="Intrst" type="AccountInterest2" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="TxsSummry" type="TotalTransactions2" minOccurs="0"/> - <xs:element name="Ntry" type="ReportEntry2" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="AddtlNtfctnInf" type="Max500Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="AccountSchemeName1Choice"> - <xs:sequence> - <xs:choice> - <xs:element name="Cd" type="ExternalAccountIdentification1Code"/> - <xs:element name="Prtry" type="Max35Text"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="ActiveOrHistoricCurrencyAndAmount_SimpleType"> - <xs:restriction base="xs:decimal"> - <xs:minInclusive value="0"/> - <xs:fractionDigits value="5"/> - <xs:totalDigits value="18"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="ActiveOrHistoricCurrencyAndAmount"> - <xs:simpleContent> - <xs:extension base="ActiveOrHistoricCurrencyAndAmount_SimpleType"> - <xs:attribute name="Ccy" type="ActiveOrHistoricCurrencyCode" use="required"/> - </xs:extension> - </xs:simpleContent> - </xs:complexType> - <xs:simpleType name="ActiveOrHistoricCurrencyCode"> - <xs:restriction base="xs:string"> - <xs:pattern value="[A-Z]{3,13}"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="AddressType2Code"> - <xs:restriction base="xs:string"> - <xs:enumeration value="ADDR"/> - <xs:enumeration value="PBOX"/> - <xs:enumeration value="HOME"/> - <xs:enumeration value="BIZZ"/> - <xs:enumeration value="MLTO"/> - <xs:enumeration value="DLVY"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="AlternateSecurityIdentification2"> - <xs:sequence> - <xs:element name="Tp" type="Max35Text"/> - <xs:element name="Id" type="Max35Text"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="AmountAndCurrencyExchange3"> - <xs:sequence> - <xs:element name="InstdAmt" type="AmountAndCurrencyExchangeDetails3" minOccurs="0"/> - <xs:element name="TxAmt" type="AmountAndCurrencyExchangeDetails3" minOccurs="0"/> - <xs:element name="CntrValAmt" type="AmountAndCurrencyExchangeDetails3" minOccurs="0"/> - <xs:element name="AnncdPstngAmt" type="AmountAndCurrencyExchangeDetails3" minOccurs="0"/> - <xs:element name="PrtryAmt" type="AmountAndCurrencyExchangeDetails4" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="AmountAndCurrencyExchangeDetails3"> - <xs:sequence> - <xs:element name="Amt" type="ActiveOrHistoricCurrencyAndAmount"/> - <xs:element name="CcyXchg" type="CurrencyExchange5" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="AmountAndCurrencyExchangeDetails4"> - <xs:sequence> - <xs:element name="Tp" type="Max35Text"/> - <xs:element name="Amt" type="ActiveOrHistoricCurrencyAndAmount"/> - <xs:element name="CcyXchg" type="CurrencyExchange5" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="AmountRangeBoundary1"> - <xs:sequence> - <xs:element name="BdryAmt" type="ImpliedCurrencyAndAmount"/> - <xs:element name="Incl" type="YesNoIndicator"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="AnyBICIdentifier"> - <xs:restriction base="xs:string"> - <xs:pattern value="[A-Z]{6,6}[A-Z2-9][A-NP-Z0-9]([A-Z0-9]{3,3}){0,1}"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="BICIdentifier"> - <xs:restriction base="xs:string"> - <xs:pattern value="[A-Z]{6,6}[A-Z2-9][A-NP-Z0-9]([A-Z0-9]{3,3}){0,1}"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="BankToCustomerDebitCreditNotificationV02"> - <xs:sequence> - <xs:element name="GrpHdr" type="GroupHeader42"/> - <xs:element name="Ntfctn" type="AccountNotification2" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="BankTransactionCodeStructure4"> - <xs:sequence> - <xs:element name="Domn" type="BankTransactionCodeStructure5" minOccurs="0"/> - <xs:element name="Prtry" type="ProprietaryBankTransactionCodeStructure1" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="BankTransactionCodeStructure5"> - <xs:sequence> - <xs:element name="Cd" type="ExternalBankTransactionDomain1Code"/> - <xs:element name="Fmly" type="BankTransactionCodeStructure6"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="BankTransactionCodeStructure6"> - <xs:sequence> - <xs:element name="Cd" type="ExternalBankTransactionFamily1Code"/> - <xs:element name="SubFmlyCd" type="ExternalBankTransactionSubFamily1Code"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="BaseOneRate"> - <xs:restriction base="xs:decimal"> - <xs:fractionDigits value="10"/> - <xs:totalDigits value="11"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="BatchInformation2"> - <xs:sequence> - <xs:element name="MsgId" type="Max35Text" minOccurs="0"/> - <xs:element name="PmtInfId" type="Max35Text" minOccurs="0"/> - <xs:element name="NbOfTxs" type="Max15NumericText" minOccurs="0"/> - <xs:element name="TtlAmt" type="ActiveOrHistoricCurrencyAndAmount" minOccurs="0"/> - <xs:element name="CdtDbtInd" type="CreditDebitCode" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="BranchAndFinancialInstitutionIdentification4"> - <xs:sequence> - <xs:element name="FinInstnId" type="FinancialInstitutionIdentification7"/> - <xs:element name="BrnchId" type="BranchData2" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="BranchData2"> - <xs:sequence> - <xs:element name="Id" type="Max35Text" minOccurs="0"/> - <xs:element name="Nm" type="Max140Text" minOccurs="0"/> - <xs:element name="PstlAdr" type="PostalAddress6" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CashAccount16"> - <xs:sequence> - <xs:element name="Id" type="AccountIdentification4Choice"/> - <xs:element name="Tp" type="CashAccountType2" minOccurs="0"/> - <xs:element name="Ccy" type="ActiveOrHistoricCurrencyCode" minOccurs="0"/> - <xs:element name="Nm" type="Max70Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CashAccount20"> - <xs:sequence> - <xs:element name="Id" type="AccountIdentification4Choice"/> - <xs:element name="Tp" type="CashAccountType2" minOccurs="0"/> - <xs:element name="Ccy" type="ActiveOrHistoricCurrencyCode" minOccurs="0"/> - <xs:element name="Nm" type="Max70Text" minOccurs="0"/> - <xs:element name="Ownr" type="PartyIdentification32" minOccurs="0"/> - <xs:element name="Svcr" type="BranchAndFinancialInstitutionIdentification4" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CashAccountType2"> - <xs:sequence> - <xs:choice> - <xs:element name="Cd" type="CashAccountType4Code"/> - <xs:element name="Prtry" type="Max35Text"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="CashAccountType4Code"> - <xs:restriction base="xs:string"> - <xs:enumeration value="CASH"/> - <xs:enumeration value="CHAR"/> - <xs:enumeration value="COMM"/> - <xs:enumeration value="TAXE"/> - <xs:enumeration value="CISH"/> - <xs:enumeration value="TRAS"/> - <xs:enumeration value="SACC"/> - <xs:enumeration value="CACC"/> - <xs:enumeration value="SVGS"/> - <xs:enumeration value="ONDP"/> - <xs:enumeration value="MGLD"/> - <xs:enumeration value="NREX"/> - <xs:enumeration value="MOMA"/> - <xs:enumeration value="LOAN"/> - <xs:enumeration value="SLRY"/> - <xs:enumeration value="ODFT"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="CashBalanceAvailability2"> - <xs:sequence> - <xs:element name="Dt" type="CashBalanceAvailabilityDate1"/> - <xs:element name="Amt" type="ActiveOrHistoricCurrencyAndAmount"/> - <xs:element name="CdtDbtInd" type="CreditDebitCode"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CashBalanceAvailabilityDate1"> - <xs:sequence> - <xs:choice> - <xs:element name="NbOfDays" type="Max15PlusSignedNumericText"/> - <xs:element name="ActlDt" type="ISODate"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="ChargeBearerType1Code"> - <xs:restriction base="xs:string"> - <xs:enumeration value="DEBT"/> - <xs:enumeration value="CRED"/> - <xs:enumeration value="SHAR"/> - <xs:enumeration value="SLEV"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ChargeType1Code"> - <xs:restriction base="xs:string"> - <xs:enumeration value="BRKF"/> - <xs:enumeration value="COMM"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="ChargeType2Choice"> - <xs:sequence> - <xs:choice> - <xs:element name="Cd" type="ChargeType1Code"/> - <xs:element name="Prtry" type="GenericIdentification3"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ChargesInformation6"> - <xs:sequence> - <xs:element name="TtlChrgsAndTaxAmt" type="ActiveOrHistoricCurrencyAndAmount" minOccurs="0"/> - <xs:element name="Amt" type="ActiveOrHistoricCurrencyAndAmount"/> - <xs:element name="CdtDbtInd" type="CreditDebitCode" minOccurs="0"/> - <xs:element name="Tp" type="ChargeType2Choice" minOccurs="0"/> - <xs:element name="Rate" type="PercentageRate" minOccurs="0"/> - <xs:element name="Br" type="ChargeBearerType1Code" minOccurs="0"/> - <xs:element name="Pty" type="BranchAndFinancialInstitutionIdentification4" minOccurs="0"/> - <xs:element name="Tax" type="TaxCharges2" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ClearingSystemIdentification2Choice"> - <xs:sequence> - <xs:choice> - <xs:element name="Cd" type="ExternalClearingSystemIdentification1Code"/> - <xs:element name="Prtry" type="Max35Text"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ClearingSystemMemberIdentification2"> - <xs:sequence> - <xs:element name="ClrSysId" type="ClearingSystemIdentification2Choice" minOccurs="0"/> - <xs:element name="MmbId" type="Max35Text"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ContactDetails2"> - <xs:sequence> - <xs:element name="NmPrfx" type="NamePrefix1Code" minOccurs="0"/> - <xs:element name="Nm" type="Max140Text" minOccurs="0"/> - <xs:element name="PhneNb" type="PhoneNumber" minOccurs="0"/> - <xs:element name="MobNb" type="PhoneNumber" minOccurs="0"/> - <xs:element name="FaxNb" type="PhoneNumber" minOccurs="0"/> - <xs:element name="EmailAdr" type="Max2048Text" minOccurs="0"/> - <xs:element name="Othr" type="Max35Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="CopyDuplicate1Code"> - <xs:restriction base="xs:string"> - <xs:enumeration value="CODU"/> - <xs:enumeration value="COPY"/> - <xs:enumeration value="DUPL"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="CorporateAction1"> - <xs:sequence> - <xs:element name="Cd" type="Max35Text" minOccurs="0"/> - <xs:element name="Nb" type="Max35Text" minOccurs="0"/> - <xs:element name="Prtry" type="Max35Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="CountryCode"> - <xs:restriction base="xs:string"> - <xs:pattern value="[A-Z]{2,2}"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="CreditDebitCode"> - <xs:restriction base="xs:string"> - <xs:enumeration value="CRDT"/> - <xs:enumeration value="DBIT"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="CreditorReferenceInformation2"> - <xs:sequence> - <xs:element name="Tp" type="CreditorReferenceType2" minOccurs="0"/> - <xs:element name="Ref" type="Max35Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CreditorReferenceType1Choice"> - <xs:sequence> - <xs:choice> - <xs:element name="Cd" type="DocumentType3Code"/> - <xs:element name="Prtry" type="Max35Text"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CreditorReferenceType2"> - <xs:sequence> - <xs:element name="CdOrPrtry" type="CreditorReferenceType1Choice"/> - <xs:element name="Issr" type="Max35Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CurrencyAndAmountRange2"> - <xs:sequence> - <xs:element name="Amt" type="ImpliedCurrencyAmountRangeChoice"/> - <xs:element name="CdtDbtInd" type="CreditDebitCode" minOccurs="0"/> - <xs:element name="Ccy" type="ActiveOrHistoricCurrencyCode"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CurrencyExchange5"> - <xs:sequence> - <xs:element name="SrcCcy" type="ActiveOrHistoricCurrencyCode"/> - <xs:element name="TrgtCcy" type="ActiveOrHistoricCurrencyCode" minOccurs="0"/> - <xs:element name="UnitCcy" type="ActiveOrHistoricCurrencyCode" minOccurs="0"/> - <xs:element name="XchgRate" type="BaseOneRate"/> - <xs:element name="CtrctId" type="Max35Text" minOccurs="0"/> - <xs:element name="QtnDt" type="ISODateTime" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="DateAndDateTimeChoice"> - <xs:sequence> - <xs:choice> - <xs:element name="Dt" type="ISODate"/> - <xs:element name="DtTm" type="ISODateTime"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:complexType name="DateAndPlaceOfBirth"> - <xs:sequence> - <xs:element name="BirthDt" type="ISODate"/> - <xs:element name="PrvcOfBirth" type="Max35Text" minOccurs="0"/> - <xs:element name="CityOfBirth" type="Max35Text"/> - <xs:element name="CtryOfBirth" type="CountryCode"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="DatePeriodDetails"> - <xs:sequence> - <xs:element name="FrDt" type="ISODate"/> - <xs:element name="ToDt" type="ISODate"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="DateTimePeriodDetails"> - <xs:sequence> - <xs:element name="FrDtTm" type="ISODateTime"/> - <xs:element name="ToDtTm" type="ISODateTime"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="DecimalNumber"> - <xs:restriction base="xs:decimal"> - <xs:fractionDigits value="17"/> - <xs:totalDigits value="18"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="Document"> - <xs:sequence> - <xs:element name="BkToCstmrDbtCdtNtfctn" type="BankToCustomerDebitCreditNotificationV02"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="DocumentAdjustment1"> - <xs:sequence> - <xs:element name="Amt" type="ActiveOrHistoricCurrencyAndAmount"/> - <xs:element name="CdtDbtInd" type="CreditDebitCode" minOccurs="0"/> - <xs:element name="Rsn" type="Max4Text" minOccurs="0"/> - <xs:element name="AddtlInf" type="Max140Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="DocumentType3Code"> - <xs:restriction base="xs:string"> - <xs:enumeration value="RADM"/> - <xs:enumeration value="RPIN"/> - <xs:enumeration value="FXDR"/> - <xs:enumeration value="DISP"/> - <xs:enumeration value="PUOR"/> - <xs:enumeration value="SCOR"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="DocumentType5Code"> - <xs:restriction base="xs:string"> - <xs:enumeration value="MSIN"/> - <xs:enumeration value="CNFA"/> - <xs:enumeration value="DNFA"/> - <xs:enumeration value="CINV"/> - <xs:enumeration value="CREN"/> - <xs:enumeration value="DEBN"/> - <xs:enumeration value="HIRI"/> - <xs:enumeration value="SBIN"/> - <xs:enumeration value="CMCN"/> - <xs:enumeration value="SOAC"/> - <xs:enumeration value="DISP"/> - <xs:enumeration value="BOLD"/> - <xs:enumeration value="VCHR"/> - <xs:enumeration value="AROI"/> - <xs:enumeration value="TSUT"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="EntryDetails1"> - <xs:sequence> - <xs:element name="Btch" type="BatchInformation2" minOccurs="0"/> - <xs:element name="TxDtls" type="EntryTransaction2" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="EntryStatus2Code"> - <xs:restriction base="xs:string"> - <xs:enumeration value="BOOK"/> - <xs:enumeration value="PDNG"/> - <xs:enumeration value="INFO"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="EntryTransaction2"> - <xs:sequence> - <xs:element name="Refs" type="TransactionReferences2" minOccurs="0"/> - <xs:element name="AmtDtls" type="AmountAndCurrencyExchange3" minOccurs="0"/> - <xs:element name="Avlbty" type="CashBalanceAvailability2" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="BkTxCd" type="BankTransactionCodeStructure4" minOccurs="0"/> - <xs:element name="Chrgs" type="ChargesInformation6" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="Intrst" type="TransactionInterest2" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="RltdPties" type="TransactionParty2" minOccurs="0"/> - <xs:element name="RltdAgts" type="TransactionAgents2" minOccurs="0"/> - <xs:element name="Purp" type="Purpose2Choice" minOccurs="0"/> - <xs:element name="RltdRmtInf" type="RemittanceLocation2" minOccurs="0" maxOccurs="10"/> - <xs:element name="RmtInf" type="RemittanceInformation5" minOccurs="0"/> - <xs:element name="RltdDts" type="TransactionDates2" minOccurs="0"/> - <xs:element name="RltdPric" type="TransactionPrice2Choice" minOccurs="0"/> - <xs:element name="RltdQties" type="TransactionQuantities1Choice" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="FinInstrmId" type="SecurityIdentification4Choice" minOccurs="0"/> - <xs:element name="Tax" type="TaxInformation3" minOccurs="0"/> - <xs:element name="RtrInf" type="ReturnReasonInformation10" minOccurs="0"/> - <xs:element name="CorpActn" type="CorporateAction1" minOccurs="0"/> - <xs:element name="SfkpgAcct" type="CashAccount16" minOccurs="0"/> - <xs:element name="AddtlTxInf" type="Max500Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="ExternalAccountIdentification1Code"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="4"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ExternalBankTransactionDomain1Code"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="4"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ExternalBankTransactionFamily1Code"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="4"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ExternalBankTransactionSubFamily1Code"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="4"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ExternalClearingSystemIdentification1Code"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="5"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ExternalFinancialInstitutionIdentification1Code"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="4"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ExternalOrganisationIdentification1Code"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="4"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ExternalPersonIdentification1Code"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="4"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ExternalPurpose1Code"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="4"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ExternalReportingSource1Code"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="4"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ExternalReturnReason1Code"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="4"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ExternalTechnicalInputChannel1Code"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="4"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="FinancialIdentificationSchemeName1Choice"> - <xs:sequence> - <xs:choice> - <xs:element name="Cd" type="ExternalFinancialInstitutionIdentification1Code"/> - <xs:element name="Prtry" type="Max35Text"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:complexType name="FinancialInstitutionIdentification7"> - <xs:sequence> - <xs:element name="BIC" type="BICIdentifier" minOccurs="0"/> - <xs:element name="ClrSysMmbId" type="ClearingSystemMemberIdentification2" minOccurs="0"/> - <xs:element name="Nm" type="Max140Text" minOccurs="0"/> - <xs:element name="PstlAdr" type="PostalAddress6" minOccurs="0"/> - <xs:element name="Othr" type="GenericFinancialIdentification1" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="FinancialInstrumentQuantityChoice"> - <xs:sequence> - <xs:choice> - <xs:element name="Unit" type="DecimalNumber"/> - <xs:element name="FaceAmt" type="ImpliedCurrencyAndAmount"/> - <xs:element name="AmtsdVal" type="ImpliedCurrencyAndAmount"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:complexType name="FromToAmountRange"> - <xs:sequence> - <xs:element name="FrAmt" type="AmountRangeBoundary1"/> - <xs:element name="ToAmt" type="AmountRangeBoundary1"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="GenericAccountIdentification1"> - <xs:sequence> - <xs:element name="Id" type="Max34Text"/> - <xs:element name="SchmeNm" type="AccountSchemeName1Choice" minOccurs="0"/> - <xs:element name="Issr" type="Max35Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="GenericFinancialIdentification1"> - <xs:sequence> - <xs:element name="Id" type="Max35Text"/> - <xs:element name="SchmeNm" type="FinancialIdentificationSchemeName1Choice" minOccurs="0"/> - <xs:element name="Issr" type="Max35Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="GenericIdentification3"> - <xs:sequence> - <xs:element name="Id" type="Max35Text"/> - <xs:element name="Issr" type="Max35Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="GenericOrganisationIdentification1"> - <xs:sequence> - <xs:element name="Id" type="Max35Text"/> - <xs:element name="SchmeNm" type="OrganisationIdentificationSchemeName1Choice" minOccurs="0"/> - <xs:element name="Issr" type="Max35Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="GenericPersonIdentification1"> - <xs:sequence> - <xs:element name="Id" type="Max35Text"/> - <xs:element name="SchmeNm" type="PersonIdentificationSchemeName1Choice" minOccurs="0"/> - <xs:element name="Issr" type="Max35Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="GroupHeader42"> - <xs:sequence> - <xs:element name="MsgId" type="Max35Text"/> - <xs:element name="CreDtTm" type="ISODateTime"/> - <xs:element name="MsgRcpt" type="PartyIdentification32" minOccurs="0"/> - <xs:element name="MsgPgntn" type="Pagination" minOccurs="0"/> - <xs:element name="AddtlInf" type="Max500Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="IBAN2007Identifier"> - <xs:restriction base="xs:string"> - <xs:pattern value="[A-Z]{2,2}[0-9]{2,2}[a-zA-Z0-9]{1,30}"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ISINIdentifier"> - <xs:restriction base="xs:string"> - <xs:pattern value="[A-Z0-9]{12,12}"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ISODate"> - <xs:restriction base="xs:date"/> - </xs:simpleType> - <xs:simpleType name="ISODateTime"> - <xs:restriction base="xs:dateTime"/> - </xs:simpleType> - <xs:complexType name="ImpliedCurrencyAmountRangeChoice"> - <xs:sequence> - <xs:choice> - <xs:element name="FrAmt" type="AmountRangeBoundary1"/> - <xs:element name="ToAmt" type="AmountRangeBoundary1"/> - <xs:element name="FrToAmt" type="FromToAmountRange"/> - <xs:element name="EQAmt" type="ImpliedCurrencyAndAmount"/> - <xs:element name="NEQAmt" type="ImpliedCurrencyAndAmount"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="ImpliedCurrencyAndAmount"> - <xs:restriction base="xs:decimal"> - <xs:minInclusive value="0"/> - <xs:fractionDigits value="5"/> - <xs:totalDigits value="18"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="InterestType1Choice"> - <xs:sequence> - <xs:choice> - <xs:element name="Cd" type="InterestType1Code"/> - <xs:element name="Prtry" type="Max35Text"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="InterestType1Code"> - <xs:restriction base="xs:string"> - <xs:enumeration value="INDY"/> - <xs:enumeration value="OVRN"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="Max105Text"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="105"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="Max140Text"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="140"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="Max15NumericText"> - <xs:restriction base="xs:string"> - <xs:pattern value="[0-9]{1,15}"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="Max15PlusSignedNumericText"> - <xs:restriction base="xs:string"> - <xs:pattern value="[+]{0,1}[0-9]{1,15}"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="Max16Text"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="16"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="Max2048Text"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="2048"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="Max34Text"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="34"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="Max35Text"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="35"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="Max4Text"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="4"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="Max500Text"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="500"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="Max5NumericText"> - <xs:restriction base="xs:string"> - <xs:pattern value="[0-9]{1,5}"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="Max70Text"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - <xs:maxLength value="70"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="MessageIdentification2"> - <xs:sequence> - <xs:element name="MsgNmId" type="Max35Text" minOccurs="0"/> - <xs:element name="MsgId" type="Max35Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="NameAndAddress10"> - <xs:sequence> - <xs:element name="Nm" type="Max140Text"/> - <xs:element name="Adr" type="PostalAddress6"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="NamePrefix1Code"> - <xs:restriction base="xs:string"> - <xs:enumeration value="DOCT"/> - <xs:enumeration value="MIST"/> - <xs:enumeration value="MISS"/> - <xs:enumeration value="MADM"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="Number"> - <xs:restriction base="xs:decimal"> - <xs:fractionDigits value="0"/> - <xs:totalDigits value="18"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="NumberAndSumOfTransactions1"> - <xs:sequence> - <xs:element name="NbOfNtries" type="Max15NumericText" minOccurs="0"/> - <xs:element name="Sum" type="DecimalNumber" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="NumberAndSumOfTransactions2"> - <xs:sequence> - <xs:element name="NbOfNtries" type="Max15NumericText" minOccurs="0"/> - <xs:element name="Sum" type="DecimalNumber" minOccurs="0"/> - <xs:element name="TtlNetNtryAmt" type="DecimalNumber" minOccurs="0"/> - <xs:element name="CdtDbtInd" type="CreditDebitCode" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="OrganisationIdentification4"> - <xs:sequence> - <xs:element name="BICOrBEI" type="AnyBICIdentifier" minOccurs="0"/> - <xs:element name="Othr" type="GenericOrganisationIdentification1" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="OrganisationIdentificationSchemeName1Choice"> - <xs:sequence> - <xs:choice> - <xs:element name="Cd" type="ExternalOrganisationIdentification1Code"/> - <xs:element name="Prtry" type="Max35Text"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:complexType name="Pagination"> - <xs:sequence> - <xs:element name="PgNb" type="Max5NumericText"/> - <xs:element name="LastPgInd" type="YesNoIndicator"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="Party6Choice"> - <xs:sequence> - <xs:choice> - <xs:element name="OrgId" type="OrganisationIdentification4"/> - <xs:element name="PrvtId" type="PersonIdentification5"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:complexType name="PartyIdentification32"> - <xs:sequence> - <xs:element name="Nm" type="Max140Text" minOccurs="0"/> - <xs:element name="PstlAdr" type="PostalAddress6" minOccurs="0"/> - <xs:element name="Id" type="Party6Choice" minOccurs="0"/> - <xs:element name="CtryOfRes" type="CountryCode" minOccurs="0"/> - <xs:element name="CtctDtls" type="ContactDetails2" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="PercentageRate"> - <xs:restriction base="xs:decimal"> - <xs:fractionDigits value="10"/> - <xs:totalDigits value="11"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="PersonIdentification5"> - <xs:sequence> - <xs:element name="DtAndPlcOfBirth" type="DateAndPlaceOfBirth" minOccurs="0"/> - <xs:element name="Othr" type="GenericPersonIdentification1" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="PersonIdentificationSchemeName1Choice"> - <xs:sequence> - <xs:choice> - <xs:element name="Cd" type="ExternalPersonIdentification1Code"/> - <xs:element name="Prtry" type="Max35Text"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="PhoneNumber"> - <xs:restriction base="xs:string"> - <xs:pattern value="\+[0-9]{1,3}-[0-9()+\-]{1,30}"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="PostalAddress6"> - <xs:sequence> - <xs:element name="AdrTp" type="AddressType2Code" minOccurs="0"/> - <xs:element name="Dept" type="Max70Text" minOccurs="0"/> - <xs:element name="SubDept" type="Max70Text" minOccurs="0"/> - <xs:element name="StrtNm" type="Max70Text" minOccurs="0"/> - <xs:element name="BldgNb" type="Max16Text" minOccurs="0"/> - <xs:element name="PstCd" type="Max16Text" minOccurs="0"/> - <xs:element name="TwnNm" type="Max35Text" minOccurs="0"/> - <xs:element name="CtrySubDvsn" type="Max35Text" minOccurs="0"/> - <xs:element name="Ctry" type="CountryCode" minOccurs="0"/> - <xs:element name="AdrLine" type="Max70Text" minOccurs="0" maxOccurs="7"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ProprietaryAgent2"> - <xs:sequence> - <xs:element name="Tp" type="Max35Text"/> - <xs:element name="Agt" type="BranchAndFinancialInstitutionIdentification4"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ProprietaryBankTransactionCodeStructure1"> - <xs:sequence> - <xs:element name="Cd" type="Max35Text"/> - <xs:element name="Issr" type="Max35Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ProprietaryDate2"> - <xs:sequence> - <xs:element name="Tp" type="Max35Text"/> - <xs:element name="Dt" type="DateAndDateTimeChoice"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ProprietaryParty2"> - <xs:sequence> - <xs:element name="Tp" type="Max35Text"/> - <xs:element name="Pty" type="PartyIdentification32"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ProprietaryPrice2"> - <xs:sequence> - <xs:element name="Tp" type="Max35Text"/> - <xs:element name="Pric" type="ActiveOrHistoricCurrencyAndAmount"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ProprietaryQuantity1"> - <xs:sequence> - <xs:element name="Tp" type="Max35Text"/> - <xs:element name="Qty" type="Max35Text"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ProprietaryReference1"> - <xs:sequence> - <xs:element name="Tp" type="Max35Text"/> - <xs:element name="Ref" type="Max35Text"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="Purpose2Choice"> - <xs:sequence> - <xs:choice> - <xs:element name="Cd" type="ExternalPurpose1Code"/> - <xs:element name="Prtry" type="Max35Text"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:complexType name="Rate3"> - <xs:sequence> - <xs:element name="Tp" type="RateType4Choice"/> - <xs:element name="VldtyRg" type="CurrencyAndAmountRange2" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="RateType4Choice"> - <xs:sequence> - <xs:choice> - <xs:element name="Pctg" type="PercentageRate"/> - <xs:element name="Othr" type="Max35Text"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ReferredDocumentInformation3"> - <xs:sequence> - <xs:element name="Tp" type="ReferredDocumentType2" minOccurs="0"/> - <xs:element name="Nb" type="Max35Text" minOccurs="0"/> - <xs:element name="RltdDt" type="ISODate" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ReferredDocumentType1Choice"> - <xs:sequence> - <xs:choice> - <xs:element name="Cd" type="DocumentType5Code"/> - <xs:element name="Prtry" type="Max35Text"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ReferredDocumentType2"> - <xs:sequence> - <xs:element name="CdOrPrtry" type="ReferredDocumentType1Choice"/> - <xs:element name="Issr" type="Max35Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="RemittanceAmount1"> - <xs:sequence> - <xs:element name="DuePyblAmt" type="ActiveOrHistoricCurrencyAndAmount" minOccurs="0"/> - <xs:element name="DscntApldAmt" type="ActiveOrHistoricCurrencyAndAmount" minOccurs="0"/> - <xs:element name="CdtNoteAmt" type="ActiveOrHistoricCurrencyAndAmount" minOccurs="0"/> - <xs:element name="TaxAmt" type="ActiveOrHistoricCurrencyAndAmount" minOccurs="0"/> - <xs:element name="AdjstmntAmtAndRsn" type="DocumentAdjustment1" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="RmtdAmt" type="ActiveOrHistoricCurrencyAndAmount" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="RemittanceInformation5"> - <xs:sequence> - <xs:element name="Ustrd" type="Max140Text" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="Strd" type="StructuredRemittanceInformation7" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="RemittanceLocation2"> - <xs:sequence> - <xs:element name="RmtId" type="Max35Text" minOccurs="0"/> - <xs:element name="RmtLctnMtd" type="RemittanceLocationMethod2Code" minOccurs="0"/> - <xs:element name="RmtLctnElctrncAdr" type="Max2048Text" minOccurs="0"/> - <xs:element name="RmtLctnPstlAdr" type="NameAndAddress10" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="RemittanceLocationMethod2Code"> - <xs:restriction base="xs:string"> - <xs:enumeration value="FAXI"/> - <xs:enumeration value="EDIC"/> - <xs:enumeration value="URID"/> - <xs:enumeration value="EMAL"/> - <xs:enumeration value="POST"/> - <xs:enumeration value="SMSM"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="ReportEntry2"> - <xs:sequence> - <xs:element name="NtryRef" type="Max35Text" minOccurs="0"/> - <xs:element name="Amt" type="ActiveOrHistoricCurrencyAndAmount"/> - <xs:element name="CdtDbtInd" type="CreditDebitCode"/> - <xs:element name="RvslInd" type="TrueFalseIndicator" minOccurs="0"/> - <xs:element name="Sts" type="EntryStatus2Code"/> - <xs:element name="BookgDt" type="DateAndDateTimeChoice" minOccurs="0"/> - <xs:element name="ValDt" type="DateAndDateTimeChoice" minOccurs="0"/> - <xs:element name="AcctSvcrRef" type="Max35Text" minOccurs="0"/> - <xs:element name="Avlbty" type="CashBalanceAvailability2" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="BkTxCd" type="BankTransactionCodeStructure4"/> - <xs:element name="ComssnWvrInd" type="YesNoIndicator" minOccurs="0"/> - <xs:element name="AddtlInfInd" type="MessageIdentification2" minOccurs="0"/> - <xs:element name="AmtDtls" type="AmountAndCurrencyExchange3" minOccurs="0"/> - <xs:element name="Chrgs" type="ChargesInformation6" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="TechInptChanl" type="TechnicalInputChannel1Choice" minOccurs="0"/> - <xs:element name="Intrst" type="TransactionInterest2" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="NtryDtls" type="EntryDetails1" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="AddtlNtryInf" type="Max500Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ReportingSource1Choice"> - <xs:sequence> - <xs:choice> - <xs:element name="Cd" type="ExternalReportingSource1Code"/> - <xs:element name="Prtry" type="Max35Text"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ReturnReason5Choice"> - <xs:sequence> - <xs:choice> - <xs:element name="Cd" type="ExternalReturnReason1Code"/> - <xs:element name="Prtry" type="Max35Text"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ReturnReasonInformation10"> - <xs:sequence> - <xs:element name="OrgnlBkTxCd" type="BankTransactionCodeStructure4" minOccurs="0"/> - <xs:element name="Orgtr" type="PartyIdentification32" minOccurs="0"/> - <xs:element name="Rsn" type="ReturnReason5Choice" minOccurs="0"/> - <xs:element name="AddtlInf" type="Max105Text" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="SecurityIdentification4Choice"> - <xs:sequence> - <xs:choice> - <xs:element name="ISIN" type="ISINIdentifier"/> - <xs:element name="Prtry" type="AlternateSecurityIdentification2"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:complexType name="StructuredRemittanceInformation7"> - <xs:sequence> - <xs:element name="RfrdDocInf" type="ReferredDocumentInformation3" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="RfrdDocAmt" type="RemittanceAmount1" minOccurs="0"/> - <xs:element name="CdtrRefInf" type="CreditorReferenceInformation2" minOccurs="0"/> - <xs:element name="Invcr" type="PartyIdentification32" minOccurs="0"/> - <xs:element name="Invcee" type="PartyIdentification32" minOccurs="0"/> - <xs:element name="AddtlRmtInf" type="Max140Text" minOccurs="0" maxOccurs="3"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TaxAmount1"> - <xs:sequence> - <xs:element name="Rate" type="PercentageRate" minOccurs="0"/> - <xs:element name="TaxblBaseAmt" type="ActiveOrHistoricCurrencyAndAmount" minOccurs="0"/> - <xs:element name="TtlAmt" type="ActiveOrHistoricCurrencyAndAmount" minOccurs="0"/> - <xs:element name="Dtls" type="TaxRecordDetails1" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TaxAuthorisation1"> - <xs:sequence> - <xs:element name="Titl" type="Max35Text" minOccurs="0"/> - <xs:element name="Nm" type="Max140Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TaxCharges2"> - <xs:sequence> - <xs:element name="Id" type="Max35Text" minOccurs="0"/> - <xs:element name="Rate" type="PercentageRate" minOccurs="0"/> - <xs:element name="Amt" type="ActiveOrHistoricCurrencyAndAmount" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TaxInformation3"> - <xs:sequence> - <xs:element name="Cdtr" type="TaxParty1" minOccurs="0"/> - <xs:element name="Dbtr" type="TaxParty2" minOccurs="0"/> - <xs:element name="AdmstnZn" type="Max35Text" minOccurs="0"/> - <xs:element name="RefNb" type="Max140Text" minOccurs="0"/> - <xs:element name="Mtd" type="Max35Text" minOccurs="0"/> - <xs:element name="TtlTaxblBaseAmt" type="ActiveOrHistoricCurrencyAndAmount" minOccurs="0"/> - <xs:element name="TtlTaxAmt" type="ActiveOrHistoricCurrencyAndAmount" minOccurs="0"/> - <xs:element name="Dt" type="ISODate" minOccurs="0"/> - <xs:element name="SeqNb" type="Number" minOccurs="0"/> - <xs:element name="Rcrd" type="TaxRecord1" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TaxParty1"> - <xs:sequence> - <xs:element name="TaxId" type="Max35Text" minOccurs="0"/> - <xs:element name="RegnId" type="Max35Text" minOccurs="0"/> - <xs:element name="TaxTp" type="Max35Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TaxParty2"> - <xs:sequence> - <xs:element name="TaxId" type="Max35Text" minOccurs="0"/> - <xs:element name="RegnId" type="Max35Text" minOccurs="0"/> - <xs:element name="TaxTp" type="Max35Text" minOccurs="0"/> - <xs:element name="Authstn" type="TaxAuthorisation1" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TaxPeriod1"> - <xs:sequence> - <xs:element name="Yr" type="ISODate" minOccurs="0"/> - <xs:element name="Tp" type="TaxRecordPeriod1Code" minOccurs="0"/> - <xs:element name="FrToDt" type="DatePeriodDetails" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TaxRecord1"> - <xs:sequence> - <xs:element name="Tp" type="Max35Text" minOccurs="0"/> - <xs:element name="Ctgy" type="Max35Text" minOccurs="0"/> - <xs:element name="CtgyDtls" type="Max35Text" minOccurs="0"/> - <xs:element name="DbtrSts" type="Max35Text" minOccurs="0"/> - <xs:element name="CertId" type="Max35Text" minOccurs="0"/> - <xs:element name="FrmsCd" type="Max35Text" minOccurs="0"/> - <xs:element name="Prd" type="TaxPeriod1" minOccurs="0"/> - <xs:element name="TaxAmt" type="TaxAmount1" minOccurs="0"/> - <xs:element name="AddtlInf" type="Max140Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TaxRecordDetails1"> - <xs:sequence> - <xs:element name="Prd" type="TaxPeriod1" minOccurs="0"/> - <xs:element name="Amt" type="ActiveOrHistoricCurrencyAndAmount"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="TaxRecordPeriod1Code"> - <xs:restriction base="xs:string"> - <xs:enumeration value="MM01"/> - <xs:enumeration value="MM02"/> - <xs:enumeration value="MM03"/> - <xs:enumeration value="MM04"/> - <xs:enumeration value="MM05"/> - <xs:enumeration value="MM06"/> - <xs:enumeration value="MM07"/> - <xs:enumeration value="MM08"/> - <xs:enumeration value="MM09"/> - <xs:enumeration value="MM10"/> - <xs:enumeration value="MM11"/> - <xs:enumeration value="MM12"/> - <xs:enumeration value="QTR1"/> - <xs:enumeration value="QTR2"/> - <xs:enumeration value="QTR3"/> - <xs:enumeration value="QTR4"/> - <xs:enumeration value="HLF1"/> - <xs:enumeration value="HLF2"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="TechnicalInputChannel1Choice"> - <xs:sequence> - <xs:choice> - <xs:element name="Cd" type="ExternalTechnicalInputChannel1Code"/> - <xs:element name="Prtry" type="Max35Text"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TotalTransactions2"> - <xs:sequence> - <xs:element name="TtlNtries" type="NumberAndSumOfTransactions2" minOccurs="0"/> - <xs:element name="TtlCdtNtries" type="NumberAndSumOfTransactions1" minOccurs="0"/> - <xs:element name="TtlDbtNtries" type="NumberAndSumOfTransactions1" minOccurs="0"/> - <xs:element name="TtlNtriesPerBkTxCd" type="TotalsPerBankTransactionCode2" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TotalsPerBankTransactionCode2"> - <xs:sequence> - <xs:element name="NbOfNtries" type="Max15NumericText" minOccurs="0"/> - <xs:element name="Sum" type="DecimalNumber" minOccurs="0"/> - <xs:element name="TtlNetNtryAmt" type="DecimalNumber" minOccurs="0"/> - <xs:element name="CdtDbtInd" type="CreditDebitCode" minOccurs="0"/> - <xs:element name="FcstInd" type="TrueFalseIndicator" minOccurs="0"/> - <xs:element name="BkTxCd" type="BankTransactionCodeStructure4"/> - <xs:element name="Avlbty" type="CashBalanceAvailability2" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TransactionAgents2"> - <xs:sequence> - <xs:element name="DbtrAgt" type="BranchAndFinancialInstitutionIdentification4" minOccurs="0"/> - <xs:element name="CdtrAgt" type="BranchAndFinancialInstitutionIdentification4" minOccurs="0"/> - <xs:element name="IntrmyAgt1" type="BranchAndFinancialInstitutionIdentification4" minOccurs="0"/> - <xs:element name="IntrmyAgt2" type="BranchAndFinancialInstitutionIdentification4" minOccurs="0"/> - <xs:element name="IntrmyAgt3" type="BranchAndFinancialInstitutionIdentification4" minOccurs="0"/> - <xs:element name="RcvgAgt" type="BranchAndFinancialInstitutionIdentification4" minOccurs="0"/> - <xs:element name="DlvrgAgt" type="BranchAndFinancialInstitutionIdentification4" minOccurs="0"/> - <xs:element name="IssgAgt" type="BranchAndFinancialInstitutionIdentification4" minOccurs="0"/> - <xs:element name="SttlmPlc" type="BranchAndFinancialInstitutionIdentification4" minOccurs="0"/> - <xs:element name="Prtry" type="ProprietaryAgent2" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TransactionDates2"> - <xs:sequence> - <xs:element name="AccptncDtTm" type="ISODateTime" minOccurs="0"/> - <xs:element name="TradActvtyCtrctlSttlmDt" type="ISODate" minOccurs="0"/> - <xs:element name="TradDt" type="ISODate" minOccurs="0"/> - <xs:element name="IntrBkSttlmDt" type="ISODate" minOccurs="0"/> - <xs:element name="StartDt" type="ISODate" minOccurs="0"/> - <xs:element name="EndDt" type="ISODate" minOccurs="0"/> - <xs:element name="TxDtTm" type="ISODateTime" minOccurs="0"/> - <xs:element name="Prtry" type="ProprietaryDate2" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TransactionInterest2"> - <xs:sequence> - <xs:element name="Amt" type="ActiveOrHistoricCurrencyAndAmount"/> - <xs:element name="CdtDbtInd" type="CreditDebitCode"/> - <xs:element name="Tp" type="InterestType1Choice" minOccurs="0"/> - <xs:element name="Rate" type="Rate3" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="FrToDt" type="DateTimePeriodDetails" minOccurs="0"/> - <xs:element name="Rsn" type="Max35Text" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TransactionParty2"> - <xs:sequence> - <xs:element name="InitgPty" type="PartyIdentification32" minOccurs="0"/> - <xs:element name="Dbtr" type="PartyIdentification32" minOccurs="0"/> - <xs:element name="DbtrAcct" type="CashAccount16" minOccurs="0"/> - <xs:element name="UltmtDbtr" type="PartyIdentification32" minOccurs="0"/> - <xs:element name="Cdtr" type="PartyIdentification32" minOccurs="0"/> - <xs:element name="CdtrAcct" type="CashAccount16" minOccurs="0"/> - <xs:element name="UltmtCdtr" type="PartyIdentification32" minOccurs="0"/> - <xs:element name="TradgPty" type="PartyIdentification32" minOccurs="0"/> - <xs:element name="Prtry" type="ProprietaryParty2" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TransactionPrice2Choice"> - <xs:sequence> - <xs:choice> - <xs:element name="DealPric" type="ActiveOrHistoricCurrencyAndAmount"/> - <xs:element name="Prtry" type="ProprietaryPrice2" maxOccurs="unbounded"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TransactionQuantities1Choice"> - <xs:sequence> - <xs:choice> - <xs:element name="Qty" type="FinancialInstrumentQuantityChoice"/> - <xs:element name="Prtry" type="ProprietaryQuantity1"/> - </xs:choice> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TransactionReferences2"> - <xs:sequence> - <xs:element name="MsgId" type="Max35Text" minOccurs="0"/> - <xs:element name="AcctSvcrRef" type="Max35Text" minOccurs="0"/> - <xs:element name="PmtInfId" type="Max35Text" minOccurs="0"/> - <xs:element name="InstrId" type="Max35Text" minOccurs="0"/> - <xs:element name="EndToEndId" type="Max35Text" minOccurs="0"/> - <xs:element name="TxId" type="Max35Text" minOccurs="0"/> - <xs:element name="MndtId" type="Max35Text" minOccurs="0"/> - <xs:element name="ChqNb" type="Max35Text" minOccurs="0"/> - <xs:element name="ClrSysRef" type="Max35Text" minOccurs="0"/> - <xs:element name="Prtry" type="ProprietaryReference1" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="TrueFalseIndicator"> - <xs:restriction base="xs:boolean"/> - </xs:simpleType> - <xs:simpleType name="YesNoIndicator"> - <xs:restriction base="xs:boolean"/> - </xs:simpleType> -</xs:schema> diff --git a/libeufin-common/src/main/resources/xsd/ebics_H004.xsd b/libeufin-common/src/main/resources/xsd/ebics_H004.xsd @@ -1,11 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<schema xmlns="http://www.w3.org/2001/XMLSchema" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:ebics="urn:org:ebics:H004" targetNamespace="urn:org:ebics:H004" elementFormDefault="qualified" attributeFormDefault="unqualified" version="1.0"> - <annotation> - <documentation xml:lang="de">ebics_H004.xsd inkludiert alle Schemadateien des EBICS-Protokolls, um die Eindeutigkeit von Element- und Typnamen im EBCIS Namespace zu erzwingen.</documentation> - <documentation xml:lang="en">ebics_H004.xsd includes all schema files for the EBICS protocol in order to enforce unique element and type names in the EBICS namespace.</documentation> - </annotation> - <include schemaLocation="ebics_request_H004.xsd"/> - <include schemaLocation="ebics_response_H004.xsd"/> - <include schemaLocation="ebics_keymgmt_request_H004.xsd"/> - <include schemaLocation="ebics_keymgmt_response_H004.xsd"/> -</schema> diff --git a/libeufin-common/src/main/resources/xsd/ebics_H005.xsd b/libeufin-common/src/main/resources/xsd/ebics_H005.xsd @@ -1,11 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<schema xmlns="http://www.w3.org/2001/XMLSchema" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:ebics="urn:org:ebics:H005" targetNamespace="urn:org:ebics:H005" elementFormDefault="qualified" attributeFormDefault="unqualified" version="1.0"> - <annotation> - <documentation xml:lang="de">ebics_H005.xsd inkludiert alle Schemadateien des EBICS-Protokolls, um die Eindeutigkeit von Element- und Typnamen im EBCIS Namespace zu erzwingen.</documentation> - <documentation xml:lang="en">ebics_H005.xsd includes all schema files for the EBICS protocol in order to enforce unique element and type names in the EBICS namespace.</documentation> - </annotation> - <include schemaLocation="ebics_request_H005.xsd"/> - <include schemaLocation="ebics_response_H005.xsd"/> - <include schemaLocation="ebics_keymgmt_request_H005.xsd"/> - <include schemaLocation="ebics_keymgmt_response_H005.xsd"/> -</schema> diff --git a/libeufin-common/src/main/resources/xsd/ebics_hev.xsd b/libeufin-common/src/main/resources/xsd/ebics_hev.xsd @@ -1,135 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:ebics="http://www.ebics.org/H000" targetNamespace="http://www.ebics.org/H000" elementFormDefault="qualified" attributeFormDefault="unqualified" version="1.0"> - <xs:annotation> - <xs:documentation xml:lang="de">ebics_hev.xsd ist das EBICS-Protokollschema entweder für Anfragen oder Rückmeldungen der Bank zu unterstützten EBICS-Versionen.</xs:documentation> - <xs:documentation xml:lang="en">ebics_hev.xsd is the appropriate EBICS protocol schema either for requests or responses according the EBICS versions supported by a bank.</xs:documentation> - </xs:annotation> - <xs:simpleType name="HostIDType"> - <xs:annotation> - <xs:documentation xml:lang="de">Datentyp für die Host-ID.</xs:documentation> - <xs:documentation xml:lang="en">Dataype for Host-ID.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:token"> - <xs:maxLength value="35"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="OrderTBaseType"> - <xs:annotation> - <xs:documentation xml:lang="de">Datentyp für allgemeine Auftragsarten (Grundtyp).</xs:documentation> - <xs:documentation xml:lang="en">Datatype for general order types (basic type).</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:token"> - <xs:length value="3"/> - <xs:pattern value="HEV"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ReturnCodeType"> - <xs:annotation> - <xs:documentation xml:lang="de">Datentyp für Antwortcodes.</xs:documentation> - <xs:documentation xml:lang="en">Datatype for the return code</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:token"> - <xs:length value="6"/> - <xs:pattern value="\d{6}"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ReportTextType"> - <xs:annotation> - <xs:documentation xml:lang="de">Datentyp für den Erklärungstext zum Antwortcode.</xs:documentation> - <xs:documentation xml:lang="en">Datatype for report text with respect to the return code</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:normalizedString"> - <xs:maxLength value="256"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="VersionNumberType"> - <xs:annotation> - <xs:documentation xml:lang="de">Datentyp für eine Versionsnummer</xs:documentation> - <xs:documentation xml:lang="en">Datatype for a release number </xs:documentation> - </xs:annotation> - <xs:restriction base="xs:token"> - <xs:length value="5"/> - <xs:pattern value="[0-9]{2}[.][0-9]{2}"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ProtocolVersionType"> - <xs:annotation> - <xs:documentation xml:lang="de">Datentyp für Versionsnummer des EBICS-schemas</xs:documentation> - <xs:documentation xml:lang="en">Datatype for release-number of the EBICS scheme</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:token"> - <xs:length value="4"/> - <xs:pattern value="H\d{3}"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="SystemReturnCodeType"> - <xs:annotation> - <xs:documentation xml:lang="de">Datentyp für technische Fehler.</xs:documentation> - <xs:documentation xml:lang="en">Datatype for technical error</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="ReturnCode" type="ebics:ReturnCodeType"> - <xs:annotation> - <xs:documentation xml:lang="de">Rückmeldung des Ausführungsstatus mit einer eindeutigen Fehlernummer.</xs:documentation> - <xs:documentation xml:lang="en">Confirmation of the carried out status with a unique error code.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ReportText" type="ebics:ReportTextType"> - <xs:annotation> - <xs:documentation xml:lang="de">Klartext der Rückmeldung des Ausführungsstatus.</xs:documentation> - <xs:documentation xml:lang="en">Clear text of the response (carried out status).</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="HEVRequestDataType"> - <xs:annotation> - <xs:documentation xml:lang="de">Datentyp für die Request-Daten</xs:documentation> - <xs:documentation xml:lang="en">Data type for Request data</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="HostID" type="ebics:HostIDType"/> - <xs:any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="HEVResponseDataType"> - <xs:annotation> - <xs:documentation xml:lang="en">Datentyp für die Response-Daten</xs:documentation> - <xs:documentation xml:lang="en">Data type for Request data</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="SystemReturnCode" type="ebics:SystemReturnCodeType"/> - <xs:element name="VersionNumber" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation xml:lang="de">Von der Bank unterstützte EBICS-Versionen, z.B. 2.4</xs:documentation> - <xs:documentation xml:lang="en">EBICS-releases supported by the bank, e.g. 2.4</xs:documentation> - </xs:annotation> - <xs:complexType> - <xs:simpleContent> - <xs:extension base="ebics:VersionNumberType"> - <xs:attribute name="ProtocolVersion" type="ebics:ProtocolVersionType" use="required"> - <xs:annotation> - <xs:documentation xml:lang="de">der EBICS-Version eindeutig zugeordnete Schema-Version, z.B. H003</xs:documentation> - <xs:documentation xml:lang="en">EBICS-scheme-version, e.g. H003, well-defined for EBICS-release-Version</xs:documentation> - </xs:annotation> - </xs:attribute> - </xs:extension> - </xs:simpleContent> - </xs:complexType> - </xs:element> - <xs:any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:element name="ebicsHEVRequest" type="ebics:HEVRequestDataType"> - <xs:annotation> - <xs:documentation xml:lang="de">Requestdaten</xs:documentation> - <xs:documentation xml:lang="en">request data</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ebicsHEVResponse" type="ebics:HEVResponseDataType"> - <xs:annotation> - <xs:documentation xml:lang="de">Responsedaten</xs:documentation> - <xs:documentation xml:lang="en">response data</xs:documentation> - </xs:annotation> - </xs:element> -</xs:schema> diff --git a/libeufin-common/src/main/resources/xsd/ebics_keymgmt_request_H004.xsd b/libeufin-common/src/main/resources/xsd/ebics_keymgmt_request_H004.xsd @@ -1,543 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- Mit XMLSpy v2008 rel. 2 sp2 (http://www.altova.com) von benutzerservice benutzerservice (SIZ GmbH) bearbeitet --> -<schema xmlns="http://www.w3.org/2001/XMLSchema" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:ebics="urn:org:ebics:H004" targetNamespace="urn:org:ebics:H004" elementFormDefault="qualified" attributeFormDefault="unqualified" version="1.0"> - <annotation> - <documentation xml:lang="de">ebics_keymgmt_request_H004.xsd ist das EBICS-Protokollschema für Schlüsselmanagement-Anfragen (HIA, HPB, HSA, INI).</documentation> - </annotation> - <import namespace="http://www.w3.org/2000/09/xmldsig#" schemaLocation="xmldsig-core-schema.xsd"> - <annotation> - <documentation xml:lang="de">XML-Signature.</documentation> - </annotation> - </import> - <include schemaLocation="ebics_types_H004.xsd"/> - <include schemaLocation="ebics_orders_H004.xsd"/> - <complexType name="StaticHeaderBaseType" abstract="true"> - <annotation> - <documentation xml:lang="de">Datentyp für den statischen EBICS-Header (allgemein).</documentation> - </annotation> - <sequence> - <element name="HostID" type="ebics:HostIDType"> - <annotation> - <documentation xml:lang="de">Hostname des Banksystems.</documentation> - </annotation> - </element> - <element name="Nonce" type="ebics:NonceType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Zufallswert; damit wird die Initialisierungsnachricht des Clients einzigartig; nur anzugeben, falls Authentifikationssignatur vorhanden.</documentation> - </annotation> - </element> - <element name="Timestamp" type="ebics:TimestampType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">aktueller Zeitstempel zur Begrenzung der serverseitigen Nonce-Speicherung; nur anzugeben, falls Authentifikationssignatur vorhanden.</documentation> - </annotation> - </element> - <element name="PartnerID" type="ebics:PartnerIDType"> - <annotation> - <documentation xml:lang="de">Kunden-ID des serverseitig administrierten Kunden.</documentation> - </annotation> - </element> - <element name="UserID" type="ebics:UserIDType"> - <annotation> - <documentation xml:lang="de">Teilnehmer-ID des serverseitig zu diesem Kunden administrierten Teilnehmers.</documentation> - </annotation> - </element> - <element name="SystemID" type="ebics:UserIDType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">technische User-ID für Multi-User-Systeme.</documentation> - </annotation> - </element> - <element name="Product" type="ebics:ProductElementType" nillable="true" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Kennung des Kundenprodukts bzw. Herstellerkennung oder Name.</documentation> - </annotation> - </element> - <element name="OrderDetails" type="ebics:OrderDetailsType"> - <annotation> - <documentation xml:lang="de">Auftragsdetails.</documentation> - </annotation> - </element> - <element name="SecurityMedium" type="ebics:SecurityMediumType"> - <annotation> - <documentation xml:lang="de">Angabe des Sicherheitsmediums, das der Kunde verwendet.</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="OrderDetailsType" abstract="true"> - <annotation> - <documentation xml:lang="de">Datentyp für OrderDetails im statischen EBICS-Header (allgemein).</documentation> - </annotation> - <sequence> - <element name="OrderType" type="ebics:OrderTBaseType"> - <annotation> - <documentation xml:lang="de">Auftragsart.</documentation> - </annotation> - </element> - <element name="OrderAttribute" type="ebics:OrderAttributeBaseType"> - <annotation> - <documentation xml:lang="de">Auftragsattribut.</documentation> - </annotation> - </element> - </sequence> - </complexType> - <complexType name="ProductElementType"> - <annotation> - <documentation xml:lang="de">Datentyp für Element mit Kennung des Kundenprodukts bzw. Herstellerkennung oder Name.</documentation> - </annotation> - <simpleContent> - <extension base="ebics:ProductType"> - <attribute name="Language" type="ebics:LanguageType" use="required"> - <annotation> - <documentation xml:lang="de">Sprachkennzeichen der Kundenproduktversion (gemäß ISO 639).</documentation> - </annotation> - </attribute> - <attribute name="InstituteID" type="ebics:InstituteIDType" use="optional"> - <annotation> - <documentation xml:lang="de">Kennung des Herausgebers des Kundenprodukts bzw. des betreuenden Kreditinstituts.</documentation> - </annotation> - </attribute> - </extension> - </simpleContent> - </complexType> - <complexType name="EmptyMutableHeaderType"> - <annotation> - <documentation xml:lang="de">Datentyp für den leeren variablen EBICS-Header von Key Managemen Aufträgen.</documentation> - </annotation> - <sequence> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <element name="ebicsUnsecuredRequest"> - <annotation> - <documentation>Anfragestruktur für ungesicherte Auftragsarten HIA (Authentifikations- und Verschlüsselungsschlüssel senden) und INI (bankfachllichen Schlüssel senden).</documentation> - </annotation> - <complexType> - <sequence> - <element name="header"> - <annotation> - <documentation xml:lang="de">enthält die technischen Transaktionsdaten.</documentation> - </annotation> - <complexType> - <sequence> - <element name="static" type="ebics:UnsecuredRequestStaticHeaderType"> - <annotation> - <documentation xml:lang="de">enhält alle festen Headereinträge.</documentation> - </annotation> - </element> - <element name="mutable" type="ebics:EmptyMutableHeaderType"> - <annotation> - <documentation xml:lang="de">enthält alle variablen Headereinträge.</documentation> - </annotation> - </element> - </sequence> - <attributeGroup ref="ebics:AuthenticationMarker"/> - </complexType> - </element> - <element name="body"> - <annotation> - <documentation xml:lang="de">enthält die Auftragsdaten.</documentation> - </annotation> - <complexType> - <sequence> - <annotation> - <documentation xml:lang="de"/> - </annotation> - <element name="DataTransfer"> - <annotation> - <documentation xml:lang="de">Transfer von Auftragsdaten.</documentation> - </annotation> - <complexType> - <sequence> - <element name="OrderData"> - <annotation> - <documentation xml:lang="de">enthält Auftragsdaten.</documentation> - </annotation> - <complexType> - <simpleContent> - <extension base="ebics:OrderDataType"> - <anyAttribute namespace="##targetNamespace" processContents="lax"/> - </extension> - </simpleContent> - </complexType> - </element> - </sequence> - </complexType> - </element> - </sequence> - </complexType> - </element> - </sequence> - <attributeGroup ref="ebics:VersionAttrGroup"/> - </complexType> - </element> - <complexType name="UnsecuredRequestStaticHeaderType"> - <annotation> - <documentation xml:lang="de">Datentyp für den statischen EBICS-Header bei ungesicherten Sendeauftragsarten (Aufträge HIA und INI): kein Nonce, kein Timestamp, keine EU-Datei, keine X001 Authentifizierung, keine Verschlüsselung, keine Digests der öffentlichen Bankschlüssel, Nutzdaten komprimiert, Auftragsattribut DZNNN</documentation> - </annotation> - <complexContent> - <restriction base="ebics:StaticHeaderBaseType"> - <sequence> - <element name="HostID" type="ebics:HostIDType"> - <annotation> - <documentation xml:lang="de">Hostname des Banksystems.</documentation> - </annotation> - </element> - <element name="Nonce" type="ebics:NonceType" minOccurs="0" maxOccurs="0"> - <annotation> - <documentation xml:lang="de">Zufallswert; damit wird die Initialisierungsnachricht des Clients einzigartig; nicht anzugeben für ebicsUnsecuredRequest.</documentation> - </annotation> - </element> - <element name="Timestamp" type="ebics:TimestampType" minOccurs="0" maxOccurs="0"> - <annotation> - <documentation xml:lang="de">aktueller Zeitstempel zur Begrenzung der serverseitigen Nonce-Speicherung; nicht anzugeben für ebicsUnsecuredRequest.</documentation> - </annotation> - </element> - <element name="PartnerID" type="ebics:PartnerIDType"> - <annotation> - <documentation xml:lang="de">Kunden-ID des serverseitig administrierten Kunden.</documentation> - </annotation> - </element> - <element name="UserID" type="ebics:UserIDType"> - <annotation> - <documentation xml:lang="de">Teilnehmer-ID des serverseitig zu diesem Kunden administrierten Teilnehmers.</documentation> - </annotation> - </element> - <element name="SystemID" type="ebics:UserIDType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">technische User-ID für Multi-User-Systeme.</documentation> - </annotation> - </element> - <element name="Product" type="ebics:ProductElementType" nillable="true" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Kennung des Kundenprodukts bzw. Herstellerkennung oder Name.</documentation> - </annotation> - </element> - <element name="OrderDetails" type="ebics:UnsecuredReqOrderDetailsType"> - <annotation> - <documentation xml:lang="de">Auftragsdetails.</documentation> - </annotation> - </element> - <element name="SecurityMedium" type="ebics:SecurityMediumType"> - <annotation> - <documentation xml:lang="de">Angabe des Sicherheitsmediums, das der Kunde verwendet.</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </restriction> - </complexContent> - </complexType> - <complexType name="UnsecuredReqOrderDetailsType"> - <annotation> - <documentation xml:lang="de">Datentyp für OrderDetails im statischen EBICS-Header von ebicsUnsecuredRequest.</documentation> - </annotation> - <complexContent> - <restriction base="ebics:OrderDetailsType"> - <sequence> - <element name="OrderType" type="ebics:OrderTBaseType"> - <annotation> - <documentation xml:lang="de">Auftragsart.</documentation> - </annotation> - </element> - <element name="OrderAttribute" type="ebics:OrderAttributeBaseType" fixed="DZNNN"> - <annotation> - <documentation xml:lang="de">Auftragsattribut: DZNNN.</documentation> - </annotation> - </element> - </sequence> - </restriction> - </complexContent> - </complexType> - <element name="ebicsNoPubKeyDigestsRequest"> - <annotation> - <documentation>Anfragestruktur für Auftragsarten ohne Übertragung der Digests der öffentlichen Bankschlüssel (HPB Bankschlüssel abholen).</documentation> - </annotation> - <complexType> - <sequence> - <element name="header"> - <annotation> - <documentation xml:lang="de">enthält die technischen Transaktionsdaten.</documentation> - </annotation> - <complexType> - <sequence> - <element name="static" type="ebics:NoPubKeyDigestsRequestStaticHeaderType"> - <annotation> - <documentation xml:lang="de">enhält alle festen Headereinträge.</documentation> - </annotation> - </element> - <element name="mutable" type="ebics:EmptyMutableHeaderType"> - <annotation> - <documentation xml:lang="de">enthält alle variablen Headereinträge.</documentation> - </annotation> - </element> - </sequence> - <attributeGroup ref="ebics:AuthenticationMarker"/> - </complexType> - </element> - <element ref="ebics:AuthSignature"> - <annotation> - <documentation xml:lang="de">Authentifikationssignatur.</documentation> - </annotation> - </element> - <element name="body"> - <annotation> - <documentation xml:lang="de">enthält optionale Zertifikate (vorgesehen).</documentation> - </annotation> - <complexType> - <sequence> - <annotation> - <documentation xml:lang="de"/> - </annotation> - <element ref="ds:X509Data" minOccurs="0" maxOccurs="0"> - <annotation> - <documentation xml:lang="de">X.509-Daten des Teilnehmers.</documentation> - </annotation> - </element> - </sequence> - </complexType> - </element> - </sequence> - <attributeGroup ref="ebics:VersionAttrGroup"/> - </complexType> - </element> - <complexType name="NoPubKeyDigestsRequestStaticHeaderType"> - <annotation> - <documentation xml:lang="de">Datentyp für den statischen EBICS-Header bei Aufträgen ohne Übertragung der Digests der Bankschlüssel (Auftrag HBP): keine Digests der öffentlichen Bankschlüssel, keine EU-Datei, keine Nutzdaten, OrderId optional!, Nonce, Timestamp, X001 Authentifizierung, Auftragsattribut DZHNN</documentation> - </annotation> - <complexContent> - <restriction base="ebics:StaticHeaderBaseType"> - <sequence> - <element name="HostID" type="ebics:HostIDType"> - <annotation> - <documentation xml:lang="de">Hostname des Banksystems.</documentation> - </annotation> - </element> - <element name="Nonce" type="ebics:NonceType"> - <annotation> - <documentation xml:lang="de">Zufallswert; damit wird die Initialisierungsnachricht des Clients einzigartig.</documentation> - </annotation> - </element> - <element name="Timestamp" type="ebics:TimestampType"> - <annotation> - <documentation xml:lang="de">aktueller Zeitstempel zur Begrenzung der serverseitigen Nonce-Speicherung.</documentation> - </annotation> - </element> - <element name="PartnerID" type="ebics:PartnerIDType"> - <annotation> - <documentation xml:lang="de">Kunden-ID des serverseitig administrierten Kunden.</documentation> - </annotation> - </element> - <element name="UserID" type="ebics:UserIDType"> - <annotation> - <documentation xml:lang="de">Teilnehmer-ID des serverseitig zu diesem Kunden administrierten Teilnehmers.</documentation> - </annotation> - </element> - <element name="SystemID" type="ebics:UserIDType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">technische User-ID für Multi-User-Systeme.</documentation> - </annotation> - </element> - <element name="Product" type="ebics:ProductElementType" nillable="true" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Kennung des Kundenprodukts bzw. Herstellerkennung oder Name.</documentation> - </annotation> - </element> - <element name="OrderDetails" type="ebics:NoPubKeyDigestsReqOrderDetailsType"> - <annotation> - <documentation xml:lang="de">Auftragsdetails.</documentation> - </annotation> - </element> - <element name="SecurityMedium" type="ebics:SecurityMediumType"> - <annotation> - <documentation xml:lang="de">Angabe des Sicherheitsmediums, das der Kunde verwendet.</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </restriction> - </complexContent> - </complexType> - <complexType name="NoPubKeyDigestsReqOrderDetailsType"> - <annotation> - <documentation xml:lang="de">Datentyp für OrderDetails im statischen EBICS-Header von ebicsNoPubKeyDigestsRequest.</documentation> - </annotation> - <complexContent> - <restriction base="ebics:OrderDetailsType"> - <sequence> - <element name="OrderType" type="ebics:OrderTBaseType"> - <annotation> - <documentation xml:lang="de">Auftragsart.</documentation> - </annotation> - </element> - <element name="OrderAttribute" type="ebics:OrderAttributeBaseType" fixed="DZHNN"> - <annotation> - <documentation xml:lang="de">Auftragsattribut: DZHNN.</documentation> - </annotation> - </element> - </sequence> - </restriction> - </complexContent> - </complexType> - <element name="ebicsUnsignedRequest"> - <annotation> - <documentation xml:lang="en">The structure for uploads contains order data and the ESs, but without an authentication signature and data digest of bank keys.</documentation> - <documentation>Anfragestruktur für Sendeaufträge mit EU-Datei und Nutzdaten aber ohne Authentifizierungssignatur und Digests der Bankschlüssel.</documentation> - </annotation> - <complexType> - <sequence> - <element name="header"> - <annotation> - <documentation xml:lang="en">Contains technical transaction data.</documentation> - <documentation xml:lang="de">enthält die technischen Transaktionsdaten.</documentation> - </annotation> - <complexType> - <sequence> - <element name="static" type="ebics:UnsignedRequestStaticHeaderType"> - <annotation> - <documentation xml:lang="en">Contains all fixed header entries.</documentation> - <documentation xml:lang="de">enhält alle festen Headereinträge.</documentation> - </annotation> - </element> - <element name="mutable" type="ebics:EmptyMutableHeaderType"> - <annotation> - <documentation xml:lang="en">Contains all mutable header entries.</documentation> - <documentation xml:lang="de">enthält alle variablen Headereinträge.</documentation> - </annotation> - </element> - </sequence> - <attributeGroup ref="ebics:AuthenticationMarker"/> - </complexType> - </element> - <element name="body"> - <annotation> - <documentation xml:lang="en">Contains the order data and the ESs.</documentation> - <documentation xml:lang="de">enthält die Auftragsdaten und EUs.</documentation> - </annotation> - <complexType> - <sequence> - <annotation> - <documentation xml:lang="de"/> - </annotation> - <element name="DataTransfer"> - <annotation> - <documentation xml:lang="en">Transfer of order data and the ESs.</documentation> - <documentation xml:lang="de">Transfer von Auftragsdaten und EUs.</documentation> - </annotation> - <complexType> - <sequence> - <element name="SignatureData"> - <annotation> - <documentation xml:lang="en">Contains the ESs.</documentation> - <documentation xml:lang="de">enthält Signaturdaten (EUs).</documentation> - </annotation> - <complexType> - <simpleContent> - <extension base="ebics:SignatureDataType"> - <attributeGroup ref="ebics:AuthenticationMarker"/> - </extension> - </simpleContent> - </complexType> - </element> - <element name="OrderData"> - <annotation> - <documentation xml:lang="en">Contains the order data</documentation> - <documentation xml:lang="de">enthält Auftragsdaten.</documentation> - </annotation> - <complexType> - <simpleContent> - <extension base="ebics:OrderDataType"> - <anyAttribute namespace="##targetNamespace" processContents="lax"/> - </extension> - </simpleContent> - </complexType> - </element> - </sequence> - </complexType> - </element> - </sequence> - </complexType> - </element> - </sequence> - <attributeGroup ref="ebics:VersionAttrGroup"/> - </complexType> - </element> - <complexType name="UnsignedRequestStaticHeaderType"> - <annotation> - <documentation xml:lang="de">Datentyp für den statischen EBICS-Header für ebicsUnsignedRequest.Datentyp für den statischen EBICS-Header bei Aufträgen ohne Authentifizierungssignatur (Auftrag HSA): keine X001 Authentifizierung, keine Digests der öffentlichen Bankschlüssel, EU-Datei, Nutzdaten, Nonce, Timestamp, OrderId, Auftragsattribut OZNNN</documentation> - </annotation> - <complexContent> - <restriction base="ebics:StaticHeaderBaseType"> - <sequence> - <element name="HostID" type="ebics:HostIDType"> - <annotation> - <documentation xml:lang="de">Hostname des Banksystems.</documentation> - </annotation> - </element> - <element name="Nonce" type="ebics:NonceType" minOccurs="0" maxOccurs="0"> - <annotation> - <documentation xml:lang="de">Zufallswert; damit wird die Initialisierungsnachricht des Clients einzigartig; nicht anzugeben bei ebicsUnsignedRequest.</documentation> - </annotation> - </element> - <element name="Timestamp" type="ebics:TimestampType" minOccurs="0" maxOccurs="0"> - <annotation> - <documentation xml:lang="de">aktueller Zeitstempel zur Begrenzung der serverseitigen Nonce-Speicherung; nicht anzugeben bei ebicsUnsignedRequest.</documentation> - </annotation> - </element> - <element name="PartnerID" type="ebics:PartnerIDType"> - <annotation> - <documentation xml:lang="de">Kunden-ID des serverseitig administrierten Kunden.</documentation> - </annotation> - </element> - <element name="UserID" type="ebics:UserIDType"> - <annotation> - <documentation xml:lang="de">Teilnehmer-ID des serverseitig zu diesem Kunden administrierten Teilnehmers.</documentation> - </annotation> - </element> - <element name="SystemID" type="ebics:UserIDType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">technische User-ID für Multi-User-Systeme.</documentation> - </annotation> - </element> - <element name="Product" type="ebics:ProductElementType" nillable="true" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Kennung des Kundenprodukts bzw. Herstellerkennung oder Name.</documentation> - </annotation> - </element> - <element name="OrderDetails" type="ebics:UnsignedReqOrderDetailsType"> - <annotation> - <documentation xml:lang="de">Auftragsdetails.</documentation> - </annotation> - </element> - <element name="SecurityMedium" type="ebics:SecurityMediumType"> - <annotation> - <documentation xml:lang="de">Angabe des Sicherheitsmediums, das der Kunde verwendet.</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </restriction> - </complexContent> - </complexType> - <complexType name="UnsignedReqOrderDetailsType"> - <annotation> - <documentation xml:lang="de">Datentyp für OrderDetails im statischen EBICS-Header von ebicsUnsignedRequest.</documentation> - </annotation> - <complexContent> - <restriction base="ebics:OrderDetailsType"> - <sequence> - <element name="OrderType" type="ebics:OrderTBaseType"> - <annotation> - <documentation xml:lang="de">Auftragsart.</documentation> - </annotation> - </element> - <element name="OrderAttribute" type="ebics:OrderAttributeBaseType" fixed="OZNNN"> - <annotation> - <documentation xml:lang="de">Auftragsattribut: OZNNN.</documentation> - </annotation> - </element> - </sequence> - </restriction> - </complexContent> - </complexType> -</schema> diff --git a/libeufin-common/src/main/resources/xsd/ebics_keymgmt_request_H005.xsd b/libeufin-common/src/main/resources/xsd/ebics_keymgmt_request_H005.xsd @@ -1,523 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- edited with XMLSpy v2016 sp1 (x64) (http://www.altova.com) by EBICS Working Group - October 2016 --> -<schema xmlns="http://www.w3.org/2001/XMLSchema" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:ebics="urn:org:ebics:H005" targetNamespace="urn:org:ebics:H005" elementFormDefault="qualified" attributeFormDefault="unqualified" version="1.0"> - <annotation> - <documentation xml:lang="de">ebics_keymgmt_request_H005.xsd ist das EBICS-Protokollschema für Schlüsselmanagement-Anfragen (HIA, HPB, HSA, INI).</documentation> - </annotation> - <import namespace="http://www.w3.org/2000/09/xmldsig#" schemaLocation="xmldsig-core-schema.xsd"> - <annotation> - <documentation xml:lang="de">XML-Signature.</documentation> - </annotation> - </import> - <include schemaLocation="ebics_types_H005.xsd"/> - <include schemaLocation="ebics_orders_H005.xsd"/> - <complexType name="StaticHeaderBaseType" abstract="true"> - <annotation> - <documentation xml:lang="de">Datentyp für den statischen EBICS-Header (allgemein).</documentation> - </annotation> - <sequence> - <element name="HostID" type="ebics:HostIDType"> - <annotation> - <documentation xml:lang="de">Hostname des Banksystems.</documentation> - </annotation> - </element> - <element name="Nonce" type="ebics:NonceType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Zufallswert; damit wird die Initialisierungsnachricht des Clients einzigartig; nur anzugeben, falls Authentifikationssignatur vorhanden.</documentation> - </annotation> - </element> - <element name="Timestamp" type="ebics:TimestampType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">aktueller Zeitstempel zur Begrenzung der serverseitigen Nonce-Speicherung; nur anzugeben, falls Authentifikationssignatur vorhanden.</documentation> - </annotation> - </element> - <element name="PartnerID" type="ebics:PartnerIDType"> - <annotation> - <documentation xml:lang="de">Kunden-ID des serverseitig administrierten Kunden.</documentation> - </annotation> - </element> - <element name="UserID" type="ebics:UserIDType"> - <annotation> - <documentation xml:lang="de">Teilnehmer-ID des serverseitig zu diesem Kunden administrierten Teilnehmers.</documentation> - </annotation> - </element> - <element name="SystemID" type="ebics:UserIDType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">technische User-ID für Multi-User-Systeme.</documentation> - </annotation> - </element> - <element name="Product" type="ebics:ProductElementType" nillable="true" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Kennung des Kundenprodukts bzw. Herstellerkennung oder Name.</documentation> - </annotation> - </element> - <element name="OrderDetails" type="ebics:OrderDetailsType"> - <annotation> - <documentation xml:lang="de">Auftragsdetails.</documentation> - </annotation> - </element> - <element name="SecurityMedium" type="ebics:SecurityMediumType"> - <annotation> - <documentation xml:lang="de">Angabe des Sicherheitsmediums, das der Kunde verwendet.</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="OrderDetailsType" abstract="true"> - <annotation> - <documentation xml:lang="de">Datentyp für OrderDetails im statischen EBICS-Header (allgemein).</documentation> - </annotation> - <sequence> - <element name="AdminOrderType" type="ebics:OrderTBaseType"> - <annotation> - <documentation xml:lang="de">Auftragsart.</documentation> - </annotation> - </element> - </sequence> - </complexType> - <complexType name="ProductElementType"> - <annotation> - <documentation xml:lang="de">Datentyp für Element mit Kennung des Kundenprodukts bzw. Herstellerkennung oder Name.</documentation> - </annotation> - <simpleContent> - <extension base="ebics:ProductType"> - <attribute name="Language" type="ebics:LanguageType" use="required"> - <annotation> - <documentation xml:lang="de">Sprachkennzeichen der Kundenproduktversion (gemäß ISO 639).</documentation> - </annotation> - </attribute> - <attribute name="InstituteID" type="ebics:InstituteIDType" use="optional"> - <annotation> - <documentation xml:lang="de">Kennung des Herausgebers des Kundenprodukts bzw. des betreuenden Kreditinstituts.</documentation> - </annotation> - </attribute> - </extension> - </simpleContent> - </complexType> - <complexType name="EmptyMutableHeaderType"> - <annotation> - <documentation xml:lang="de">Datentyp für den leeren variablen EBICS-Header von Key Managemen Aufträgen.</documentation> - </annotation> - <sequence> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <element name="ebicsUnsecuredRequest"> - <annotation> - <documentation>Anfragestruktur für ungesicherte Auftragsarten HIA (Authentifikations- und Verschlüsselungsschlüssel senden) und INI (bankfachllichen Schlüssel senden).</documentation> - </annotation> - <complexType> - <sequence> - <element name="header"> - <annotation> - <documentation xml:lang="de">enthält die technischen Transaktionsdaten.</documentation> - </annotation> - <complexType> - <sequence> - <element name="static" type="ebics:UnsecuredRequestStaticHeaderType"> - <annotation> - <documentation xml:lang="de">enhält alle festen Headereinträge.</documentation> - </annotation> - </element> - <element name="mutable" type="ebics:EmptyMutableHeaderType"> - <annotation> - <documentation xml:lang="de">enthält alle variablen Headereinträge.</documentation> - </annotation> - </element> - </sequence> - <attributeGroup ref="ebics:AuthenticationMarker"/> - </complexType> - </element> - <element name="body"> - <annotation> - <documentation xml:lang="de">enthält die Auftragsdaten.</documentation> - </annotation> - <complexType> - <sequence> - <annotation> - <documentation xml:lang="de"/> - </annotation> - <element name="DataTransfer"> - <annotation> - <documentation xml:lang="de">Transfer von Auftragsdaten.</documentation> - </annotation> - <complexType> - <sequence> - <element name="OrderData"> - <annotation> - <documentation xml:lang="de">enthält Auftragsdaten.</documentation> - </annotation> - <complexType> - <simpleContent> - <extension base="ebics:OrderDataType"> - <anyAttribute namespace="##targetNamespace" processContents="lax"/> - </extension> - </simpleContent> - </complexType> - </element> - </sequence> - </complexType> - </element> - </sequence> - </complexType> - </element> - </sequence> - <attributeGroup ref="ebics:VersionAttrGroup"/> - </complexType> - </element> - <complexType name="UnsecuredRequestStaticHeaderType"> - <annotation> - <documentation xml:lang="de">Datentyp für den statischen EBICS-Header bei ungesicherten Sendeauftragsarten (Aufträge HIA und INI): kein Nonce, kein Timestamp, keine EU-Datei, keine X001 Authentifizierung, keine Verschlüsselung, keine Digests der öffentlichen Bankschlüssel, Nutzdaten komprimiert</documentation> - </annotation> - <complexContent> - <restriction base="ebics:StaticHeaderBaseType"> - <sequence> - <element name="HostID" type="ebics:HostIDType"> - <annotation> - <documentation xml:lang="de">Hostname des Banksystems.</documentation> - </annotation> - </element> - <element name="Nonce" type="ebics:NonceType" minOccurs="0" maxOccurs="0"> - <annotation> - <documentation xml:lang="de">Zufallswert; damit wird die Initialisierungsnachricht des Clients einzigartig; nicht anzugeben für ebicsUnsecuredRequest.</documentation> - </annotation> - </element> - <element name="Timestamp" type="ebics:TimestampType" minOccurs="0" maxOccurs="0"> - <annotation> - <documentation xml:lang="de">aktueller Zeitstempel zur Begrenzung der serverseitigen Nonce-Speicherung; nicht anzugeben für ebicsUnsecuredRequest.</documentation> - </annotation> - </element> - <element name="PartnerID" type="ebics:PartnerIDType"> - <annotation> - <documentation xml:lang="de">Kunden-ID des serverseitig administrierten Kunden.</documentation> - </annotation> - </element> - <element name="UserID" type="ebics:UserIDType"> - <annotation> - <documentation xml:lang="de">Teilnehmer-ID des serverseitig zu diesem Kunden administrierten Teilnehmers.</documentation> - </annotation> - </element> - <element name="SystemID" type="ebics:UserIDType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">technische User-ID für Multi-User-Systeme.</documentation> - </annotation> - </element> - <element name="Product" type="ebics:ProductElementType" nillable="true" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Kennung des Kundenprodukts bzw. Herstellerkennung oder Name.</documentation> - </annotation> - </element> - <element name="OrderDetails" type="ebics:UnsecuredReqOrderDetailsType"> - <annotation> - <documentation xml:lang="de">Auftragsdetails.</documentation> - </annotation> - </element> - <element name="SecurityMedium" type="ebics:SecurityMediumType"> - <annotation> - <documentation xml:lang="de">Angabe des Sicherheitsmediums, das der Kunde verwendet.</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </restriction> - </complexContent> - </complexType> - <complexType name="UnsecuredReqOrderDetailsType"> - <annotation> - <documentation xml:lang="de">Datentyp für OrderDetails im statischen EBICS-Header von ebicsUnsecuredRequest.</documentation> - </annotation> - <complexContent> - <restriction base="ebics:OrderDetailsType"> - <sequence> - <element name="AdminOrderType" type="ebics:OrderTBaseType"> - <annotation> - <documentation xml:lang="de">Auftragsart.</documentation> - </annotation> - </element> - </sequence> - </restriction> - </complexContent> - </complexType> - <element name="ebicsNoPubKeyDigestsRequest"> - <annotation> - <documentation>Anfragestruktur für Auftragsarten ohne Übertragung der Digests der öffentlichen Bankschlüssel (HPB Bankschlüssel abholen).</documentation> - </annotation> - <complexType> - <sequence> - <element name="header"> - <annotation> - <documentation xml:lang="de">enthält die technischen Transaktionsdaten.</documentation> - </annotation> - <complexType> - <sequence> - <element name="static" type="ebics:NoPubKeyDigestsRequestStaticHeaderType"> - <annotation> - <documentation xml:lang="de">enhält alle festen Headereinträge.</documentation> - </annotation> - </element> - <element name="mutable" type="ebics:EmptyMutableHeaderType"> - <annotation> - <documentation xml:lang="de">enthält alle variablen Headereinträge.</documentation> - </annotation> - </element> - </sequence> - <attributeGroup ref="ebics:AuthenticationMarker"/> - </complexType> - </element> - <element ref="ebics:AuthSignature"> - <annotation> - <documentation xml:lang="de">Authentifikationssignatur.</documentation> - </annotation> - </element> - <element name="body"> - <annotation> - <documentation xml:lang="de">enthält optionale Zertifikate (vorgesehen).</documentation> - </annotation> - <complexType> - <sequence> - <annotation> - <documentation xml:lang="de"/> - </annotation> - <element ref="ds:X509Data" minOccurs="0" maxOccurs="0"> - <annotation> - <documentation xml:lang="de">X.509-Daten des Teilnehmers.</documentation> - </annotation> - </element> - </sequence> - </complexType> - </element> - </sequence> - <attributeGroup ref="ebics:VersionAttrGroup"/> - </complexType> - </element> - <complexType name="NoPubKeyDigestsRequestStaticHeaderType"> - <annotation> - <documentation xml:lang="de">Datentyp für den statischen EBICS-Header bei Aufträgen ohne Übertragung der Digests der Bankschlüssel (Auftrag HBP): keine Digests der öffentlichen Bankschlüssel, keine EU-Datei, keine Nutzdaten, OrderId optional!, Nonce, Timestamp, X001 Authentifizierung, Auftragsattribut DZHNN</documentation> - </annotation> - <complexContent> - <restriction base="ebics:StaticHeaderBaseType"> - <sequence> - <element name="HostID" type="ebics:HostIDType"> - <annotation> - <documentation xml:lang="de">Hostname des Banksystems.</documentation> - </annotation> - </element> - <element name="Nonce" type="ebics:NonceType"> - <annotation> - <documentation xml:lang="de">Zufallswert; damit wird die Initialisierungsnachricht des Clients einzigartig.</documentation> - </annotation> - </element> - <element name="Timestamp" type="ebics:TimestampType"> - <annotation> - <documentation xml:lang="de">aktueller Zeitstempel zur Begrenzung der serverseitigen Nonce-Speicherung.</documentation> - </annotation> - </element> - <element name="PartnerID" type="ebics:PartnerIDType"> - <annotation> - <documentation xml:lang="de">Kunden-ID des serverseitig administrierten Kunden.</documentation> - </annotation> - </element> - <element name="UserID" type="ebics:UserIDType"> - <annotation> - <documentation xml:lang="de">Teilnehmer-ID des serverseitig zu diesem Kunden administrierten Teilnehmers.</documentation> - </annotation> - </element> - <element name="SystemID" type="ebics:UserIDType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">technische User-ID für Multi-User-Systeme.</documentation> - </annotation> - </element> - <element name="Product" type="ebics:ProductElementType" nillable="true" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Kennung des Kundenprodukts bzw. Herstellerkennung oder Name.</documentation> - </annotation> - </element> - <element name="OrderDetails" type="ebics:NoPubKeyDigestsReqOrderDetailsType"> - <annotation> - <documentation xml:lang="de">Auftragsdetails.</documentation> - </annotation> - </element> - <element name="SecurityMedium" type="ebics:SecurityMediumType"> - <annotation> - <documentation xml:lang="de">Angabe des Sicherheitsmediums, das der Kunde verwendet.</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </restriction> - </complexContent> - </complexType> - <complexType name="NoPubKeyDigestsReqOrderDetailsType"> - <annotation> - <documentation xml:lang="de"> Datentyp für OrderDetails im statischen EBICS-Header von ebicsNoPubKeyDigestsRequest.</documentation> - </annotation> - <complexContent> - <restriction base="ebics:OrderDetailsType"> - <sequence> - <element name="AdminOrderType" type="ebics:OrderTBaseType"> - <annotation> - <documentation xml:lang="de">Auftragsart.</documentation> - </annotation> - </element> - </sequence> - </restriction> - </complexContent> - </complexType> - <element name="ebicsUnsignedRequest"> - <annotation> - <documentation xml:lang="en">The structure for uploads contains order data and the ESs, but without an authentication signature and data digest of bank keys.</documentation> - <documentation>Anfragestruktur für Sendeaufträge mit EU-Datei und Nutzdaten aber ohne Authentifizierungssignatur und Digests der Bankschlüssel.</documentation> - </annotation> - <complexType> - <sequence> - <element name="header"> - <annotation> - <documentation xml:lang="en">Contains technical transaction data.</documentation> - <documentation xml:lang="de">enthält die technischen Transaktionsdaten.</documentation> - </annotation> - <complexType> - <sequence> - <element name="static" type="ebics:UnsignedRequestStaticHeaderType"> - <annotation> - <documentation xml:lang="en">Contains all fixed header entries.</documentation> - <documentation xml:lang="de">enhält alle festen Headereinträge.</documentation> - </annotation> - </element> - <element name="mutable" type="ebics:EmptyMutableHeaderType"> - <annotation> - <documentation xml:lang="en">Contains all mutable header entries.</documentation> - <documentation xml:lang="de">enthält alle variablen Headereinträge.</documentation> - </annotation> - </element> - </sequence> - <attributeGroup ref="ebics:AuthenticationMarker"/> - </complexType> - </element> - <element name="body"> - <annotation> - <documentation xml:lang="en">Contains the order data and the ESs.</documentation> - <documentation xml:lang="de">enthält die Auftragsdaten und EUs.</documentation> - </annotation> - <complexType> - <sequence> - <annotation> - <documentation xml:lang="de"/> - </annotation> - <element name="DataTransfer"> - <annotation> - <documentation xml:lang="en">Transfer of order data and the ESs.</documentation> - <documentation xml:lang="de">Transfer von Auftragsdaten und EUs.</documentation> - </annotation> - <complexType> - <sequence> - <element name="SignatureData"> - <annotation> - <documentation xml:lang="en">Contains the ESs.</documentation> - <documentation xml:lang="de">enthält Signaturdaten (EUs).</documentation> - </annotation> - <complexType> - <simpleContent> - <extension base="ebics:SignatureDataType"> - <attributeGroup ref="ebics:AuthenticationMarker"/> - </extension> - </simpleContent> - </complexType> - </element> - <element name="OrderData"> - <annotation> - <documentation xml:lang="en">Contains the order data</documentation> - <documentation xml:lang="de">enthält Auftragsdaten.</documentation> - </annotation> - <complexType> - <simpleContent> - <extension base="ebics:OrderDataType"> - <anyAttribute namespace="##targetNamespace" processContents="lax"/> - </extension> - </simpleContent> - </complexType> - </element> - </sequence> - </complexType> - </element> - </sequence> - </complexType> - </element> - </sequence> - <attributeGroup ref="ebics:VersionAttrGroup"/> - </complexType> - </element> - <complexType name="UnsignedRequestStaticHeaderType"> - <annotation> - <documentation xml:lang="de">Datentyp für den statischen EBICS-Header für ebicsUnsignedRequest.Datentyp für den statischen EBICS-Header bei Aufträgen ohne Authentifizierungssignatur (Auftrag HSA): keine X001 Authentifizierung, keine Digests der öffentlichen Bankschlüssel, EU-Datei, Nutzdaten, Nonce, Timestamp, OrderId, Auftragsattribut OZNNN</documentation> - </annotation> - <complexContent> - <restriction base="ebics:StaticHeaderBaseType"> - <sequence> - <element name="HostID" type="ebics:HostIDType"> - <annotation> - <documentation xml:lang="de">Hostname des Banksystems.</documentation> - </annotation> - </element> - <element name="Nonce" type="ebics:NonceType" minOccurs="0" maxOccurs="0"> - <annotation> - <documentation xml:lang="de">Zufallswert; damit wird die Initialisierungsnachricht des Clients einzigartig; nicht anzugeben bei ebicsUnsignedRequest.</documentation> - </annotation> - </element> - <element name="Timestamp" type="ebics:TimestampType" minOccurs="0" maxOccurs="0"> - <annotation> - <documentation xml:lang="de">aktueller Zeitstempel zur Begrenzung der serverseitigen Nonce-Speicherung; nicht anzugeben bei ebicsUnsignedRequest.</documentation> - </annotation> - </element> - <element name="PartnerID" type="ebics:PartnerIDType"> - <annotation> - <documentation xml:lang="de">Kunden-ID des serverseitig administrierten Kunden.</documentation> - </annotation> - </element> - <element name="UserID" type="ebics:UserIDType"> - <annotation> - <documentation xml:lang="de">Teilnehmer-ID des serverseitig zu diesem Kunden administrierten Teilnehmers.</documentation> - </annotation> - </element> - <element name="SystemID" type="ebics:UserIDType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">technische User-ID für Multi-User-Systeme.</documentation> - </annotation> - </element> - <element name="Product" type="ebics:ProductElementType" nillable="true" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Kennung des Kundenprodukts bzw. Herstellerkennung oder Name.</documentation> - </annotation> - </element> - <element name="OrderDetails" type="ebics:UnsignedReqOrderDetailsType"> - <annotation> - <documentation xml:lang="de">Auftragsdetails.</documentation> - </annotation> - </element> - <element name="SecurityMedium" type="ebics:SecurityMediumType"> - <annotation> - <documentation xml:lang="de">Angabe des Sicherheitsmediums, das der Kunde verwendet.</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </restriction> - </complexContent> - </complexType> - <complexType name="UnsignedReqOrderDetailsType"> - <annotation> - <documentation xml:lang="de">Datentyp für OrderDetails im statischen EBICS-Header von ebicsUnsignedRequest.</documentation> - </annotation> - <complexContent> - <restriction base="ebics:OrderDetailsType"> - <sequence> - <element name="AdminOrderType" type="ebics:OrderTBaseType"> - <annotation> - <documentation xml:lang="de">Auftragsart.</documentation> - </annotation> - </element> - </sequence> - </restriction> - </complexContent> - </complexType> -</schema> diff --git a/libeufin-common/src/main/resources/xsd/ebics_keymgmt_response_H004.xsd b/libeufin-common/src/main/resources/xsd/ebics_keymgmt_response_H004.xsd @@ -1,137 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- Mit XMLSpy v2008 rel. 2 sp2 (http://www.altova.com) von benutzerservice benutzerservice (SIZ GmbH) bearbeitet --> -<schema xmlns="http://www.w3.org/2001/XMLSchema" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:ebics="urn:org:ebics:H004" targetNamespace="urn:org:ebics:H004" elementFormDefault="qualified" attributeFormDefault="unqualified" version="1.0"> - <annotation> - <documentation xml:lang="de">ebics_keymgmt_response_H004.xsd ist das EBICS-Protokollschema für Schlüsselmanagement-Antwortnachrichten (HIA, HPB, HSA, INI).</documentation> - </annotation> - <import namespace="http://www.w3.org/2000/09/xmldsig#" schemaLocation="xmldsig-core-schema.xsd"> - <annotation> - <documentation xml:lang="de">XML-Signature.</documentation> - </annotation> - </import> - <include schemaLocation="ebics_types_H004.xsd"/> - <include schemaLocation="ebics_orders_H004.xsd"/> - <element name="ebicsKeyManagementResponse"> - <annotation> - <documentation xml:lang="de">Electronic Banking Internet Communication Standard des Zentralen Kreditausschusses (ZKA): Multibankfähige Schnittstelle zur internetbasierten Kommunikation.</documentation> - </annotation> - <complexType> - <sequence> - <element name="header"> - <annotation> - <documentation xml:lang="de">enthält die technischen Transaktionsdaten.</documentation> - </annotation> - <complexType> - <sequence> - <element name="static"> - <annotation> - <documentation xml:lang="de">enhält alle festen Headereinträge.</documentation> - </annotation> - <complexType> - <sequence/> - </complexType> - </element> - <element name="mutable" type="ebics:KeyMgmntResponseMutableHeaderType"> - <annotation> - <documentation xml:lang="de">enthält alle variablen Headereinträge.</documentation> - </annotation> - </element> - </sequence> - <attributeGroup ref="ebics:AuthenticationMarker"/> - </complexType> - </element> - <element name="body"> - <annotation> - <documentation xml:lang="de">enthält die Auftragsdaten und den fachlichen ReturnCode.</documentation> - </annotation> - <complexType> - <sequence> - <element name="DataTransfer" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Transfer von Auftragsdaten; nur bei Download anzugeben (HPB).</documentation> - </annotation> - <complexType> - <sequence> - <element name="DataEncryptionInfo"> - <annotation> - <documentation xml:lang="de">Informationen zur Verschlüsselung der Auftragsdaten</documentation> - </annotation> - <complexType> - <complexContent> - <extension base="ebics:DataEncryptionInfoType"> - <attributeGroup ref="ebics:AuthenticationMarker"/> - </extension> - </complexContent> - </complexType> - </element> - <element name="OrderData"> - <annotation> - <documentation xml:lang="de">enthält Auftragsdaten.</documentation> - </annotation> - <complexType> - <simpleContent> - <extension base="ebics:OrderDataType"> - <anyAttribute namespace="##targetNamespace" processContents="lax"/> - </extension> - </simpleContent> - </complexType> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - </element> - <element name="ReturnCode"> - <annotation> - <documentation xml:lang="de">Antwortcode für den vorangegangenen Transfer.</documentation> - </annotation> - <complexType> - <simpleContent> - <extension base="ebics:ReturnCodeType"> - <attributeGroup ref="ebics:AuthenticationMarker"/> - </extension> - </simpleContent> - </complexType> - </element> - <element name="TimestampBankParameter" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Zeitstempel der letzten Aktualisierung der Bankparameter; nur in der Initialisierungsphase anzugeben.</documentation> - </annotation> - <complexType> - <simpleContent> - <extension base="ebics:TimestampType"> - <attributeGroup ref="ebics:AuthenticationMarker"/> - </extension> - </simpleContent> - </complexType> - </element> - </sequence> - </complexType> - </element> - </sequence> - <attributeGroup ref="ebics:VersionAttrGroup"/> - </complexType> - </element> - <complexType name="KeyMgmntResponseMutableHeaderType"> - <annotation> - <documentation xml:lang="de">Datentyp für den variablen EBICS-Header.</documentation> - </annotation> - <sequence> - <element name="OrderID" type="ebics:OrderIDType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Auftragsnummer von Sendeaufträgen gemäß DFÜ-Abkommen (used for all key management order types except download order type HPB).</documentation> - </annotation> - </element> - <element name="ReturnCode" type="ebics:ReturnCodeType"> - <annotation> - <documentation xml:lang="de">Rückmeldung des Ausführungsstatus mit einer eindeutigen Fehlernummer.</documentation> - </annotation> - </element> - <element name="ReportText" type="ebics:ReportTextType"> - <annotation> - <documentation xml:lang="de">Klartext der Rückmeldung des Ausführungsstatus.</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> -</schema> diff --git a/libeufin-common/src/main/resources/xsd/ebics_keymgmt_response_H005.xsd b/libeufin-common/src/main/resources/xsd/ebics_keymgmt_response_H005.xsd @@ -1,137 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- edited with XMLSpy v2016 sp1 (x64) (http://www.altova.com) by EBICS Working Group - October 2016 --> -<schema xmlns="http://www.w3.org/2001/XMLSchema" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:ebics="urn:org:ebics:H005" targetNamespace="urn:org:ebics:H005" elementFormDefault="qualified" attributeFormDefault="unqualified" version="1.0"> - <annotation> - <documentation xml:lang="de">ebics_keymgmt_response_H005.xsd ist das EBICS-Protokollschema für Schlüsselmanagement-Antwortnachrichten (HIA, HPB, HSA, INI).</documentation> - </annotation> - <import namespace="http://www.w3.org/2000/09/xmldsig#" schemaLocation="xmldsig-core-schema.xsd"> - <annotation> - <documentation xml:lang="de">XML-Signature.</documentation> - </annotation> - </import> - <include schemaLocation="ebics_types_H005.xsd"/> - <include schemaLocation="ebics_orders_H005.xsd"/> - <element name="ebicsKeyManagementResponse"> - <annotation> - <documentation xml:lang="de">Electronic Banking Internet Communication Standard des Zentralen Kreditausschusses (ZKA): Multibankfähige Schnittstelle zur internetbasierten Kommunikation.</documentation> - </annotation> - <complexType> - <sequence> - <element name="header"> - <annotation> - <documentation xml:lang="de">enthält die technischen Transaktionsdaten.</documentation> - </annotation> - <complexType> - <sequence> - <element name="static"> - <annotation> - <documentation xml:lang="de">enhält alle festen Headereinträge.</documentation> - </annotation> - <complexType> - <sequence/> - </complexType> - </element> - <element name="mutable" type="ebics:KeyMgmntResponseMutableHeaderType"> - <annotation> - <documentation xml:lang="de">enthält alle variablen Headereinträge.</documentation> - </annotation> - </element> - </sequence> - <attributeGroup ref="ebics:AuthenticationMarker"/> - </complexType> - </element> - <element name="body"> - <annotation> - <documentation xml:lang="de">enthält die Auftragsdaten und den fachlichen ReturnCode.</documentation> - </annotation> - <complexType> - <sequence> - <element name="DataTransfer" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Transfer von Auftragsdaten; nur bei Download anzugeben (HPB).</documentation> - </annotation> - <complexType> - <sequence> - <element name="DataEncryptionInfo"> - <annotation> - <documentation xml:lang="de">Informationen zur Verschlüsselung der Auftragsdaten</documentation> - </annotation> - <complexType> - <complexContent> - <extension base="ebics:DataEncryptionInfoType"> - <attributeGroup ref="ebics:AuthenticationMarker"/> - </extension> - </complexContent> - </complexType> - </element> - <element name="OrderData"> - <annotation> - <documentation xml:lang="de">enthält Auftragsdaten.</documentation> - </annotation> - <complexType> - <simpleContent> - <extension base="ebics:OrderDataType"> - <anyAttribute namespace="##targetNamespace" processContents="lax"/> - </extension> - </simpleContent> - </complexType> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - </element> - <element name="ReturnCode"> - <annotation> - <documentation xml:lang="de">Antwortcode für den vorangegangenen Transfer.</documentation> - </annotation> - <complexType> - <simpleContent> - <extension base="ebics:ReturnCodeType"> - <attributeGroup ref="ebics:AuthenticationMarker"/> - </extension> - </simpleContent> - </complexType> - </element> - <element name="TimestampBankParameter" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Zeitstempel der letzten Aktualisierung der Bankparameter; nur in der Initialisierungsphase anzugeben.</documentation> - </annotation> - <complexType> - <simpleContent> - <extension base="ebics:TimestampType"> - <attributeGroup ref="ebics:AuthenticationMarker"/> - </extension> - </simpleContent> - </complexType> - </element> - </sequence> - </complexType> - </element> - </sequence> - <attributeGroup ref="ebics:VersionAttrGroup"/> - </complexType> - </element> - <complexType name="KeyMgmntResponseMutableHeaderType"> - <annotation> - <documentation xml:lang="de">Datentyp für den variablen EBICS-Header.</documentation> - </annotation> - <sequence> - <element name="OrderID" type="ebics:OrderIDType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Auftragsnummer von Sendeaufträgen gemäß DFÜ-Abkommen (used for all key management order types except download order type HPB).</documentation> - </annotation> - </element> - <element name="ReturnCode" type="ebics:ReturnCodeType"> - <annotation> - <documentation xml:lang="de">Rückmeldung des Ausführungsstatus mit einer eindeutigen Fehlernummer.</documentation> - </annotation> - </element> - <element name="ReportText" type="ebics:ReportTextType"> - <annotation> - <documentation xml:lang="de">Klartext der Rückmeldung des Ausführungsstatus.</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> -</schema> diff --git a/libeufin-common/src/main/resources/xsd/ebics_orders_H004.xsd b/libeufin-common/src/main/resources/xsd/ebics_orders_H004.xsd @@ -1,1892 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- Mit XMLSpy v2008 rel. 2 (http://www.altova.com) von Sabine Wenzel (SIZ Bonn) bearbeitet --> -<schema xmlns="http://www.w3.org/2001/XMLSchema" xmlns:esig="http://www.ebics.org/S001" xmlns:ebics="urn:org:ebics:H004" targetNamespace="urn:org:ebics:H004" elementFormDefault="qualified" attributeFormDefault="unqualified" version="1.0"> - <annotation> - <documentation xml:lang="de">ebics_orders_H004.xsd enthält auftragsbezogene Referenzelemente und auftragsbezogene Typdefinitionen für EBICS.</documentation> - <documentation xml:lang="en">ebics_orders_H004.xsd contains order-based reference elements and order-based type definitions for EBICS.</documentation> - </annotation> - <import namespace="http://www.ebics.org/S001" schemaLocation="ebics_signatures.xsd"/> - <include schemaLocation="ebics_types_H004.xsd"/> - <!--Es folgen die Elementdefinitionen für neue EBICS-Auftragsarten (Hxx). Die XML-Klartext-Struktur wird im EBICS-Auftragsdatenkontext binär interpretiert.--> - <element name="EBICSOrderData" abstract="true"> - <annotation> - <documentation xml:lang="de">XML-Klartext-Auftragsdaten für neue EBICS-Auftragsarten.</documentation> - <documentation xml:lang="en">Order data in XML format for new EBICS order types.</documentation> - </annotation> - </element> - <element name="HAAResponseOrderData" type="ebics:HAAResponseOrderDataType" substitutionGroup="ebics:EBICSOrderData"> - <annotation> - <documentation xml:lang="de">Auftragsdaten für Auftragsart HAA (Antwort: abrufbare Auftragsarten abholen).</documentation> - <documentation xml:lang="en">Order data for order type HAA (response: receive downloadable order types).</documentation> - </annotation> - </element> - <element name="HCARequestOrderData" type="ebics:HCARequestOrderDataType" substitutionGroup="ebics:EBICSOrderData"> - <annotation> - <documentation xml:lang="de">Auftragsdaten für Auftragsart HCA (Anfrage: Änderung der Teilnehmerschlüssel für Authentifikation und Verschlüsselung).</documentation> - <documentation xml:lang="en">Order data for order type HCA (request: replace user's keys for authentication and encryption).</documentation> - </annotation> - </element> - <element name="HCSRequestOrderData" type="ebics:HCSRequestOrderDataType" substitutionGroup="ebics:EBICSOrderData"> - <annotation> - <documentation xml:lang="de">Auftragsdaten für Auftragsart HCS (Anfrage: Schlüsselwechsel aller Schlüssel).</documentation> - <documentation xml:lang="en">Order data for order type HCS (request: replace all keys).</documentation> - </annotation> - </element> - <element name="HIARequestOrderData" type="ebics:HIARequestOrderDataType" substitutionGroup="ebics:EBICSOrderData"> - <annotation> - <documentation xml:lang="de">Auftragsdaten für Auftragsart HIA (Anfrage: Initialisierung der Teilnehmerschlüssel für Authentifikation und Verschlüsselung).</documentation> - <documentation xml:lang="en">Order data for order type HIA (request: initialise user's keys for authentication and encryption).</documentation> - </annotation> - </element> - <element name="H3KRequestOrderData" type="ebics:H3KRequestOrderDataType"> - <annotation> - <documentation xml:lang="en">Order data for order type H3K (request: initialise all three user's keys).</documentation> - <documentation xml:lang="de">Auftragsdaten für Auftragsart H3K (Anfrage: Initialisierung aller drei Teilnehmerschlüssel).</documentation> - </annotation> - </element> - <element name="HSARequestOrderData" type="ebics:HSARequestOrderDataType" substitutionGroup="ebics:EBICSOrderData"> - <annotation> - <documentation xml:lang="de">Auftragsdaten für Auftragsart HSA (Anfrage: Initialisierung der Teilnehmerschlüssel für Authentifikation und Verschlüsselung, bankfachlich signiert mit Signaturschlüssel aus FTAM).</documentation> - <documentation xml:lang="en">Order data for order type HSA (request: initialise the user's keys for authentication and encryption; this request is signed using the user's FTAM signature key).</documentation> - </annotation> - </element> - <element name="HKDResponseOrderData" type="ebics:HKDResponseOrderDataType" substitutionGroup="ebics:EBICSOrderData"> - <annotation> - <documentation xml:lang="de">Auftragsdaten für Auftragsart HKD (Antwort: Kunden- und Teilnehmerdaten des Kunden abholen).</documentation> - <documentation xml:lang="en">Order data for order type HKD (response: receive customer-based information on the customer and the customer's users).</documentation> - </annotation> - <key name="HKDAccountKey"> - <annotation> - <documentation xml:lang="de">Schlüssel zur Identifikation des Kontos.</documentation> - <documentation xml:lang="en">Key for the identification of the account.</documentation> - </annotation> - <selector xpath="./ebics:PartnerInfo/ebics:AccountInfo"/> - <field xpath="@ID"/> - </key> - <keyref name="HKDAccountRef" refer="ebics:HKDAccountKey"> - <annotation> - <documentation xml:lang="de">Referenz auf die Konten-Identifikationsschlüssel.</documentation> - <documentation xml:lang="en">Reference to the account identification keys.</documentation> - </annotation> - <selector xpath="./ebics:UserInfo/ebics:Permission"/> - <field xpath="ebics:AccountID"/> - </keyref> - </element> - <element name="HPBResponseOrderData" type="ebics:HPBResponseOrderDataType" substitutionGroup="ebics:EBICSOrderData"> - <annotation> - <documentation xml:lang="de">Auftragsdaten für Auftragsart HPB (Antwort: Transfer der Bankschlüssel).</documentation> - <documentation xml:lang="en">Order data for order type HPB (response: receive bank's public keys).</documentation> - </annotation> - </element> - <element name="HPDResponseOrderData" type="ebics:HPDResponseOrderDataType" substitutionGroup="ebics:EBICSOrderData"> - <annotation> - <documentation xml:lang="de">Auftragsdaten für Auftragsart HPD (Antwort: Bankparameter abholen).</documentation> - <documentation xml:lang="en">Order data for order type HPD (response: receive bank parameters).</documentation> - </annotation> - </element> - <element name="HTDResponseOrderData" type="ebics:HTDReponseOrderDataType" substitutionGroup="ebics:EBICSOrderData"> - <annotation> - <documentation xml:lang="de">Auftragsdaten für Auftragsart HTD (Antwort: Kunden- und Teilnehmerdaten des Teilnehmers abholen).</documentation> - <documentation xml:lang="en">Order data for order type HTD (response: receive user-based information on the user's customer and the user herself/himself).</documentation> - </annotation> - <key name="HTDAccountKey"> - <annotation> - <documentation xml:lang="de">Schlüssel zur Identifikation des Kontos.</documentation> - <documentation xml:lang="en">Key for the identification of the account.</documentation> - </annotation> - <selector xpath="./ebics:PartnerInfo/ebics:AccountInfo"/> - <field xpath="@ID"/> - </key> - <keyref name="HTDAccountRef" refer="ebics:HTDAccountKey"> - <annotation> - <documentation xml:lang="de">Referenz auf die Konten-Identifikationsschlüssel.</documentation> - <documentation xml:lang="en">Reference to the account identification keys.</documentation> - </annotation> - <selector xpath="./ebics:UserInfo/ebics:Permission"/> - <field xpath="ebics:AccountID"/> - </keyref> - </element> - <element name="HVDResponseOrderData" type="ebics:HVDResponseOrderDataType" substitutionGroup="ebics:EBICSOrderData"> - <annotation> - <documentation xml:lang="de">Auftragsdaten für Auftragsart HVD (Antwort: VEU-Status abrufen).</documentation> - <documentation xml:lang="en">Order data for order type HVD (response: receive the status of an order currently stored in the distributed signature processing unit).</documentation> - </annotation> - </element> - <element name="HVSRequestOrderData" type="ebics:HVSRequestOrderDataType" substitutionGroup="ebics:EBICSOrderData"> - <annotation> - <documentation xml:lang="de">Auftragsdaten für Auftragsart HVS (Anfrage: VEU-Storno).</documentation> - <documentation xml:lang="en">Order data for order type HVS (request: reject an order currently stored in the distributed signature processing unit).</documentation> - </annotation> - </element> - <element name="HVTResponseOrderData" substitutionGroup="ebics:EBICSOrderData"> - <annotation> - <documentation xml:lang="de">Auftragsdaten für Auftragsart HVT (Antwort: VEU-Transaktionsdetails abrufen).</documentation> - <documentation xml:lang="en">Order data for order type HVT (response: receive transaction details of an order currently stored in the distributed signature processing unit).</documentation> - </annotation> - <complexType> - <complexContent> - <extension base="ebics:HVTResponseOrderDataType"/> - </complexContent> - </complexType> - </element> - <element name="HVUResponseOrderData" type="ebics:HVUResponseOrderDataType" substitutionGroup="ebics:EBICSOrderData"> - <annotation> - <documentation xml:lang="de">Auftragsdaten für Auftragsart HVU (Antwort: VEU-Übersicht abholen).</documentation> - <documentation xml:lang="en">Order data for order type HVU (response: receive summary of orders currently stored in the distributed signature processing unit).</documentation> - </annotation> - </element> - <element name="HVZResponseOrderData" type="ebics:HVZResponseOrderDataType" substitutionGroup="ebics:EBICSOrderData"> - <annotation> - <documentation xml:lang="de">Auftragsdaten für Auftragsart HVZ (Antwort: VEU-Übersicht mit Zusatzinformationen abholen).</documentation> - <documentation xml:lang="en">Order data for order type HVZ (response: receive summary of orders currently stored in the distributed signature processing unit with additional information).</documentation> - </annotation> - </element> - <!--Es folgen die Elementdefinitionen für die EU. Die XML-Klartext-Struktur wird im EBICS-Signaturdatenkontext binär interpretiert.--> - <element name="EBICSSignatureData" abstract="true"> - <annotation> - <documentation xml:lang="de">XML-Strukturen für bankfachliche Elektronische Unterschriften (EUs).</documentation> - <documentation xml:lang="en">contains the digital signatures.</documentation> - </annotation> - </element> - <element name="BankSignatureData" type="ebics:BankSignatureDataSigBookType" substitutionGroup="ebics:EBICSSignatureData"> - <annotation> - <documentation xml:lang="de">enthält die EU des Kreditinstituts.</documentation> - <documentation xml:lang="en">contains the digital signatures.</documentation> - </annotation> - </element> - <!--Es folgen die Elementdefinitionen für zusätzliche Auftragsparameter. Die XML-Klartext-Struktur wird in die Headerdaten eingebettet.--> - <element name="OrderParams" abstract="true"> - <annotation> - <documentation xml:lang="de">zusätzliche Auftragsparameter, die zur Ausführung des Auftrags notwendig sind.</documentation> - <documentation xml:lang="en">additional order parameters required to execute the order.</documentation> - </annotation> - </element> - <element name="HVDOrderParams" type="ebics:HVDOrderParamsType" substitutionGroup="ebics:OrderParams"> - <annotation> - <documentation xml:lang="de">zusätzliche Auftragsparameter für Auftragsart HVD.</documentation> - <documentation xml:lang="en">additional order parameters for order type HVD.</documentation> - </annotation> - </element> - <element name="HVEOrderParams" type="ebics:HVEOrderParamsType" substitutionGroup="ebics:OrderParams"> - <annotation> - <documentation xml:lang="de">zusätzliche Auftragsparameter für Auftragsart HVE.</documentation> - <documentation xml:lang="en">additional order parameters for order type HVE.</documentation> - </annotation> - </element> - <element name="HVSOrderParams" type="ebics:HVSOrderParamsType" substitutionGroup="ebics:OrderParams"> - <annotation> - <documentation xml:lang="de">zusätzliche Auftragsparameter für Auftragsart HVS.</documentation> - <documentation xml:lang="en">additional order parameters for order type HVS.</documentation> - </annotation> - </element> - <element name="HVTOrderParams" type="ebics:HVTOrderParamsType" substitutionGroup="ebics:OrderParams"> - <annotation> - <documentation xml:lang="de">zusätzliche Auftragsparameter für Auftragsart HVT.</documentation> - <documentation xml:lang="en">additional order parameters for order type HVT.</documentation> - </annotation> - </element> - <element name="HVUOrderParams" type="ebics:HVUOrderParamsType" substitutionGroup="ebics:OrderParams"> - <annotation> - <documentation xml:lang="de">zusätzliche Auftragsparameter für Auftragsart HVU.</documentation> - <documentation xml:lang="en">additional order parameters for order type HVU.</documentation> - </annotation> - </element> - <element name="HVZOrderParams" type="ebics:HVZOrderParamsType" substitutionGroup="ebics:OrderParams"> - <annotation> - <documentation xml:lang="de">zusätzliche Auftragsparameter für Auftragsart HVZ.</documentation> - <documentation xml:lang="en">additional order parameters for order type HVZ.</documentation> - </annotation> - </element> - <element name="FULOrderParams" type="ebics:FULOrderParamsType" substitutionGroup="ebics:OrderParams"> - <annotation> - <documentation xml:lang="de">zusätzliche Auftragsparameter für Auftragsart FUL.</documentation> - <documentation xml:lang="en">additional order parameters for order type FUL.</documentation> - </annotation> - </element> - <element name="FDLOrderParams" type="ebics:FDLOrderParamsType" substitutionGroup="ebics:OrderParams"> - <annotation> - <documentation xml:lang="de">zusätzliche Auftragsparameter für Auftragsart FDL.</documentation> - <documentation xml:lang="en">additional order parameters for order type FDL.</documentation> - </annotation> - </element> - <element name="StandardOrderParams" type="ebics:StandardOrderParamsType" substitutionGroup="ebics:OrderParams"> - <annotation> - <documentation xml:lang="de">zusätzliche Auftragsparameter für Standard-Auftragsarten.</documentation> - <documentation xml:lang="en">additional order parameters for standard order types.</documentation> - </annotation> - </element> - <element name="GenericOrderParams" type="ebics:GenericOrderParamsType" substitutionGroup="ebics:OrderParams"> - <annotation> - <documentation xml:lang="de">zusätzliche Auftragsparameter für beliebige Auftragsarten.</documentation> - <documentation xml:lang="en">additional order parameters for generic order types.</documentation> - </annotation> - </element> - <!--Es folgen Elementgruppen- und Strukturdefinitionen.--> - <group name="HVRequestStructure"> - <annotation> - <documentation xml:lang="de">Standard-Requeststruktur für HVx-Aufträge (HVD, HVT, HVE, HVS).</documentation> - <documentation xml:lang="en">Standard request structure for HVx orders (HVD, HVT, HVE, HVS).</documentation> - </annotation> - <sequence> - <annotation> - <documentation xml:lang="de">Standard-Requestdaten.</documentation> - <documentation xml:lang="en">Standard request data.</documentation> - </annotation> - <element name="PartnerID" type="ebics:PartnerIDType"> - <annotation> - <documentation xml:lang="de">Kunden-ID des Einreichers des ausgewählten Auftrags.</documentation> - <documentation xml:lang="en">Customer ID of the presenter of the selected order.</documentation> - </annotation> - </element> - <element name="OrderType" type="ebics:OrderTBaseType"> - <annotation> - <documentation xml:lang="de">Auftragsart des ausgewählten Auftrags.</documentation> - <documentation xml:lang="en">Order type of the selected order.</documentation> - </annotation> - </element> - <element name="FileFormat" type="ebics:FileFormatType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Identifizierung des Dateiformats im Falle von FUL/FDL</documentation> - <documentation xml:lang="en">Identification of the file format in the case of FUL/FDL</documentation> - </annotation> - </element> - <element name="OrderID" type="ebics:OrderIDType"> - <annotation> - <documentation xml:lang="de">Auftragsnummer des ausgewählten Auftrags.</documentation> - <documentation xml:lang="en">Order ID of the selected order.</documentation> - </annotation> - </element> - </sequence> - </group> - <attributeGroup name="AuthenticationMarker"> - <annotation> - <documentation xml:lang="de">Marker für Elemente und deren Substrukturen, die authentifiziert werden sollen.</documentation> - <documentation xml:lang="en">Marker for elements and their substructures that are to be authenticated.</documentation> - </annotation> - <attribute name="authenticate" type="boolean" use="required" fixed="true"> - <annotation> - <documentation xml:lang="de">Das zugehörige Element ist mitsamt seinen Unterstrukturen zu authentifizieren.</documentation> - <documentation xml:lang="en">The element (and its substructures) that belongs to this attribute is to be authenticated.</documentation> - </annotation> - </attribute> - </attributeGroup> - <attributeGroup name="OptSupportFlag"> - <annotation> - <documentation xml:lang="de">optionales Support-Flag, Default = true.</documentation> - <documentation xml:lang="en">optional support flag, default = true.</documentation> - </annotation> - <attribute name="supported" type="boolean" use="optional" default="true"> - <annotation> - <documentation xml:lang="de">Wird die Funktion unterstützt?</documentation> - <documentation xml:lang="en">Is this function supported?</documentation> - </annotation> - </attribute> - <anyAttribute namespace="##targetNamespace" processContents="strict"/> - </attributeGroup> - <attributeGroup name="SignerPermission"> - <annotation> - <documentation xml:lang="de">EU-Berechtigungsinformationen.</documentation> - <documentation xml:lang="en">permission information of a user's digital signature.</documentation> - </annotation> - <attribute name="AuthorisationLevel" type="ebics:AuthorisationLevelType" use="required"> - <annotation> - <documentation xml:lang="de">Unterschriftsberechtigung des Teilnehmers, der unterzeichnet hat.</documentation> - <documentation xml:lang="en">Authorisation level of the user that signed the order.</documentation> - </annotation> - </attribute> - <anyAttribute namespace="##targetNamespace" processContents="strict"/> - </attributeGroup> - <!--Es folgen Typdefinitionen für auftragsbezogene komplexe Typen.--> - <complexType name="BankSignatureDataSigBookType"> - <annotation> - <documentation xml:lang="de">Datentyp für Signaturdaten des Kreditinstituts beim EU-Transfer.</documentation> - <documentation xml:lang="en">Data type for digital signature data transferred using EBICS.</documentation> - </annotation> - <sequence> - <element name="OrderSignature" minOccurs="0" maxOccurs="0"> - <annotation> - <documentation xml:lang="de">bankfachliche Elektronische Unterschrift.</documentation> - <documentation xml:lang="en">Digital signature (either autorising an order or applied for transportation).</documentation> - </annotation> - <complexType> - <simpleContent> - <extension base="ebics:SignatureType"/> - </simpleContent> - </complexType> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="PreValidationRequestType"> - <annotation> - <documentation xml:lang="de">Datentyp für Vorabprüfung (Anfrage).</documentation> - <documentation xml:lang="en">Data type for pre-validation (request).</documentation> - </annotation> - <sequence> - <annotation> - <documentation xml:lang="de">Client sendet den Hashwert der Auftragsdaten und alle weiteren Daten, die er im Rahmen der Vorabprüfung zur Verfügung stellen will</documentation> - </annotation> - <element name="DataDigest" type="ebics:DataDigestType" minOccurs="0" maxOccurs="unbounded"> - <annotation> - <documentation xml:lang="de">Hashwert der zu übertragenden Auftragsdatendatei für die Vorabprüfung.</documentation> - </annotation> - </element> - <element name="AccountAuthorisation" type="ebics:PreValidationAccountAuthType" minOccurs="0" maxOccurs="unbounded"> - <annotation> - <documentation xml:lang="de">Kontoangabe zur Kontoberechtigung für diesen Zahlungsverkehrsauftrag bei der Vorabprüfung.</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="PreValidationAccountAuthType"> - <annotation> - <documentation xml:lang="de">Datentyp für Kontenberechtigungsdaten zur Vorabprüfung.</documentation> - </annotation> - <complexContent> - <extension base="ebics:AccountType"> - <sequence> - <element name="Amount" type="ebics:AmountType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Summe der Zahlungsverkehrsaufträge dieses Kontos für die Höchstbetragsprüfung der EU.</documentation> - <documentation xml:lang="en">Total sum of the ordered payments regarding this account in order to check the maximum amount limit of the signature permission grades.</documentation> - </annotation> - </element> - </sequence> - </extension> - </complexContent> - </complexType> - <complexType name="DataTransferRequestType"> - <annotation> - <documentation xml:lang="de">Datentyp für den Transfer von Auftragsdaten (Anfrage).</documentation> - </annotation> - <sequence> - <choice> - <annotation> - <documentation xml:lang="de">Transaktionsphase?</documentation> - </annotation> - <sequence> - <annotation> - <documentation xml:lang="de">Initialisierungsphase: Transfer der Signaturdaten (EUs) und des Transaktionsschlüssels.</documentation> - </annotation> - <element name="DataEncryptionInfo"> - <annotation> - <documentation xml:lang="de">Information zur Verschlüsselung der Signatur- und Auftragsdaten.</documentation> - </annotation> - <complexType> - <complexContent> - <extension base="ebics:DataEncryptionInfoType"> - <attributeGroup ref="ebics:AuthenticationMarker"/> - </extension> - </complexContent> - </complexType> - </element> - <element name="SignatureData"> - <annotation> - <documentation xml:lang="de">enthält Signaturdaten (EUs).</documentation> - </annotation> - <complexType> - <simpleContent> - <extension base="ebics:SignatureDataType"> - <attributeGroup ref="ebics:AuthenticationMarker"/> - </extension> - </simpleContent> - </complexType> - </element> - </sequence> - <sequence> - <annotation> - <documentation xml:lang="de">Transferphase: Transfer von Auftragsdaten.</documentation> - </annotation> - <element name="OrderData"> - <annotation> - <documentation xml:lang="de">enthält Auftragsdaten.</documentation> - </annotation> - <complexType> - <simpleContent> - <extension base="ebics:OrderDataType"> - <anyAttribute namespace="##targetNamespace" processContents="lax"/> - </extension> - </simpleContent> - </complexType> - </element> - </sequence> - </choice> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="DataTransferResponseType"> - <annotation> - <documentation xml:lang="de">Datentyp für den Transfer von Auftragsdaten (Antwort).</documentation> - </annotation> - <sequence> - <sequence minOccurs="0"> - <annotation> - <documentation xml:lang="de">Transfer des Sitzungsschlüssels und (optional) der Signaturdaten (EUs); nur in der Initialisierungsphase anzugeben.</documentation> - </annotation> - <element name="DataEncryptionInfo"> - <annotation> - <documentation xml:lang="de">Information zur Verschlüsselung der Signatur- und Auftragsdaten.</documentation> - </annotation> - <complexType> - <complexContent> - <extension base="ebics:DataEncryptionInfoType"> - <attributeGroup ref="ebics:AuthenticationMarker"/> - </extension> - </complexContent> - </complexType> - </element> - <element name="SignatureData" minOccurs="0" maxOccurs="0"> - <annotation> - <documentation xml:lang="de">enthält Signaturdaten (EUs).</documentation> - </annotation> - <complexType> - <simpleContent> - <extension base="ebics:SignatureDataType"> - <attributeGroup ref="ebics:AuthenticationMarker"/> - </extension> - </simpleContent> - </complexType> - </element> - </sequence> - <element name="OrderData"> - <annotation> - <documentation xml:lang="de">enthält Auftragsdaten.</documentation> - </annotation> - <complexType> - <simpleContent> - <extension base="ebics:OrderDataType"> - <anyAttribute namespace="##targetNamespace" processContents="lax"/> - </extension> - </simpleContent> - </complexType> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="TransferReceiptRequestType"> - <annotation> - <documentation xml:lang="de">Datentyp für den Transfer von Transferquittungen.</documentation> - </annotation> - <sequence> - <element name="ReceiptCode" type="ebics:ReceiptCodeType"> - <annotation> - <documentation xml:lang="de">Quittierungscode für Auftragsdatentransfer.</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="TransferReceiptResponseType"> - <annotation> - <documentation xml:lang="de">Datentyp für den Transfer von Antwortcodes.</documentation> - </annotation> - <sequence> - <element name="ReturnCodeReceipt" type="ebics:ReturnCodeType"> - <annotation> - <documentation xml:lang="de">Antwortcode für den vorangegangenen Transfer.</documentation> - </annotation> - </element> - <element name="TimestampBankParameter" type="ebics:TimestampType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Zeitstempel der letzten Aktualisierung der Bankparameter.</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="HAAResponseOrderDataType"> - <annotation> - <documentation xml:lang="de">Datentyp für Auftragsdaten für Auftragsart HAA (Antwort: abrufbare Auftragsarten abholen).</documentation> - </annotation> - <sequence> - <element name="OrderTypes" type="ebics:OrderTListType"> - <annotation> - <documentation xml:lang="de">Liste von Auftragsarten, für die Daten bereit stehen.</documentation> - </annotation> - </element> - <any namespace="##other" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="HCARequestOrderDataType"> - <annotation> - <documentation xml:lang="de">Datentyp für Auftragsdaten für Auftragsart HCA (Anfrage: Änderung der Teilnehmerschlüssel für Authentifikation und Verschlüsselung).</documentation> - </annotation> - <sequence> - <element name="AuthenticationPubKeyInfo" type="ebics:AuthenticationPubKeyInfoType"> - <annotation> - <documentation xml:lang="de">öffentlicher Authentifikationsschlüssel.</documentation> - </annotation> - </element> - <element name="EncryptionPubKeyInfo" type="ebics:EncryptionPubKeyInfoType"> - <annotation> - <documentation xml:lang="de">öffentlicher Verschlüsselungsschlüssel.</documentation> - </annotation> - </element> - <element name="PartnerID" type="ebics:PartnerIDType"> - <annotation> - <documentation xml:lang="de">Kunden-ID.</documentation> - </annotation> - </element> - <element name="UserID" type="ebics:UserIDType"> - <annotation> - <documentation xml:lang="de">Teilnehmer-ID.</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="HCSRequestOrderDataType"> - <annotation> - <documentation xml:lang="de">Datentyp für Auftragsdaten für Auftragsart HCS (Anfrage: Schlüsselwechsel aller Schlüssel).</documentation> - </annotation> - <sequence> - <element name="AuthenticationPubKeyInfo" type="ebics:AuthenticationPubKeyInfoType"> - <annotation> - <documentation xml:lang="de">öffentlicher Authentifikationsschlüssel.</documentation> - </annotation> - </element> - <element name="EncryptionPubKeyInfo" type="ebics:EncryptionPubKeyInfoType"> - <annotation> - <documentation xml:lang="de">öffentlicher Verschlüsselungsschlüssel.</documentation> - </annotation> - </element> - <element ref="esig:SignaturePubKeyInfo"/> - <element name="PartnerID" type="ebics:PartnerIDType"> - <annotation> - <documentation xml:lang="de">Kunden-ID.</documentation> - </annotation> - </element> - <element name="UserID" type="ebics:UserIDType"> - <annotation> - <documentation xml:lang="de">Teilnehmer-ID.</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="HIARequestOrderDataType"> - <annotation> - <documentation xml:lang="de">Datentyp für Auftragsdaten für Auftragsart HIA (Anfrage: Initialisierung der Teilnehmerschlüssel für Authentifikation und Verschlüsselung).</documentation> - </annotation> - <sequence> - <element name="AuthenticationPubKeyInfo" type="ebics:AuthenticationPubKeyInfoType"> - <annotation> - <documentation xml:lang="de">öffentlicher Authentifikationsschlüssel.</documentation> - </annotation> - </element> - <element name="EncryptionPubKeyInfo" type="ebics:EncryptionPubKeyInfoType"> - <annotation> - <documentation xml:lang="de">öffentlicher Verschlüsselungsschlüssel.</documentation> - </annotation> - </element> - <element name="PartnerID" type="ebics:PartnerIDType"> - <annotation> - <documentation xml:lang="de">Kunden-ID.</documentation> - </annotation> - </element> - <element name="UserID" type="ebics:UserIDType"> - <annotation> - <documentation xml:lang="de">Teilnehmer-ID.</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="H3KRequestOrderDataType"> - <annotation> - <documentation xml:lang="de">Datentyp für Auftragsdaten für Auftragsart H3K (Anfrage: Initialisierung aller drei Teilnehmerschlüssel).</documentation> - <documentation xml:lang="en">Order type for order data H3K (request: initialise all three user's keys).</documentation> - </annotation> - <sequence> - <element name="SignatureCertificateInfo" type="ebics:SignatureCertificateInfoType"> - <annotation> - <documentation xml:lang="en">Key for electronic Signature</documentation> - <documentation xml:lang="de">Signaturschlüssel.</documentation> - </annotation> - </element> - <element name="AuthenticationCertificateInfo" type="ebics:AuthenticationCertificateInfoType"> - <annotation> - <documentation xml:lang="en">Authentication key</documentation> - <documentation xml:lang="de">Authentifikationsschlüssel.</documentation> - </annotation> - </element> - <element name="EncryptionCertificateInfo" type="ebics:EncryptionCertificateInfoType"> - <annotation> - <documentation xml:lang="en">Encryption key</documentation> - <documentation xml:lang="de">Verschlüsselungsschlüssel.</documentation> - </annotation> - </element> - <element name="PartnerID" type="ebics:PartnerIDType"> - <annotation> - <documentation xml:lang="en">PartnerID.</documentation> - <documentation xml:lang="de">Kunden-ID.</documentation> - </annotation> - </element> - <element name="UserID" type="ebics:UserIDType"> - <annotation> - <documentation xml:lang="en">UserID.</documentation> - <documentation xml:lang="de">Teilnehmer-ID.</documentation> - </annotation> - </element> - </sequence> - </complexType> - <complexType name="HSARequestOrderDataType"> - <annotation> - <documentation xml:lang="de">Datentyp für Auftragsdaten für Auftragsart HSA (Anfrage: Initialisierung der Teilnehmerschlüssel für Authentifikation und Verschlüsselung, bankfachlich signiert mit FTAM-Schlüssel).</documentation> - </annotation> - <sequence> - <element name="AuthenticationPubKeyInfo" type="ebics:AuthenticationPubKeyInfoType"> - <annotation> - <documentation xml:lang="de">öffentlicher Authentifikationsschlüssel.</documentation> - </annotation> - </element> - <element name="EncryptionPubKeyInfo" type="ebics:EncryptionPubKeyInfoType"> - <annotation> - <documentation xml:lang="de">öffentlicher Verschlüsselungsschlüssel.</documentation> - </annotation> - </element> - <element name="PartnerID" type="ebics:PartnerIDType"> - <annotation> - <documentation xml:lang="de">Kunden-ID.</documentation> - </annotation> - </element> - <element name="UserID" type="ebics:UserIDType"> - <annotation> - <documentation xml:lang="de">Teilnehmer-ID.</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="HKDResponseOrderDataType"> - <annotation> - <documentation xml:lang="de">Datentyp für Auftragsdaten für Auftragsart HKD (Antwort: Kunden- und Teilnehmerdaten des Kunden abholen).</documentation> - <documentation xml:lang="en">Order data for order type HKD (response: receive customer based information on the customer and the customer's user.</documentation> - </annotation> - <sequence> - <element name="PartnerInfo" type="ebics:PartnerInfoType"> - <annotation> - <documentation xml:lang="de">Kundendaten.</documentation> - <documentation xml:lang="en">Customer data.</documentation> - </annotation> - </element> - <element name="UserInfo" type="ebics:UserInfoType" maxOccurs="unbounded"> - <annotation> - <documentation xml:lang="de">Teilnehmerdaten.</documentation> - <documentation xml:lang="en">User data.</documentation> - </annotation> - </element> - <any namespace="##other" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="HPBResponseOrderDataType"> - <annotation> - <documentation xml:lang="de">Datentyp für Auftragsdaten für Auftragsart HPB (Antwort: Transfer der Bankschlüssel).</documentation> - </annotation> - <sequence> - <element name="AuthenticationPubKeyInfo" type="ebics:AuthenticationPubKeyInfoType"> - <annotation> - <documentation xml:lang="de">öffentlicher Authentifikationsschlüssel.</documentation> - </annotation> - </element> - <element name="EncryptionPubKeyInfo" type="ebics:EncryptionPubKeyInfoType"> - <annotation> - <documentation xml:lang="de">öffentlicher Verschlüsselungsschlüssel.</documentation> - </annotation> - </element> - <element ref="esig:SignaturePubKeyInfo" minOccurs="0" maxOccurs="0"> - <annotation> - <documentation xml:lang="de">öffentlicher EU-Signaturschlüssel.</documentation> - </annotation> - </element> - <element name="HostID" type="ebics:HostIDType"> - <annotation> - <documentation xml:lang="de">Banksystem-ID.</documentation> - </annotation> - </element> - <any namespace="##other" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="HPDResponseOrderDataType"> - <annotation> - <documentation xml:lang="de">Datentyp für Auftragsdaten für Auftragsart HPD (Antwort: Bankparameter abholen).</documentation> - </annotation> - <sequence> - <element name="AccessParams" type="ebics:HPDAccessParamsType"> - <annotation> - <documentation xml:lang="de">Zugangsparameter.</documentation> - </annotation> - </element> - <element name="ProtocolParams" type="ebics:HPDProtocolParamsType"> - <annotation> - <documentation xml:lang="de">Protokollparameter.</documentation> - </annotation> - </element> - </sequence> - </complexType> - <complexType name="HPDAccessParamsType"> - <annotation> - <documentation xml:lang="de">Datentyp für HPD-Zugangsparameter.</documentation> - </annotation> - <sequence> - <element name="URL" maxOccurs="unbounded"> - <annotation> - <documentation xml:lang="de">institutsspezifische IP-Adresse/URL.</documentation> - </annotation> - <complexType> - <simpleContent> - <extension base="anyURI"> - <attribute name="valid_from" type="ebics:TimestampType"> - <annotation> - <documentation xml:lang="de">Gültigkeitsbeginn für die angegebene URL/IP.</documentation> - </annotation> - </attribute> - </extension> - </simpleContent> - </complexType> - </element> - <element name="Institute"> - <annotation> - <documentation xml:lang="de">Institutsbezeichnung.</documentation> - </annotation> - <simpleType> - <restriction base="normalizedString"> - <maxLength value="80"/> - </restriction> - </simpleType> - </element> - <element name="HostID" type="ebics:HostIDType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Banksystem-ID.</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="HPDProtocolParamsType"> - <annotation> - <documentation xml:lang="de">Datentyp für HPD-Protokollparameter.</documentation> - <documentation xml:lang="en">Data type for HPD's parameters regarding the EBICS protocol.</documentation> - </annotation> - <sequence> - <element name="Version" type="ebics:HPDVersionType"> - <annotation> - <documentation xml:lang="de">Spezifikation unterstützter Versionen.</documentation> - <documentation xml:lang="en">Specification of supported versions..</documentation> - </annotation> - </element> - <element name="Recovery" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Parameter zur Recovery-Funktion (Wiederaufnahme abgebrochener Übertragungen).</documentation> - <documentation xml:lang="en">Parameter denoting the recovery function (recovery of aborted transmissions).</documentation> - </annotation> - <complexType> - <attributeGroup ref="ebics:OptSupportFlag"/> - </complexType> - </element> - <element name="PreValidation" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Parameter zur Vorabprüfung (über die Übermittlung der EU hinaus).</documentation> - <documentation xml:lang="en">Parameter denoting the pre-validation (beyond transmission of signatures).</documentation> - </annotation> - <complexType> - <annotation> - <documentation xml:lang="de">Optionales Support-Flag, Default = true.</documentation> - <documentation xml:lang="en">Optional support flag, default = true.</documentation> - </annotation> - <attributeGroup ref="ebics:OptSupportFlag"/> - </complexType> - </element> - <element name="X509Data" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Parameter zur X.509-Funktionalität.</documentation> - <documentation xml:lang="en">Parameter denoting the X.509 functionality.</documentation> - </annotation> - <complexType> - <attributeGroup ref="ebics:OptSupportFlag"/> - <attribute name="persistent" type="boolean" use="optional" default="false"> - <annotation> - <documentation xml:lang="de">Sind die X.509-Daten der Teilnehmer serverseitig persistent gespeichert?</documentation> - <documentation xml:lang="en">Will the user's X.509 data be stored persistently on server side?</documentation> - </annotation> - </attribute> - </complexType> - </element> - <element name="ClientDataDownload" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Parameter zum Download von Kunden- und Teilnehmerdaten (Auftragsarten HKD/HTD).</documentation> - <documentation xml:lang="en">Parameter denoting the download of customer and user data (order types HKD/HTD).</documentation> - </annotation> - <complexType> - <attributeGroup ref="ebics:OptSupportFlag"/> - </complexType> - </element> - <element name="DownloadableOrderData" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Parameter zum Abruf von Auftragsarten, zu denen Auftragsdaten verfügbar sind (Auftragsart HAA).</documentation> - <documentation xml:lang="en">Parameter denoting the reception of order types which provides downloadable order data (order type HAA).</documentation> - </annotation> - <complexType> - <attributeGroup ref="ebics:OptSupportFlag"/> - </complexType> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="HPDVersionType"> - <annotation> - <documentation xml:lang="de">Datentyp für HPD-Versionsinformationen.</documentation> - </annotation> - <sequence> - <element name="Protocol"> - <annotation> - <documentation xml:lang="de">unterstützte EBICS-Protokollversionen (H...).</documentation> - </annotation> - <simpleType> - <list itemType="ebics:ProtocolVersionType"/> - </simpleType> - </element> - <element name="Authentication"> - <annotation> - <documentation xml:lang="de">unterstützte Versionen der Authentifikation (X...).</documentation> - </annotation> - <simpleType> - <list itemType="ebics:AuthenticationVersionType"/> - </simpleType> - </element> - <element name="Encryption"> - <annotation> - <documentation xml:lang="de">unterstützte Versionen der Verschlüsselung (E...).</documentation> - </annotation> - <simpleType> - <list itemType="ebics:EncryptionVersionType"/> - </simpleType> - </element> - <element name="Signature"> - <annotation> - <documentation xml:lang="de">unterstützte EU-Versionen (A...).</documentation> - </annotation> - <simpleType> - <list itemType="esig:SignatureVersionType"/> - </simpleType> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="HTDReponseOrderDataType"> - <annotation> - <documentation>Datentyp für Auftragsdaten für Auftragsart HTD (Antwort: Kunden- und Teilnehmerdaten des Teilnehmers abholen).</documentation> - </annotation> - <sequence> - <element name="PartnerInfo" type="ebics:PartnerInfoType"> - <annotation> - <documentation xml:lang="de">Kundendaten.</documentation> - <documentation xml:lang="en">Customer data.</documentation> - </annotation> - </element> - <element name="UserInfo" type="ebics:UserInfoType"> - <annotation> - <documentation xml:lang="de">Teilnehmerdaten.</documentation> - <documentation xml:lang="en">User data.</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="HVDResponseOrderDataType"> - <annotation> - <documentation xml:lang="de">Datentyp für Auftragsdaten für Auftragsart HVD (Antwort: VEU-Status abrufen).</documentation> - </annotation> - <sequence> - <element name="DataDigest" type="ebics:DataDigestType"> - <annotation> - <documentation xml:lang="de">Hashwert der Auftragsdaten.</documentation> - <documentation xml:lang="en">Hash value of the order data.</documentation> - </annotation> - </element> - <element name="DisplayFile" type="base64Binary"> - <annotation> - <documentation xml:lang="de">Begleitzettel/"Displaydatei" (entspricht der Dateianzeige im Kundenprotokoll gemäß DFÜ-Abkommen).</documentation> - <documentation xml:lang="en">Accompanying ticket/"display file" (corresponds to the display file of the customer's journal according to the document "DFÜ-Abkommen").</documentation> - </annotation> - </element> - <element name="OrderDataAvailable" type="boolean"> - <annotation> - <documentation xml:lang="de">Kann die Auftragsdatei im Originalformat abgeholt werden? (HVT mit completeOrderData=true)</documentation> - <documentation xml:lang="en">Can the order file be downloaded in the original format? (HVT with completeOrderData=true)</documentation> - </annotation> - </element> - <element name="OrderDataSize" type="positiveInteger"> - <annotation> - <documentation xml:lang="de">Größe der unkomprimierten Auftragsdaten in Bytes.</documentation> - <documentation xml:lang="en">Size of the uncompressed order data (byte count).</documentation> - </annotation> - </element> - <element name="OrderDetailsAvailable" type="boolean"> - <annotation> - <documentation xml:lang="de">Können die Auftragsdetails als XML-Dokument HVTResponseOrderData abgeholt werden? (HVT mit completeOrderData=false)</documentation> - <documentation xml:lang="en">Can the order details be downloaded as XML document HVTResponseOrderData? (HVT with completeOrderData=false)</documentation> - </annotation> - </element> - <element name="BankSignature" type="ebics:SignatureType" minOccurs="0" maxOccurs="0"> - <annotation> - <documentation xml:lang="de">bankfachliche Elektronische Unterschrift des Kreditinstituts über Hashwert und Displaydatei.</documentation> - <documentation xml:lang="en">Digital Signature issued by the bank, covering the hash value and the accompanying ticket.</documentation> - </annotation> - </element> - <element name="SignerInfo" type="ebics:SignerInfoType" minOccurs="0" maxOccurs="unbounded"> - <annotation> - <documentation xml:lang="de">Informationen zu den bisherigen Unterzeichnern.</documentation> - <documentation xml:lang="en">Information about the already existing signers.</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="HVDOrderParamsType"> - <annotation> - <documentation xml:lang="de">Datentyp für zusätzliche Auftragsparameter für Auftragsart HVD.</documentation> - </annotation> - <sequence> - <group ref="ebics:HVRequestStructure"/> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="HVEOrderParamsType"> - <annotation> - <documentation xml:lang="de">Datentyp für zusätzliche Auftragsparameter für Auftragsart HVE.</documentation> - </annotation> - <sequence> - <group ref="ebics:HVRequestStructure"/> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="HVSOrderParamsType"> - <annotation> - <documentation xml:lang="de">Datentyp für zusätzliche Auftragsparameter für Auftragsart HVS.</documentation> - </annotation> - <sequence> - <group ref="ebics:HVRequestStructure"/> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="HVSRequestOrderDataType"> - <annotation> - <documentation xml:lang="de">Datentyp für Auftragsdaten für Auftragsart HVS (Anfrage: VEU-Storno).</documentation> - </annotation> - <sequence> - <element name="CancelledDataDigest" type="ebics:DataDigestType"> - <annotation> - <documentation xml:lang="de">Hashwert der Auftragsdaten des stornierten Auftrags.</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="HVTResponseOrderDataType"> - <annotation> - <documentation xml:lang="de">Datentyp für Antwort mit Einzelauftraginfos für Auftragsart HVT (Antwort VEU-Transaktionsdetails abrufen mit completeOrderData="false").</documentation> - </annotation> - <sequence> - <element name="NumOrderInfos" type="ebics:NumOrderInfosType"> - <annotation> - <documentation xml:lang="de">Gesamtanzahl der Einzelaufträge für den Auftrag.</documentation> - <documentation xml:lang="en">Total number of order infos for the order.</documentation> - </annotation> - </element> - <element name="OrderInfo" type="ebics:HVTOrderInfoType" maxOccurs="unbounded"> - <annotation> - <documentation xml:lang="de">Einzelauftragsinfos.</documentation> - <documentation xml:lang="en">Particular order content information requested for display matters.</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="HVTAccountInfoType"> - <annotation> - <documentation xml:lang="de">Datentyp für HVT-Konteninformationen.</documentation> - <documentation xml:lang="en">Data type for account information regarding order type HVT.</documentation> - </annotation> - <complexContent> - <extension base="ebics:AttributedAccountType"/> - </complexContent> - </complexType> - <complexType name="HVTOrderInfoType"> - <annotation> - <documentation xml:lang="de">Datentyp für HVT-Auftragsinformationen.</documentation> - </annotation> - <sequence> - <element name="OrderFormat" type="ebics:OrderFormatType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Auftragsformat (z.B. DTAZV).</documentation> - </annotation> - </element> - <element name="AccountInfo" type="ebics:HVTAccountInfoType" minOccurs="2" maxOccurs="3"> - <annotation> - <documentation xml:lang="de">kontobezogene Details des Auftrags (Auftraggeber, Empfänger etc.).</documentation> - </annotation> - </element> - <element name="ExecutionDate" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Ausführungsdatum.</documentation> - </annotation> - <complexType> - <simpleContent> - <extension base="date"/> - </simpleContent> - </complexType> - </element> - <element name="Amount"> - <annotation> - <documentation xml:lang="de">Betrag.</documentation> - </annotation> - <complexType> - <simpleContent> - <extension base="ebics:AmountValueType"> - <attribute name="isCredit" type="boolean" use="optional"> - <annotation> - <documentation xml:lang="de">Gutschrift (isCredit = "true") oder Lastschrift (isCredit = "false")?</documentation> - </annotation> - </attribute> - <attribute name="Currency" type="ebics:CurrencyBaseType" use="optional"> - <annotation> - <documentation xml:lang="de">Währungscode.</documentation> - </annotation> - </attribute> - </extension> - </simpleContent> - </complexType> - </element> - <element name="Description" minOccurs="0" maxOccurs="4"> - <annotation> - <documentation xml:lang="de">Textfeld zur weiteren Beschreibung der Transaktion (Verwendungszweck, Auftragsdetails, Kommentar).</documentation> - </annotation> - <complexType> - <simpleContent> - <extension base="string"> - <attribute name="Type" use="required"> - <annotation> - <documentation xml:lang="de">Beschreibungstyp.</documentation> - </annotation> - <simpleType> - <restriction base="token"> - <enumeration value="Purpose"> - <annotation> - <documentation xml:lang="de">Verwendungszweck</documentation> - </annotation> - </enumeration> - <enumeration value="Details"> - <annotation> - <documentation xml:lang="de">Auftragsdetails</documentation> - </annotation> - </enumeration> - <enumeration value="Comment"> - <annotation> - <documentation xml:lang="de">Kommentar</documentation> - </annotation> - </enumeration> - </restriction> - </simpleType> - </attribute> - </extension> - </simpleContent> - </complexType> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="HVTOrderFlagsType"> - <annotation> - <documentation xml:lang="de">Datentyp für HVT-Auftragsflags.</documentation> - </annotation> - <attribute name="completeOrderData" type="boolean" use="required"> - <annotation> - <documentation xml:lang="de">Sollen die Transaktionsdetails als Einzelauftragsinfos (completeOrderData=false) oder als komplette Originaldaten (completeOrderData=true) übertragen werden? (Vorschlag für Default=false)</documentation> - <documentation xml:lang="en">Are the transaction details so be transmitted as particular order content information requested for display matters or in complete order data file form? (Proposal for Default=false)</documentation> - </annotation> - </attribute> - <attribute name="fetchLimit" use="required"> - <annotation> - <documentation xml:lang="de">Limit für die zu liefernden Transaktionsdetails, bei completeOrderData=false maximale Anzahl zu liefernder Einzelauftragsinfos, 0 für unbegrenzt (Vorschlag für Default=100).</documentation> - <documentation xml:lang="en">Limit for the transaction details to be transmitted; if completeOrderData=false, maximum number of details of a particular order; 0 for unlimited number of details (Proposal for Default=100).</documentation> - </annotation> - <simpleType> - <restriction base="nonNegativeInteger"> - <totalDigits value="10"/> - </restriction> - </simpleType> - </attribute> - <attribute name="fetchOffset" use="required"> - <annotation> - <documentation xml:lang="de">Offset vom Anfang der Originalauftragsdatei für die zu liefernden Transaktionsdetails, bei completeOrderData=false bezogen auf laufende Nummer des Einzelauftrags (Vorschlag für Default=0).</documentation> - <documentation xml:lang="en">Offset position in the original order file which marks the starting point for the transaction details to be transmitted; applies to the sequential number of a particular order if completeOrderData=false (Proposal for Default=0).</documentation> - </annotation> - <simpleType> - <restriction base="nonNegativeInteger"> - <totalDigits value="10"/> - </restriction> - </simpleType> - </attribute> - <anyAttribute namespace="##targetNamespace" processContents="strict"/> - </complexType> - <complexType name="HVTOrderParamsType"> - <annotation> - <documentation xml:lang="de">Datentyp für zusätzliche Auftragsparameter für Auftragsart HVT.</documentation> - <documentation xml:lang="en">Data type for additional order parameters for order type HVT.</documentation> - </annotation> - <sequence> - <group ref="ebics:HVRequestStructure"/> - <element name="OrderFlags"> - <annotation> - <documentation xml:lang="de">spezielle Flags für HVT-Aufträge.</documentation> - <documentation xml:lang="en">Special order flags for orders of type HVT.</documentation> - </annotation> - <complexType> - <complexContent> - <extension base="ebics:HVTOrderFlagsType"/> - </complexContent> - </complexType> - </element> - <element ref="ebics:Parameter" minOccurs="0" maxOccurs="unbounded"> - <annotation> - <documentation xml:lang="de">Generische Schlüssel-Wert-Parameter</documentation> - <documentation xml:lang="en">Generic key-value parameters</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="HVUResponseOrderDataType"> - <annotation> - <documentation xml:lang="de">Datentyp für Auftragsdaten für Auftragsart HVU (Antwort: VEU-Übersicht abholen).</documentation> - </annotation> - <sequence> - <annotation> - <documentation xml:lang="de"/> - </annotation> - <element name="OrderDetails" type="ebics:HVUOrderDetailsType" maxOccurs="unbounded"> - <annotation> - <documentation xml:lang="de">Auftragsinformationen.</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="HVUOrderDetailsType"> - <annotation> - <documentation xml:lang="de">Datentyp für HVU-Auftragsdetails.</documentation> - </annotation> - <sequence> - <element name="OrderType" type="ebics:OrderTBaseType"> - <annotation> - <documentation xml:lang="de">Auftragsart lt. DFÜ-Abkommen des ausgewählten Auftrags.</documentation> - <documentation xml:lang="en">Type of the order.</documentation> - </annotation> - </element> - <element name="FileFormat" type="ebics:FileFormatType" minOccurs="0"> - <annotation> - <documentation xml:lang="en">Identification of the file format in the case of FUL/FDL</documentation> - </annotation> - </element> - <element name="OrderID" type="ebics:OrderIDType"> - <annotation> - <documentation xml:lang="de">Auftragsnummer lt. DFÜ-Abkommen des ausgewählten Auftrags.</documentation> - </annotation> - </element> - <element name="OrderDataSize" type="positiveInteger"> - <annotation> - <documentation xml:lang="de">Größe der unkomprimierten Auftragsdaten in Bytes.</documentation> - </annotation> - </element> - <element name="SigningInfo" type="ebics:HVUSigningInfoType"> - <annotation> - <documentation xml:lang="de">Informationen zu den Unterschriftsmodalitäten.</documentation> - </annotation> - </element> - <element name="SignerInfo" type="ebics:SignerInfoType" minOccurs="0" maxOccurs="unbounded"> - <annotation> - <documentation xml:lang="de">Informationen zu den bisherigen Unterzeichnern.</documentation> - </annotation> - </element> - <element name="OriginatorInfo" type="ebics:HVUOriginatorInfoType"> - <annotation> - <documentation xml:lang="de">Informationen zum Einreicher.</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="HVUOrderParamsType"> - <annotation> - <documentation xml:lang="de">Datentyp für zusätzliche Auftragsparameter für Auftragsart HVU.</documentation> - </annotation> - <sequence> - <element name="OrderTypes" type="ebics:OrderTListType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Liste von Auftragsarten, für die zur Unterschrift vorliegende Aufträge abgerufen werden sollen; falls nicht angegeben, werden sämtliche für den Teilnehmer unterschriftsfähigen Aufträge abgerufen.</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="HVZOrderParamsType"> - <annotation> - <documentation xml:lang="de">Datentyp für zusätzliche Auftragsparameter für Auftragsart HVZ.</documentation> - </annotation> - <sequence> - <element name="OrderTypes" type="ebics:OrderTListType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Liste von Auftragsarten, für die zur Unterschrift vorliegende Aufträge abgerufen werden sollen; falls nicht angegeben, werden sämtliche für den Teilnehmer unterschriftsfähigen Aufträge abgerufen.</documentation> - <documentation xml:lang="de">List of order types that the orders ready to be signed by the requesting user should match; if not specified, a list of all orders ready to be signed by the requesting user is returned.</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="HVUSigningInfoType"> - <annotation> - <documentation xml:lang="de">Datentyp für Informationen zu den HVU-Unterschriftsmodalitäten.</documentation> - </annotation> - <attribute name="readyToBeSigned" type="boolean" use="required"> - <annotation> - <documentation xml:lang="de">Ist der Auftrag unterschriftsreif ("true") oder bereits vom Teilnehmer unterschrieben ("false")?</documentation> - </annotation> - </attribute> - <attribute name="NumSigRequired" type="positiveInteger" use="required"> - <annotation> - <documentation xml:lang="de">Anzahl der insgesamt zur Freigabe erforderlichen EUs.</documentation> - </annotation> - </attribute> - <attribute name="NumSigDone" type="nonNegativeInteger" use="required"> - <annotation> - <documentation xml:lang="de">Anzahl der bereits geleisteten EUs.</documentation> - </annotation> - </attribute> - </complexType> - <complexType name="HVUOriginatorInfoType"> - <annotation> - <documentation xml:lang="de">Datentyp für Informationen zum Ersteller eines HVU-Auftrags.</documentation> - </annotation> - <sequence> - <element name="PartnerID" type="ebics:PartnerIDType"> - <annotation> - <documentation xml:lang="de">Kunden-ID des Einreichers.</documentation> - </annotation> - </element> - <element name="UserID" type="ebics:UserIDType"> - <annotation> - <documentation xml:lang="de">Teilnehmer-ID des Einreichers.</documentation> - </annotation> - </element> - <element name="Name" type="ebics:NameType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Name des Einreichers.</documentation> - </annotation> - </element> - <element name="Timestamp" type="ebics:TimestampType"> - <annotation> - <documentation xml:lang="de">Zeitstempel der Einreichung (d.h. der Übertragung der Auftragsdatei).</documentation> - </annotation> - </element> - <any namespace="##targetNamespace" processContents="strict" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="HVZResponseOrderDataType"> - <annotation> - <documentation xml:lang="de">Datentyp für Auftragsdaten für Auftragsart HVZ (Antwort: VEU-Übersicht mit Zusatzinformationen abholen).</documentation> - <documentation xml:lang="en">Order data for order type HVZ (response: receive summary of orders currently stored in the distributed signature processing unit with additional informations).</documentation> - </annotation> - <sequence> - <annotation> - <documentation xml:lang="de"/> - </annotation> - <element name="OrderDetails" type="ebics:HVZOrderDetailsType" maxOccurs="unbounded"> - <annotation> - <documentation xml:lang="de">Auftragsinformationen.</documentation> - <documentation xml:lang="en">Summary of order information.</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="HVZOrderDetailsType"> - <annotation> - <documentation xml:lang="de">Datentyp für HVZ-Auftragsdetails.</documentation> - </annotation> - <sequence> - <element name="OrderType" type="ebics:OrderTBaseType"> - <annotation> - <documentation xml:lang="de">Auftragsart lt. DFÜ-Abkommen des ausgewählten Auftrags.</documentation> - <documentation xml:lang="en">Type of the order.</documentation> - </annotation> - </element> - <element name="FileFormat" type="ebics:FileFormatType" minOccurs="0"> - <annotation> - <documentation xml:lang="en">Identification of the file format in the case of FUL/FDL</documentation> - </annotation> - </element> - <element name="OrderID" type="ebics:OrderIDType"> - <annotation> - <documentation xml:lang="de">Auftragsnummer lt. DFÜ-Abkommen des ausgewählten Auftrags.</documentation> - <documentation xml:lang="en">ID number of the order.</documentation> - </annotation> - </element> - <element name="DataDigest" type="ebics:DataDigestType"> - <annotation> - <documentation xml:lang="de">Hashwert der Auftragsdaten.</documentation> - <documentation xml:lang="en">Hash value of the order data.</documentation> - </annotation> - </element> - <element name="OrderDataAvailable" type="boolean"> - <annotation> - <documentation xml:lang="de">Kann die Auftragsdatei im Originalformat abgeholt werden? (HVT mit completeOrderData=true).</documentation> - <documentation xml:lang="en">Can the order file be downloaded in the original format? (HVT with completeOrderData=true)</documentation> - </annotation> - </element> - <element name="OrderDataSize" type="positiveInteger"> - <annotation> - <documentation xml:lang="de">Größe der unkomprimierten Auftragsdaten in Bytes.</documentation> - <documentation xml:lang="en">Size of uncompressed order data in Bytes.</documentation> - </annotation> - </element> - <element name="OrderDetailsAvailable" type="boolean"> - <annotation> - <documentation xml:lang="de">Können die Auftragsdetails als XML-Dokument HVTResponseOrderData abgeholt werden? (HVT mit completeOrderData=false).</documentation> - <documentation xml:lang="en">Can the order details be downloaded as XML document HVTResponseOrderData? (HVT with completeOrderData=false)</documentation> - </annotation> - </element> - <group ref="ebics:HVZPaymentOrderDetailsStructure" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Zusätzliche Auftragsdetails nur für Zahlungsaufträge.</documentation> - <documentation xml:lang="en">Order details related to payment orders only.</documentation> - </annotation> - </group> - <element name="SigningInfo" type="ebics:HVUSigningInfoType"> - <annotation> - <documentation xml:lang="de">Informationen zu den Unterschriftsmodalitäten.</documentation> - <documentation xml:lang="en">Information regarding the signing modalities of the order.</documentation> - </annotation> - </element> - <element name="SignerInfo" type="ebics:SignerInfoType" minOccurs="0" maxOccurs="unbounded"> - <annotation> - <documentation xml:lang="de">Informationen zu den bisherigen Unterzeichnern.</documentation> - <documentation xml:lang="en">Information regarding the users who already signed the order.</documentation> - </annotation> - </element> - <element name="OriginatorInfo" type="ebics:HVUOriginatorInfoType"> - <annotation> - <documentation xml:lang="de">Informationen zum Einreicher.</documentation> - <documentation xml:lang="en">Information regarding the originator of the order.</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <group name="HVZPaymentOrderDetailsStructure"> - <annotation> - <documentation xml:lang="de">Standard-Requeststruktur für HVx-Aufträge (HVD, HVT, HVE, HVS).</documentation> - <documentation xml:lang="en">Standard structure for HVZ OrderDetails related to payment orders</documentation> - </annotation> - <sequence> - <element name="TotalOrders" type="nonNegativeInteger" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Anzahl der Zahlungssätze über alle logische Dateien entsprechend Dateianzeige.</documentation> - <documentation xml:lang="en">Total transaction number for all logical files (from dispay file).</documentation> - </annotation> - </element> - <element name="TotalAmount" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Summe der Beträge über alle logische Dateien entsprechend Dateianzeige.</documentation> - <documentation xml:lang="en">Total transaction amount for all logical files (from dispay file).</documentation> - </annotation> - <complexType> - <simpleContent> - <extension base="ebics:AmountValueType"> - <attribute name="isCredit" type="boolean" use="optional"> - <annotation> - <documentation xml:lang="de">Nur Gutschriften (isCredit = "true") oder nur Lastschriften (isCredit = "false")? Sonst keine Nutzung des Elements.</documentation> - </annotation> - </attribute> - </extension> - </simpleContent> - </complexType> - </element> - <element name="Currency" type="ebics:CurrencyBaseType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Auftragswährung (nur bei sortenreinen Zahlungen, sonst keine Angabe).</documentation> - <documentation xml:lang="en">Order currency (only if identical across all transactions, ship otherwise).</documentation> - </annotation> - </element> - <element name="FirstOrderInfo" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Informationen aus Dateianzeige der ersten logischen Datei.</documentation> - <documentation xml:lang="en">Order details from display file for first logical file.</documentation> - </annotation> - <complexType> - <sequence> - <element name="OrderPartyInfo" type="normalizedString" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Auftraggeber entsprechend Dateianzeige.</documentation> - <documentation xml:lang="en">Order party information (from display file).</documentation> - </annotation> - </element> - <element name="AccountInfo" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Erstes Auftraggeberkonto entsprechend Dateianzeige.</documentation> - <documentation xml:lang="en">First order party account (from display file).</documentation> - </annotation> - <complexType> - <sequence> - <choice maxOccurs="2"> - <element name="AccountNumber"> - <annotation> - <documentation xml:lang="de">Kontonummer (deutsches Format oder international als IBAN).</documentation> - <documentation xml:lang="en">Account number (German format or international as IBAN).</documentation> - </annotation> - <complexType> - <simpleContent> - <extension base="ebics:AccountNumberType"> - <attribute name="international" type="boolean" use="optional" default="false"> - <annotation> - <documentation xml:lang="de">Ist die Kontonummer im deutschen Format (international=false) oder im internationalen Format (international=true, IBAN) angegeben?</documentation> - <documentation xml:lang="en">Account number given in German format (international=false) or in international format (international=true, IBAN)?</documentation> - </annotation> - </attribute> - </extension> - </simpleContent> - </complexType> - </element> - <element name="NationalAccountNumber"> - <annotation> - <documentation xml:lang="de">Kontonummer im freien Format.</documentation> - <documentation xml:lang="en">Account number in free format.</documentation> - </annotation> - <complexType> - <simpleContent> - <extension base="ebics:NationalAccountNumberType"> - <attribute name="format" type="token" use="required"> - <annotation> - <documentation xml:lang="de">Formatkennung.</documentation> - <documentation xml:lang="en">Format type.</documentation> - </annotation> - </attribute> - </extension> - </simpleContent> - </complexType> - </element> - </choice> - <choice maxOccurs="2"> - <element name="BankCode"> - <annotation> - <documentation xml:lang="de">Bankleitzahl (deutsches Format oder international als SWIFT-BIC).</documentation> - <documentation xml:lang="en">Bank sort code (German format or international as SWIFT-BIC).</documentation> - </annotation> - <complexType> - <simpleContent> - <extension base="ebics:BankCodeType"> - <attribute name="international" type="boolean" use="optional" default="false"> - <annotation> - <documentation xml:lang="de">Ist die Bankleitzahl im deutschen Format (international=false, BLZ) oder im internationalen Format (international=true, SWIFT-BIC) angegeben?</documentation> - <documentation xml:lang="en">Bank sort code given in German format (international=false) or in international format (international=true, SWIFT-BIC)?</documentation> - </annotation> - </attribute> - <attribute name="Prefix" type="ebics:BankCodePrefixType" use="optional"> - <annotation> - <documentation xml:lang="de">nationales Präfix für Bankleitzahlen.</documentation> - <documentation xml:lang="en">National prefix for bank sort code.</documentation> - </annotation> - </attribute> - </extension> - </simpleContent> - </complexType> - </element> - <element name="NationalBankCode"> - <annotation> - <documentation xml:lang="de">Bankleitzahl im freien Format.</documentation> - <documentation xml:lang="en">Bank sort code in free format.</documentation> - </annotation> - <complexType> - <simpleContent> - <extension base="ebics:NationalBankCodeType"> - <attribute name="format" type="token" use="required"> - <annotation> - <documentation xml:lang="de">Formatkennung.</documentation> - <documentation xml:lang="en">Format type.</documentation> - </annotation> - </attribute> - </extension> - </simpleContent> - </complexType> - </element> - </choice> - </sequence> - </complexType> - </element> - </sequence> - </complexType> - </element> - </sequence> - </group> - <complexType name="SignerInfoType"> - <annotation> - <documentation xml:lang="de">Datentyp für Informationen zu einem Unterzeichner eines VEU-Auftrags (HVU, HVD).</documentation> - </annotation> - <sequence> - <element name="PartnerID" type="ebics:PartnerIDType"> - <annotation> - <documentation xml:lang="de">Kunden-ID des Unterzeichners.</documentation> - </annotation> - </element> - <element name="UserID" type="ebics:UserIDType"> - <annotation> - <documentation xml:lang="de">Teilnehmer-ID des Unterzeichners.</documentation> - </annotation> - </element> - <element name="Name" type="ebics:NameType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Name des Unterzeichners.</documentation> - </annotation> - </element> - <element name="Timestamp" type="ebics:TimestampType"> - <annotation> - <documentation xml:lang="de">Zeitstempel der Unterzeichnung (d.h. der Übertragung der Unterschrift).</documentation> - </annotation> - </element> - <element name="Permission"> - <annotation> - <documentation xml:lang="de">zusätzliche Informationen zu den Berechtigungen des Teilnehmers, der unterzeichnet hat.</documentation> - </annotation> - <complexType> - <attributeGroup ref="ebics:SignerPermission"/> - </complexType> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="UserPermissionType"> - <annotation> - <documentation xml:lang="de">Datentyp für VEU-Berechtigungsinformationen des Teilnehmers (HKD, HTD).</documentation> - </annotation> - <sequence> - <element name="OrderTypes" type="ebics:OrderTListType"> - <annotation> - <documentation xml:lang="de">Liste von Auftragsarten, für die die Berechtigung des Teilnehmers gültig ist.</documentation> - <documentation xml:lang="en">List of order types which the user's permission belongs to.</documentation> - </annotation> - </element> - <element name="FileFormat" type="ebics:FileFormatType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Identifikation des Dateiformats im Falle von FUL/FDL</documentation> - <documentation xml:lang="en">Identification of the file format in the case of FUL/FDL</documentation> - </annotation> - </element> - <element name="AccountID" type="ebics:AccountIDType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Verweis auf den Identifikationscode des berechtigten Kontos.</documentation> - <documentation xml:lang="en">Identification codes of the affected accounts.</documentation> - </annotation> - </element> - <element name="MaxAmount" type="ebics:AmountType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Betragshöchstgrenze, bis zu der die Berechtigung des Teilnehmers gültig ist.</documentation> - <documentation xml:lang="en">Maximum total amount which the user's permission is valid for.</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - <attribute name="AuthorisationLevel" type="ebics:AuthorisationLevelType"> - <annotation> - <documentation xml:lang="de">Unterschriftsklasse, für die der Teilnehmer berechtigt ist; nicht anzugeben bei Download-Auftragsarten.</documentation> - <documentation xml:lang="en">Authorization level of the user who signed the order; to be omitted for orders of type "download".</documentation> - </annotation> - </attribute> - <anyAttribute namespace="##targetNamespace" processContents="strict"/> - </complexType> - <complexType name="PartnerInfoType"> - <annotation> - <documentation xml:lang="de">Datentyp für VEU-Partnerdaten (HKD, HTD).</documentation> - <documentation xml:lang="en">Data type for customer data with regard to distributed signatures (order types HKD, HTD).</documentation> - </annotation> - <sequence> - <element name="AddressInfo" type="ebics:AddressInfoType"> - <annotation> - <documentation xml:lang="de">Informationen zur Adresse des Kunden.</documentation> - <documentation xml:lang="en">Information about the customer's adress.</documentation> - </annotation> - </element> - <element name="BankInfo" type="ebics:BankInfoType"> - <annotation> - <documentation xml:lang="de">Informationen zur Kreditinstitutsanbindung des Kunden.</documentation> - <documentation xml:lang="en">Information about the customer's banking access paramters.</documentation> - </annotation> - </element> - <element name="AccountInfo" minOccurs="0" maxOccurs="unbounded"> - <annotation> - <documentation xml:lang="de">Informationen zu den Konten des Kunden.</documentation> - <documentation xml:lang="en">Information about the customer's accounts.</documentation> - </annotation> - <complexType> - <complexContent> - <extension base="ebics:AccountType"> - <sequence> - <element name="UsageOrderTypes" type="ebics:OrderTListType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Liste der Auftragsartenbeschränkungen; falls nicht angegeben, gibt es keine Auftragsartenbeschränkungen; falls Liste leer, ist das Konto für keine Auftragsart freigegeben.</documentation> - <documentation xml:lang="en">List containing the order types which contain this account is restricted to; if omitted, the account is unrestricted; if the list is empty the account is blocked for any order type.</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - <attribute name="ID" type="ebics:AccountIDType" use="required"> - <annotation> - <documentation xml:lang="de">Identifikationscode des Kontos.</documentation> - </annotation> - </attribute> - </extension> - </complexContent> - </complexType> - </element> - <element name="OrderInfo" type="ebics:AuthOrderInfoType" maxOccurs="unbounded"> - <annotation> - <documentation xml:lang="de">Informationen zu den Auftragsarten, für die der Kunde berechtigt ist.</documentation> - <documentation xml:lang="en">Information about order types which the customer is authorised to use.</documentation> - </annotation> - </element> - </sequence> - </complexType> - <complexType name="AddressInfoType"> - <annotation> - <documentation xml:lang="de">Datentyp für VEU-Adressinformationen (HKD, HTD).</documentation> - <documentation xml:lang="en">Data type for address information with regard to distributed signature (order types HKD, HTD).</documentation> - </annotation> - <sequence> - <element name="Name" type="ebics:NameType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Name des Kunden.</documentation> - <documentation xml:lang="en">Customer's name.</documentation> - </annotation> - </element> - <element name="Street" type="ebics:NameType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Straße und Hausnummer.</documentation> - <documentation xml:lang="en">Street and house number.</documentation> - </annotation> - </element> - <element name="PostCode" type="token" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Postleitzahl.</documentation> - <documentation xml:lang="en">Postal code.</documentation> - </annotation> - </element> - <element name="City" type="ebics:NameType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Stadt.</documentation> - <documentation xml:lang="en">City.</documentation> - </annotation> - </element> - <element name="Region" type="ebics:NameType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Region / Bundesland / Bundesstaat.</documentation> - <documentation xml:lang="en">Region / province / federal state.</documentation> - </annotation> - </element> - <element name="Country" type="ebics:NameType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Land.</documentation> - <documentation xml:lang="en">Country.</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="BankInfoType"> - <annotation> - <documentation xml:lang="de">Datentyp für VEU-Kreditinstitutsinformationen (HKD, HTD).</documentation> - </annotation> - <sequence> - <element name="HostID" type="ebics:HostIDType"> - <annotation> - <documentation xml:lang="de">Banksystem-ID.</documentation> - </annotation> - </element> - <element ref="ebics:Parameter" minOccurs="0" maxOccurs="unbounded"/> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="UserInfoType"> - <annotation> - <documentation xml:lang="de">Datentyp für VEU-Teilnehmerinformationen (HKD, HTD).</documentation> - </annotation> - <sequence> - <element name="UserID"> - <annotation> - <documentation xml:lang="de">Teilnehmer-ID.</documentation> - </annotation> - <complexType> - <simpleContent> - <extension base="ebics:UserIDType"> - <attribute name="Status" type="ebics:UserStatusType" use="required"> - <annotation> - <documentation xml:lang="de">Status des Teilnehmers.</documentation> - </annotation> - </attribute> - </extension> - </simpleContent> - </complexType> - </element> - <element name="Name" type="ebics:NameType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Name des Teilnehmers.</documentation> - </annotation> - </element> - <element name="Permission" type="ebics:UserPermissionType" maxOccurs="unbounded"> - <annotation> - <documentation xml:lang="de">Informationen zu den Berechtigungen des Teilnehmers.</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="AuthOrderInfoType"> - <annotation> - <documentation xml:lang="de">Datentyp für VEU-Berechtigungsinformationen zu Auftragsarten (HKD, HTD).</documentation> - <documentation xml:lang="en">Data type for user permissions with regard to distributed signatures (order types HKD, HTD).</documentation> - </annotation> - <sequence> - <element name="OrderType" type="ebics:OrderTBaseType"> - <annotation> - <documentation xml:lang="de">Auftragsart.</documentation> - </annotation> - </element> - <element name="FileFormat" type="ebics:FileFormatType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Identifikation des Dateiformats im Falle von FUL/FDL</documentation> - <documentation xml:lang="en">Identification of the file format in the case of FUL/FDL</documentation> - </annotation> - </element> - <element name="TransferType" type="ebics:TransferType"> - <annotation> - <documentation xml:lang="de">Transfertyp (Upload/Download).</documentation> - </annotation> - </element> - <element name="OrderFormat" type="ebics:OrderFormatType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Auftragsformat (z.B. DTAZV).</documentation> - </annotation> - </element> - <element name="Description" type="ebics:OrderDescriptionType"> - <annotation> - <documentation xml:lang="de">Beschreibung der Auftragsart.</documentation> - </annotation> - </element> - <element name="NumSigRequired" type="nonNegativeInteger" default="0" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Anzahl erforderlicher EUs (Default=0).</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="StandardOrderParamsType"> - <annotation> - <documentation xml:lang="de">Datentyp für zusätzliche Auftragsparameter bei Standard-Auftragsarten.</documentation> - </annotation> - <sequence> - <element name="DateRange" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Datumsbereich (von-bis).</documentation> - </annotation> - <complexType> - <sequence> - <element name="Start" type="ebics:DateType"> - <annotation> - <documentation xml:lang="de">Startdatum (inkl.).</documentation> - </annotation> - </element> - <element name="End" type="ebics:DateType"> - <annotation> - <documentation xml:lang="de">Enddatum (inkl.).</documentation> - </annotation> - </element> - </sequence> - </complexType> - </element> - </sequence> - </complexType> - <complexType name="GenericOrderParamsType"> - <annotation> - <documentation xml:lang="de">Datentyp für zusätzliche Auftragsparameter für beliebige Auftragsarten.</documentation> - </annotation> - <sequence> - <element ref="ebics:Parameter" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="FULOrderParamsType"> - <annotation> - <documentation xml:lang="de">Datentyp für zusätzliche Auftragsparameter für Auftragsart FUL.</documentation> - </annotation> - <sequence> - <element ref="ebics:Parameter" minOccurs="0" maxOccurs="unbounded"/> - <element name="FileFormat" type="ebics:FileFormatType"> - <annotation> - <documentation xml:lang="de">Bezeichnung des Dateiformats</documentation> - <documentation xml:lang="en">Name of data format</documentation> - </annotation> - </element> - </sequence> - </complexType> - <complexType name="FDLOrderParamsType"> - <annotation> - <documentation xml:lang="de">Datentyp für zusätzliche Auftragsparameter für Auftragsart FDL.</documentation> - </annotation> - <sequence> - <element name="DateRange" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Datumsbereich (von-bis).</documentation> - <documentation xml:lang="en">Range of date (from-to).</documentation> - </annotation> - <complexType> - <sequence> - <element name="Start" type="ebics:DateType"> - <annotation> - <documentation xml:lang="de">Startdatum (inkl.).</documentation> - </annotation> - </element> - <element name="End" type="ebics:DateType"> - <annotation> - <documentation xml:lang="de">Enddatum (inkl.).</documentation> - </annotation> - </element> - </sequence> - </complexType> - </element> - <element ref="ebics:Parameter" minOccurs="0" maxOccurs="unbounded"/> - <element name="FileFormat" type="ebics:FileFormatType"> - <annotation> - <documentation xml:lang="de">Bezeichnung des Dateiformats</documentation> - <documentation xml:lang="en">Name of data format</documentation> - </annotation> - </element> - </sequence> - </complexType> - <attributeGroup name="VersionAttrGroup"> - <annotation> - <documentation xml:lang="de">Attribute zur EBICS-Protokollversion und -revision.</documentation> - <documentation xml:lang="en">Attributes regarding the protocol version and revision of EBICS.</documentation> - </annotation> - <attribute name="Version" type="ebics:ProtocolVersionType" use="required"> - <annotation> - <documentation xml:lang="de">Version des EBICS-Protokolls (z.B. "H00x").</documentation> - <documentation xml:lang="en">Version of the EBICS protocol (e.g. "H00x").</documentation> - </annotation> - </attribute> - <attribute name="Revision" type="ebics:ProtocolRevisionType" use="optional"> - <annotation> - <documentation xml:lang="de">Revision des EBICS-Protokolls (z.B. 1).</documentation> - <documentation xml:lang="en">Revision of the EBICS protocol (e.g. 1).</documentation> - </annotation> - </attribute> - </attributeGroup> -</schema> diff --git a/libeufin-common/src/main/resources/xsd/ebics_orders_H005.xsd b/libeufin-common/src/main/resources/xsd/ebics_orders_H005.xsd @@ -1,2094 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- edited with XMLSpy v2017 sp2 (http://www.altova.com) by EBICS Working Group (July 3rd 2017) - Compared to the status of March 27th 2017 only the element group standardOrderParams was reinserted (as used in schema version H004) and several annotations in English added --> -<schema xmlns="http://www.w3.org/2001/XMLSchema" xmlns:esig="http://www.ebics.org/S002" xmlns:ebics="urn:org:ebics:H005" targetNamespace="urn:org:ebics:H005" elementFormDefault="qualified" attributeFormDefault="unqualified" version="1.0"> - <annotation> - <documentation xml:lang="en">ebics_orders_H005.xsd contains order-based reference elements and order-based type definitions for EBICS.</documentation> - <documentation xml:lang="de">ebics_orders_H005.xsd enthält auftragsbezogene Referenzelemente und auftragsbezogene Typdefinitionen für EBICS.</documentation> - </annotation> - <import namespace="http://www.ebics.org/S002" schemaLocation="ebics_signature_S002.xsd"/> - <include schemaLocation="ebics_types_H005.xsd"/> - <!--Element definitions for the new EBICS order types (Hxx) - Binary interpretation of the XML clear text structure in the context.--> - <element name="EBICSOrderData" abstract="true"> - <annotation> - <documentation xml:lang="de">XML-Klartext-Auftragsdaten für neue EBICS-Auftragsarten.</documentation> - <documentation xml:lang="en">Order data in XML format for new EBICS order types.</documentation> - </annotation> - </element> - <element name="HAAResponseOrderData" type="ebics:HAAResponseOrderDataType" substitutionGroup="ebics:EBICSOrderData"> - <annotation> - <documentation xml:lang="de">Auftragsdaten für Auftragsart HAA (Antwort: abrufbare Auftragsarten abholen).</documentation> - <documentation xml:lang="en">Order data for order type HAA (response: receive downloadable order types).</documentation> - </annotation> - </element> - <element name="HCARequestOrderData" type="ebics:HCARequestOrderDataType" substitutionGroup="ebics:EBICSOrderData"> - <annotation> - <documentation xml:lang="de">Auftragsdaten für Auftragsart HCA (Anfrage: Änderung der Teilnehmerschlüssel für Authentifikation und Verschlüsselung).</documentation> - <documentation xml:lang="en">Order data for order type HCA (request: replace user's keys for authentication and encryption).</documentation> - </annotation> - </element> - <element name="HCSRequestOrderData" type="ebics:HCSRequestOrderDataType" substitutionGroup="ebics:EBICSOrderData"> - <annotation> - <documentation xml:lang="de">Auftragsdaten für Auftragsart HCS (Anfrage: Schlüsselwechsel aller Schlüssel).</documentation> - <documentation xml:lang="en">Order data for order type HCS (request: replace all keys).</documentation> - </annotation> - </element> - <element name="HIARequestOrderData" type="ebics:HIARequestOrderDataType" substitutionGroup="ebics:EBICSOrderData"> - <annotation> - <documentation xml:lang="de">Auftragsdaten für Auftragsart HIA (Anfrage: Initialisierung der Teilnehmerschlüssel für Authentifikation und Verschlüsselung).</documentation> - <documentation xml:lang="en">Order data for order type HIA (request: initialise user's keys for authentication and encryption).</documentation> - </annotation> - </element> - <element name="H3KRequestOrderData" type="ebics:H3KRequestOrderDataType"> - <annotation> - <documentation xml:lang="en">Order data for order type H3K (request: initialise all three user's keys).</documentation> - <documentation xml:lang="de">Auftragsdaten für Auftragsart H3K (Anfrage: Initialisierung aller drei Teilnehmerschlüssel).</documentation> - </annotation> - </element> - <element name="HKDResponseOrderData" type="ebics:HKDResponseOrderDataType" substitutionGroup="ebics:EBICSOrderData"> - <annotation> - <documentation xml:lang="de">Auftragsdaten für Auftragsart HKD (Antwort: Kunden- und Teilnehmerdaten des Kunden abholen).</documentation> - <documentation xml:lang="en">Order data for order type HKD (response: receive customer-based information on the customer and the customer's users).</documentation> - </annotation> - <key name="HKDAccountKey"> - <annotation> - <documentation xml:lang="de">Schlüssel zur Identifikation des Kontos.</documentation> - <documentation xml:lang="en">Key for the identification of the account.</documentation> - </annotation> - <selector xpath="./ebics:PartnerInfo/ebics:AccountInfo"/> - <field xpath="@ID"/> - </key> - <keyref name="HKDAccountRef" refer="ebics:HKDAccountKey"> - <annotation> - <documentation xml:lang="de">Referenz auf die Konten-Identifikationsschlüssel.</documentation> - <documentation xml:lang="en">Reference to the account identification keys.</documentation> - </annotation> - <selector xpath="./ebics:UserInfo/ebics:Permission"/> - <field xpath="ebics:AccountID"/> - </keyref> - </element> - <element name="HPBResponseOrderData" type="ebics:HPBResponseOrderDataType" substitutionGroup="ebics:EBICSOrderData"> - <annotation> - <documentation xml:lang="de">Auftragsdaten für Auftragsart HPB (Antwort: Transfer der Bankschlüssel).</documentation> - <documentation xml:lang="en">Order data for order type HPB (response: receive bank's public keys).</documentation> - </annotation> - </element> - <element name="HPDResponseOrderData" type="ebics:HPDResponseOrderDataType" substitutionGroup="ebics:EBICSOrderData"> - <annotation> - <documentation xml:lang="de">Auftragsdaten für Auftragsart HPD (Antwort: Bankparameter abholen).</documentation> - <documentation xml:lang="en">Order data for order type HPD (response: receive bank parameters).</documentation> - </annotation> - </element> - <element name="HTDResponseOrderData" type="ebics:HTDReponseOrderDataType" substitutionGroup="ebics:EBICSOrderData"> - <annotation> - <documentation xml:lang="de">Auftragsdaten für Auftragsart HTD (Antwort: Kunden- und Teilnehmerdaten des Teilnehmers abholen).</documentation> - <documentation xml:lang="en">Order data for order type HTD (response: receive user-based information on the user's customer and the user herself/himself).</documentation> - </annotation> - <key name="HTDAccountKey"> - <annotation> - <documentation xml:lang="de">Schlüssel zur Identifikation des Kontos.</documentation> - <documentation xml:lang="en">Key for the identification of the account.</documentation> - </annotation> - <selector xpath="./ebics:PartnerInfo/ebics:AccountInfo"/> - <field xpath="@ID"/> - </key> - <keyref name="HTDAccountRef" refer="ebics:HTDAccountKey"> - <annotation> - <documentation xml:lang="de">Referenz auf die Konten-Identifikationsschlüssel.</documentation> - <documentation xml:lang="en">Reference to the account identification keys.</documentation> - </annotation> - <selector xpath="./ebics:UserInfo/ebics:Permission"/> - <field xpath="ebics:AccountID"/> - </keyref> - </element> - <element name="HVDResponseOrderData" type="ebics:HVDResponseOrderDataType" substitutionGroup="ebics:EBICSOrderData"> - <annotation> - <documentation xml:lang="de">Auftragsdaten für Auftragsart HVD (Antwort: VEU-Status abrufen).</documentation> - <documentation xml:lang="en">Order data for order type HVD (response: receive the status of an order currently stored in the distributed signature processing unit).</documentation> - </annotation> - </element> - <element name="HVSRequestOrderData" type="ebics:HVSRequestOrderDataType" substitutionGroup="ebics:EBICSOrderData"> - <annotation> - <documentation xml:lang="de">Auftragsdaten für Auftragsart HVS (Anfrage: VEU-Storno).</documentation> - <documentation xml:lang="en">Order data for order type HVS (request: reject an order currently stored in the distributed signature processing unit).</documentation> - </annotation> - </element> - <element name="HVTResponseOrderData" substitutionGroup="ebics:EBICSOrderData"> - <annotation> - <documentation xml:lang="de">Auftragsdaten für Auftragsart HVT (Antwort: VEU-Transaktionsdetails abrufen).</documentation> - <documentation xml:lang="en">Order data for order type HVT (response: receive transaction details of an order currently stored in the distributed signature processing unit).</documentation> - </annotation> - <complexType> - <complexContent> - <extension base="ebics:HVTResponseOrderDataType"/> - </complexContent> - </complexType> - </element> - <element name="HVUResponseOrderData" type="ebics:HVUResponseOrderDataType" substitutionGroup="ebics:EBICSOrderData"> - <annotation> - <documentation xml:lang="de">Auftragsdaten für Auftragsart HVU (Antwort: VEU-Übersicht abholen).</documentation> - <documentation xml:lang="en">Order data for order type HVU (response: receive summary of orders currently stored in the distributed signature processing unit).</documentation> - </annotation> - </element> - <element name="HVZResponseOrderData" type="ebics:HVZResponseOrderDataType" substitutionGroup="ebics:EBICSOrderData"> - <annotation> - <documentation xml:lang="de">Auftragsdaten für Auftragsart HVZ (Antwort: VEU-Übersicht mit Zusatzinformationen abholen).</documentation> - <documentation xml:lang="en">Order data for order type HVZ (response: receive summary of orders currently stored in the distributed signature processing unit with additional information).</documentation> - </annotation> - </element> - <!--Element definitions for the ES. Binary interpreation of the XML clear text structure within the signature context.--> - <element name="EBICSSignatureData" abstract="true"> - <annotation> - <documentation xml:lang="de">XML-Strukturen für bankfachliche Elektronische Unterschriften (EUs).</documentation> - <documentation xml:lang="en">contains the digital signatures.</documentation> - </annotation> - </element> - <element name="BankSignatureData" type="ebics:BankSignatureDataSigBookType" substitutionGroup="ebics:EBICSSignatureData"> - <annotation> - <documentation xml:lang="de">enthält die EU des Kreditinstituts.</documentation> - <documentation xml:lang="en">contains the digital signatures.</documentation> - </annotation> - </element> - <!--Element definitions for additional order parameters. XML clear text is embedded in the header data.--> - <element name="OrderParams" abstract="true"> - <annotation> - <documentation xml:lang="de">zusätzliche Auftragsparameter, die zur Ausführung des Auftrags notwendig sind.</documentation> - <documentation xml:lang="en">additional order parameters required to execute the order.</documentation> - </annotation> - </element> - <element name="HVDOrderParams" type="ebics:HVDOrderParamsType" substitutionGroup="ebics:OrderParams"> - <annotation> - <documentation xml:lang="de">zusätzliche Auftragsparameter für Auftragsart HVD.</documentation> - <documentation xml:lang="en">additional order parameters for order type HVD.</documentation> - </annotation> - </element> - <element name="HVEOrderParams" type="ebics:HVEOrderParamsType" substitutionGroup="ebics:OrderParams"> - <annotation> - <documentation xml:lang="de">zusätzliche Auftragsparameter für Auftragsart HVE.</documentation> - <documentation xml:lang="en">additional order parameters for order type HVE.</documentation> - </annotation> - </element> - <element name="HVSOrderParams" type="ebics:HVSOrderParamsType" substitutionGroup="ebics:OrderParams"> - <annotation> - <documentation xml:lang="de">zusätzliche Auftragsparameter für Auftragsart HVS.</documentation> - <documentation xml:lang="en">additional order parameters for order type HVS.</documentation> - </annotation> - </element> - <element name="HVTOrderParams" type="ebics:HVTOrderParamsType" substitutionGroup="ebics:OrderParams"> - <annotation> - <documentation xml:lang="de">zusätzliche Auftragsparameter für Auftragsart HVT.</documentation> - <documentation xml:lang="en">additional order parameters for order type HVT.</documentation> - </annotation> - </element> - <element name="HVUOrderParams" type="ebics:HVUOrderParamsType" substitutionGroup="ebics:OrderParams"> - <annotation> - <documentation xml:lang="de">zusätzliche Auftragsparameter für Auftragsart HVU.</documentation> - <documentation xml:lang="en">additional order parameters for order type HVU.</documentation> - </annotation> - </element> - <element name="HVZOrderParams" type="ebics:HVZOrderParamsType" substitutionGroup="ebics:OrderParams"> - <annotation> - <documentation xml:lang="de">zusätzliche Auftragsparameter für Auftragsart HVZ.</documentation> - <documentation xml:lang="en">additional order parameters for order type HVZ.</documentation> - </annotation> - </element> - <element name="StandardOrderParams" type="ebics:StandardOrderParamsType" substitutionGroup="ebics:OrderParams"> - <annotation> - <documentation xml:lang="de">zusätzliche Auftragsparameter für Standard-Auftragsarten.</documentation> - <documentation xml:lang="en">additional order parameters for standard order types.</documentation> - </annotation> - </element> - <!--Element groups and structural information.--> - <group name="HVRequestStructure"> - <annotation> - <documentation xml:lang="de">Standard-Requeststruktur für HVx-Aufträge (HVD, HVT, HVE, HVS).</documentation> - <documentation xml:lang="en">Standard request structure for HVx orders (HVD, HVT, HVE, HVS).</documentation> - </annotation> - <sequence> - <annotation> - <documentation xml:lang="de">Standard-Requestdaten.</documentation> - <documentation xml:lang="en">Standard request data.</documentation> - </annotation> - <element name="PartnerID" type="ebics:PartnerIDType"> - <annotation> - <documentation xml:lang="de">Kunden-ID des Einreichers des ausgewählten Auftrags.</documentation> - <documentation xml:lang="en">Customer ID of the presenter of the selected order.</documentation> - </annotation> - </element> - <element name="Service" type="ebics:RestrictedServiceType"> - <annotation> - <documentation xml:lang="de">BTF Service Parameter struktur im Falle von BTU/BTD</documentation> - <documentation xml:lang="en">Identification of the file format in the case of FUL/FDL</documentation> - </annotation> - </element> - <element name="OrderID" type="ebics:OrderIDType"> - <annotation> - <documentation xml:lang="de">Auftragsnummer des ausgewählten Auftrags.</documentation> - <documentation xml:lang="en">Order ID of the selected order.</documentation> - </annotation> - </element> - </sequence> - </group> - <attributeGroup name="AuthenticationMarker"> - <annotation> - <documentation xml:lang="de">Marker für Elemente und deren Substrukturen, die authentifiziert werden sollen.</documentation> - <documentation xml:lang="en">Marker for elements and their substructures that are to be authenticated.</documentation> - </annotation> - <attribute name="authenticate" type="boolean" use="required" fixed="true"> - <annotation> - <documentation xml:lang="de">Das zugehörige Element ist mitsamt seinen Unterstrukturen zu authentifizieren.</documentation> - <documentation xml:lang="en">The element (and its substructures) that belongs to this attribute is to be authenticated.</documentation> - </annotation> - </attribute> - </attributeGroup> - <attributeGroup name="OptSupportFlag"> - <annotation> - <documentation xml:lang="de">optionales Support-Flag, Default = true.</documentation> - <documentation xml:lang="en">optional support flag, default = true.</documentation> - </annotation> - <attribute name="supported" type="boolean" use="optional" default="true"> - <annotation> - <documentation xml:lang="de">Wird die Funktion unterstützt?</documentation> - <documentation xml:lang="en">Is this function supported?</documentation> - </annotation> - </attribute> - <anyAttribute namespace="##targetNamespace" processContents="strict"/> - </attributeGroup> - <attributeGroup name="SignerPermission"> - <annotation> - <documentation xml:lang="de">EU-Berechtigungsinformationen.</documentation> - <documentation xml:lang="en">permission information of a user's digital signature.</documentation> - </annotation> - <attribute name="AuthorisationLevel" type="ebics:AuthorisationLevelType" use="required"> - <annotation> - <documentation xml:lang="de">Unterschriftsberechtigung des Teilnehmers, der unterzeichnet hat.</documentation> - <documentation xml:lang="en">Authorisation level of the user that signed the order.</documentation> - </annotation> - </attribute> - <anyAttribute namespace="##targetNamespace" processContents="strict"/> - </attributeGroup> - <!--Type definitions and order-related complex types.--> - <complexType name="BankSignatureDataSigBookType"> - <annotation> - <documentation xml:lang="de">Datentyp für Signaturdaten des Kreditinstituts beim EU-Transfer.</documentation> - <documentation xml:lang="en">Data type for digital signature data transferred using EBICS.</documentation> - </annotation> - <sequence> - <element name="OrderSignature" minOccurs="0" maxOccurs="0"> - <annotation> - <documentation xml:lang="de">bankfachliche Elektronische Unterschrift.</documentation> - <documentation xml:lang="en">Digital signature (either autorising an order or applied for transportation).</documentation> - </annotation> - <complexType> - <simpleContent> - <extension base="ebics:SignatureType"/> - </simpleContent> - </complexType> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="PreValidationRequestType"> - <annotation> - <documentation xml:lang="de">Datentyp für Vorabprüfung (Anfrage).</documentation> - <documentation xml:lang="en">Data type for pre-validation (request).</documentation> - </annotation> - <sequence> - <annotation> - <documentation xml:lang="de">Client sendet den Hashwert der Auftragsdaten und alle weiteren Daten, die er im Rahmen der Vorabprüfung zur Verfügung stellen will</documentation> - </annotation> - <element name="DataDigest" type="ebics:DataDigestType" minOccurs="0" maxOccurs="unbounded"> - <annotation> - <documentation xml:lang="de">Hashwert der zu übertragenden Auftragsdatendatei für die Vorabprüfung.</documentation> - <documentation xml:lang="en">Hashvalue of the transmitted order data for the prevalidation.</documentation> - </annotation> - </element> - <element name="AccountAuthorisation" type="ebics:PreValidationAccountAuthType" minOccurs="0" maxOccurs="unbounded"> - <annotation> - <documentation xml:lang="de">Kontoangabe zur Kontoberechtigung für diesen Zahlungsverkehrsauftrag bei der Vorabprüfung.</documentation> - <documentation xml:lang="en">Account information for authorisation checks for the payment order within the prevalidation.</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="PreValidationAccountAuthType"> - <annotation> - <documentation xml:lang="de">Datentyp für Kontenberechtigungsdaten zur Vorabprüfung.</documentation> - <documentation xml:lang="en">Data type for the account authorisation data for the prevalidation.</documentation> - </annotation> - <complexContent> - <extension base="ebics:AccountType"> - <sequence> - <element name="Amount" type="ebics:AmountType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Summe der Zahlungsverkehrsaufträge dieses Kontos für die Höchstbetragsprüfung der EU.</documentation> - <documentation xml:lang="en">Total sum of the ordered payments regarding this account in order to check the maximum amount limit of the signature permission grades.</documentation> - </annotation> - </element> - </sequence> - </extension> - </complexContent> - </complexType> - <complexType name="DataTransferRequestType"> - <annotation> - <documentation xml:lang="de">Datentyp für den Transfer von Auftragsdaten (Anfrage).</documentation> - <documentation xml:lang="en">Data type for the transfer of order data (request).</documentation> - </annotation> - <sequence> - <choice> - <annotation> - <documentation xml:lang="de">Transaktionsphase?</documentation> - </annotation> - <sequence> - <annotation> - <documentation xml:lang="de">Initialisierungsphase: Transfer der Signaturdaten (EUs) und des Transaktionsschlüssels.</documentation> - <documentation xml:lang="en">Inituialisation phase: Transfer of signatur data (ESs) and transaktion key.</documentation> - </annotation> - <element name="DataEncryptionInfo"> - <annotation> - <documentation xml:lang="de">Information zur Verschlüsselung der Signatur- und Auftragsdaten.</documentation> - <documentation xml:lang="en">Information regarding the encryption of signature and order data.</documentation> - </annotation> - <complexType> - <complexContent> - <extension base="ebics:DataEncryptionInfoType"> - <attributeGroup ref="ebics:AuthenticationMarker"/> - </extension> - </complexContent> - </complexType> - </element> - <element name="SignatureData"> - <annotation> - <documentation xml:lang="de">enthält Signaturdaten (EUs).</documentation> - <documentation xml:lang="en">contains signature data (ESs).</documentation> - </annotation> - <complexType> - <simpleContent> - <extension base="ebics:SignatureDataType"> - <attributeGroup ref="ebics:AuthenticationMarker"/> - </extension> - </simpleContent> - </complexType> - </element> - <element name="DataDigest" type="ebics:DataDigestType"> - <annotation> - <documentation xml:lang="de">Hashwert der Auftragsdaten.</documentation> - <documentation xml:lang="de">Hashvalue of the order data.</documentation> - </annotation> - </element> - <element name="AdditionalOrderInfo" type="ebics:String255Type" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Additional Information about the order (unstructured, up to 255 characters).</documentation> - </annotation> - </element> - </sequence> - <sequence> - <annotation> - <documentation xml:lang="de">Transferphase: Transfer von Auftragsdaten.</documentation> - <documentation xml:lang="en">Transferphase: Transfer of order data.</documentation> - </annotation> - <element name="OrderData"> - <annotation> - <documentation xml:lang="de">enthält Auftragsdaten.</documentation> - <documentation xml:lang="en">contains order data.</documentation> - </annotation> - <complexType> - <simpleContent> - <extension base="ebics:OrderDataType"> - <anyAttribute namespace="##targetNamespace" processContents="lax"/> - </extension> - </simpleContent> - </complexType> - </element> - </sequence> - </choice> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="DataTransferResponseType"> - <annotation> - <documentation xml:lang="de">Datentyp für den Transfer von Auftragsdaten (Antwort).</documentation> - </annotation> - <sequence> - <sequence minOccurs="0"> - <annotation> - <documentation xml:lang="de">Transfer des Sitzungsschlüssels und (optional) der Signaturdaten (EUs); nur in der Initialisierungsphase anzugeben.</documentation> - <documentation xml:lang="en">Transfer of the session key and (optional) signature data (ESs); to be specified only in the initialisation phase.</documentation> - </annotation> - <element name="DataEncryptionInfo"> - <annotation> - <documentation xml:lang="de">Information zur Verschlüsselung der Signatur- und Auftragsdaten.</documentation> - <documentation xml:lang="en">Information regarding the encryption of signature and order data.</documentation> - </annotation> - <complexType> - <complexContent> - <extension base="ebics:DataEncryptionInfoType"> - <attributeGroup ref="ebics:AuthenticationMarker"/> - </extension> - </complexContent> - </complexType> - </element> - <element name="SignatureData" minOccurs="0" maxOccurs="0"> - <annotation> - <documentation xml:lang="de">enthält Signaturdaten (EUs).</documentation> - <documentation xml:lang="en">contains signature data (ESs).</documentation> - </annotation> - <complexType> - <simpleContent> - <extension base="ebics:SignatureDataType"> - <attributeGroup ref="ebics:AuthenticationMarker"/> - </extension> - </simpleContent> - </complexType> - </element> - </sequence> - <element name="OrderData"> - <annotation> - <documentation xml:lang="de">enthält Auftragsdaten.</documentation> - <documentation xml:lang="en">contains order data.</documentation> - </annotation> - <complexType> - <simpleContent> - <extension base="ebics:OrderDataType"> - <anyAttribute namespace="##targetNamespace" processContents="lax"/> - </extension> - </simpleContent> - </complexType> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="TransferReceiptRequestType"> - <annotation> - <documentation xml:lang="de">Datentyp für den Transfer von Transferquittungen.</documentation> - <documentation xml:lang="en">Data type for the transfer of transfer receipts.</documentation> - </annotation> - <sequence> - <element name="ReceiptCode" type="ebics:ReceiptCodeType"> - <annotation> - <documentation xml:lang="de">Quittierungscode für Auftragsdatentransfer.</documentation> - <documentation xml:lang="en">Receipt code fpr transfer of order data.</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="TransferReceiptResponseType"> - <annotation> - <documentation xml:lang="de">Datentyp für den Transfer von Antwortcodes.</documentation> - </annotation> - <sequence> - <element name="ReturnCodeReceipt" type="ebics:ReturnCodeType"> - <annotation> - <documentation xml:lang="de">Antwortcode für den vorangegangenen Transfer.</documentation> - <documentation xml:lang="en">response code for the foregoing transfer.</documentation> - </annotation> - </element> - <element name="TimestampBankParameter" type="ebics:TimestampType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Zeitstempel der letzten Aktualisierung der Bankparameter.</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="HAAResponseOrderDataType"> - <annotation> - <documentation xml:lang="de">Datentyp für Auftragsdaten für Auftragsart HAA (Antwort: abrufbare Auftragsarten abholen).</documentation> - <documentation xml:lang="en">Data type for order data of order type HAA (Response: Download of available order data).</documentation> - </annotation> - <sequence> - <element name="Service" type="ebics:RestrictedServiceType" minOccurs="0" maxOccurs="unbounded"> - <annotation> - <documentation xml:lang="de">Liste von Auftragsarten, für die Daten bereit stehen.</documentation> - <documentation xml:lang="en">List of order types for which data are available.</documentation> - </annotation> - </element> - <any namespace="##other" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="HCARequestOrderDataType"> - <annotation> - <documentation xml:lang="de">Datentyp für Auftragsdaten für Auftragsart HCA (Anfrage: Änderung der Teilnehmerschlüssel für Authentifikation und Verschlüsselung).</documentation> - <documentation xml:lang="en">Data type for order data regarding order type HCA (Request: Update of Subscriber's key for authentication and encryption).</documentation> - </annotation> - <sequence> - <element name="AuthenticationPubKeyInfo" type="ebics:AuthenticationPubKeyInfoType"> - <annotation> - <documentation xml:lang="de">öffentlicher Authentifikationsschlüssel.</documentation> - <documentation xml:lang="en">public key for authentication.</documentation> - </annotation> - </element> - <element name="EncryptionPubKeyInfo" type="ebics:EncryptionPubKeyInfoType"> - <annotation> - <documentation xml:lang="de">öffentlicher Verschlüsselungsschlüssel.</documentation> - <documentation xml:lang="en">public key for encryption.</documentation> - </annotation> - </element> - <element name="PartnerID" type="ebics:PartnerIDType"> - <annotation> - <documentation xml:lang="de">Kunden-ID.</documentation> - <documentation xml:lang="en">Partner-ID.</documentation> - </annotation> - </element> - <element name="UserID" type="ebics:UserIDType"> - <annotation> - <documentation xml:lang="de">Teilnehmer-ID.</documentation> - <documentation xml:lang="en">User-ID.</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="HCSRequestOrderDataType"> - <annotation> - <documentation xml:lang="de">Datentyp für Auftragsdaten für Auftragsart HCS (Anfrage: Schlüsselwechsel aller Schlüssel).</documentation> - <documentation xml:lang="en">Data type for order data for order type HCS (Request: Update of all keys).</documentation> - </annotation> - <sequence> - <element name="AuthenticationPubKeyInfo" type="ebics:AuthenticationPubKeyInfoType"> - <annotation> - <documentation xml:lang="de">öffentlicher Authentifikationsschlüssel.</documentation> - <documentation xml:lang="en">public key for authentication.</documentation> - </annotation> - </element> - <element name="EncryptionPubKeyInfo" type="ebics:EncryptionPubKeyInfoType"> - <annotation> - <documentation xml:lang="de">öffentlicher Verschlüsselungsschlüssel.</documentation> - <documentation xml:lang="en">public key for encryption.</documentation> - </annotation> - </element> - <element ref="esig:SignaturePubKeyInfo"/> - <element name="PartnerID" type="ebics:PartnerIDType"> - <annotation> - <documentation xml:lang="de">Kunden-ID.</documentation> - <documentation xml:lang="en">Partner-ID.</documentation> - </annotation> - </element> - <element name="UserID" type="ebics:UserIDType"> - <annotation> - <documentation xml:lang="de">Teilnehmer-ID.</documentation> - <documentation xml:lang="en">User-ID.</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="HIARequestOrderDataType"> - <annotation> - <documentation xml:lang="de">Datentyp für Auftragsdaten für Auftragsart HIA (Anfrage: Initialisierung der Teilnehmerschlüssel für Authentifikation und Verschlüsselung).</documentation> - <documentation xml:lang="en">Data type for order data for order type HIA (Request: Initialisation of subcriber keys for authentication and encryption).</documentation> - </annotation> - <sequence> - <element name="AuthenticationPubKeyInfo" type="ebics:AuthenticationPubKeyInfoType"> - <annotation> - <documentation xml:lang="de">öffentlicher Authentifikationsschlüssel.</documentation> - <documentation xml:lang="en">public key for authentication.</documentation> - </annotation> - </element> - <element name="EncryptionPubKeyInfo" type="ebics:EncryptionPubKeyInfoType"> - <annotation> - <documentation xml:lang="de">öffentlicher Verschlüsselungsschlüssel.</documentation> - <documentation xml:lang="en">public key for encryption.</documentation> - </annotation> - </element> - <element name="PartnerID" type="ebics:PartnerIDType"> - <annotation> - <documentation xml:lang="de">Kunden-ID.</documentation> - <documentation xml:lang="en">Partner-ID.</documentation> - </annotation> - </element> - <element name="UserID" type="ebics:UserIDType"> - <annotation> - <documentation xml:lang="de">Teilnehmer-ID.</documentation> - <documentation xml:lang="en">User-ID.</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="H3KRequestOrderDataType"> - <annotation> - <documentation xml:lang="de">Datentyp für Auftragsdaten für Auftragsart H3K (Anfrage: Initialisierung aller drei Teilnehmerschlüssel).</documentation> - <documentation xml:lang="en">Order type for order data H3K (request: initialise all three user's keys).</documentation> - </annotation> - <sequence> - <element name="SignatureCertificateInfo" type="ebics:SignatureCertificateInfoType"> - <annotation> - <documentation xml:lang="en">Key for electronic Signature</documentation> - <documentation xml:lang="de">Signaturschlüssel.</documentation> - </annotation> - </element> - <element name="AuthenticationCertificateInfo" type="ebics:AuthenticationCertificateInfoType"> - <annotation> - <documentation xml:lang="en">Authentication key</documentation> - <documentation xml:lang="de">Authentifikationsschlüssel.</documentation> - </annotation> - </element> - <element name="EncryptionCertificateInfo" type="ebics:EncryptionCertificateInfoType"> - <annotation> - <documentation xml:lang="en">Encryption key</documentation> - <documentation xml:lang="de">Verschlüsselungsschlüssel.</documentation> - </annotation> - </element> - <element name="PartnerID" type="ebics:PartnerIDType"> - <annotation> - <documentation xml:lang="en">PartnerID.</documentation> - <documentation xml:lang="de">Kunden-ID.</documentation> - </annotation> - </element> - <element name="UserID" type="ebics:UserIDType"> - <annotation> - <documentation xml:lang="en">UserID.</documentation> - <documentation xml:lang="de">Teilnehmer-ID.</documentation> - </annotation> - </element> - </sequence> - </complexType> - <complexType name="HKDResponseOrderDataType"> - <annotation> - <documentation xml:lang="de">Datentyp für Auftragsdaten für Auftragsart HKD (Antwort: Kunden- und Teilnehmerdaten des Kunden abholen).</documentation> - <documentation xml:lang="en">Order data for order type HKD (response: receive customer based information on the customer and the customer's user.</documentation> - </annotation> - <sequence> - <element name="PartnerInfo" type="ebics:PartnerInfoType"> - <annotation> - <documentation xml:lang="de">Kundendaten.</documentation> - <documentation xml:lang="en">Customer data.</documentation> - </annotation> - </element> - <element name="UserInfo" type="ebics:UserInfoType" maxOccurs="unbounded"> - <annotation> - <documentation xml:lang="de">Teilnehmerdaten.</documentation> - <documentation xml:lang="en">User data.</documentation> - </annotation> - </element> - <any namespace="##other" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="HPBResponseOrderDataType"> - <annotation> - <documentation xml:lang="de">Datentyp für Auftragsdaten für Auftragsart HPB (Antwort: Transfer der Bankschlüssel).</documentation> - <documentation xml:lang="en">Data type for order data for order type HPB (Response: Transfer of bank keys).</documentation> - </annotation> - <sequence> - <element name="AuthenticationPubKeyInfo" type="ebics:AuthenticationPubKeyInfoType"> - <annotation> - <documentation xml:lang="de">öffentlicher Authentifikationsschlüssel.</documentation> - <documentation xml:lang="en">public authentication key</documentation> - </annotation> - </element> - <element name="EncryptionPubKeyInfo" type="ebics:EncryptionPubKeyInfoType"> - <annotation> - <documentation xml:lang="de">öffentlicher Verschlüsselungsschlüssel.</documentation> - <documentation xml:lang="en">public encryption key</documentation> - </annotation> - </element> - <element ref="esig:SignaturePubKeyInfo" minOccurs="0" maxOccurs="0"> - <annotation> - <documentation xml:lang="de">öffentlicher EU-Signaturschlüssel.</documentation> - <documentation xml:lang="en">public ES key.</documentation> - </annotation> - </element> - <element name="HostID" type="ebics:HostIDType"> - <annotation> - <documentation xml:lang="de">Banksystem-ID.</documentation> - <documentation xml:lang="en">Host-ID.</documentation> - </annotation> - </element> - <any namespace="##other" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="HPDResponseOrderDataType"> - <annotation> - <documentation xml:lang="de">Datentyp für Auftragsdaten für Auftragsart HPD (Antwort: Bankparameter abholen).</documentation> - <documentation xml:lang="en">Data type for order data for order type HPD (Response: Download bank parameters).</documentation> - </annotation> - <sequence> - <element name="AccessParams" type="ebics:HPDAccessParamsType"> - <annotation> - <documentation xml:lang="de">Zugangsparameter.</documentation> - <documentation xml:lang="en">Access Parameter.</documentation> - </annotation> - </element> - <element name="ProtocolParams" type="ebics:HPDProtocolParamsType"> - <annotation> - <documentation xml:lang="de">Protokollparameter.</documentation> - <documentation xml:lang="en">Protocol Parameter.</documentation> - </annotation> - </element> - </sequence> - </complexType> - <complexType name="HPDAccessParamsType"> - <annotation> - <documentation xml:lang="de">Datentyp für HPD-Zugangsparameter.</documentation> - <documentation xml:lang="en">data type for HPD Access Parameter.</documentation> - </annotation> - <sequence> - <element name="URL" maxOccurs="unbounded"> - <annotation> - <documentation xml:lang="de">institutsspezifische IP-Adresse/URL.</documentation> - <documentation xml:lang="en">individual IP-address/URL of the bank.</documentation> - </annotation> - <complexType> - <simpleContent> - <extension base="anyURI"> - <attribute name="valid_from" type="ebics:TimestampType"> - <annotation> - <documentation xml:lang="de">Gültigkeitsbeginn für die angegebene URL/IP.</documentation> - <documentation xml:lang="en">Valid-From-Date of the URL/IP.</documentation> - </annotation> - </attribute> - </extension> - </simpleContent> - </complexType> - </element> - <element name="Institute"> - <annotation> - <documentation xml:lang="de">Institutsbezeichnung.</documentation> - <documentation xml:lang="en">Name of the bank.</documentation> - </annotation> - <simpleType> - <restriction base="normalizedString"> - <maxLength value="80"/> - </restriction> - </simpleType> - </element> - <element name="HostID" type="ebics:HostIDType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Banksystem-ID.</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="HPDProtocolParamsType"> - <annotation> - <documentation xml:lang="de">Datentyp für HPD-Protokollparameter.</documentation> - <documentation xml:lang="en">Data type for HPD's parameters regarding the EBICS protocol.</documentation> - </annotation> - <sequence> - <element name="Version" type="ebics:HPDVersionType"> - <annotation> - <documentation xml:lang="de">Spezifikation unterstützter Versionen.</documentation> - <documentation xml:lang="en">Specification of supported versions..</documentation> - </annotation> - </element> - <element name="Recovery" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Parameter zur Recovery-Funktion (Wiederaufnahme abgebrochener Übertragungen).</documentation> - <documentation xml:lang="en">Parameter denoting the recovery function (recovery of aborted transmissions).</documentation> - </annotation> - <complexType> - <attributeGroup ref="ebics:OptSupportFlag"/> - </complexType> - </element> - <element name="PreValidation" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Parameter zur Vorabprüfung (über die Übermittlung der EU hinaus).</documentation> - <documentation xml:lang="en">Parameter denoting the pre-validation (beyond transmission of signatures).</documentation> - </annotation> - <complexType> - <annotation> - <documentation xml:lang="de">Optionales Support-Flag, Default = true.</documentation> - <documentation xml:lang="en">Optional support flag, default = true.</documentation> - </annotation> - <attributeGroup ref="ebics:OptSupportFlag"/> - </complexType> - </element> - <element name="ClientDataDownload" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Parameter zum Download von Kunden- und Teilnehmerdaten (Auftragsarten HKD/HTD).</documentation> - <documentation xml:lang="en">Parameter denoting the download of customer and user data (order types HKD/HTD).</documentation> - </annotation> - <complexType> - <attributeGroup ref="ebics:OptSupportFlag"/> - </complexType> - </element> - <element name="DownloadableOrderData" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Parameter zum Abruf von Auftragsarten, zu denen Auftragsdaten verfügbar sind (Auftragsart HAA).</documentation> - <documentation xml:lang="en">Parameter denoting the reception of order types which provides downloadable order data (order type HAA).</documentation> - </annotation> - <complexType> - <attributeGroup ref="ebics:OptSupportFlag"/> - </complexType> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="HPDVersionType"> - <annotation> - <documentation xml:lang="de">Datentyp für HPD-Versionsinformationen.</documentation> - <documentation xml:lang="en">Data type for HPD version information.</documentation> - </annotation> - <sequence> - <element name="Protocol"> - <annotation> - <documentation xml:lang="de">unterstützte EBICS-Protokollversionen (H...).</documentation> - <documentation xml:lang="en">supported EBICS protocol versions. (H...).</documentation> - </annotation> - <simpleType> - <list itemType="ebics:ProtocolVersionType"/> - </simpleType> - </element> - <element name="Authentication"> - <annotation> - <documentation xml:lang="de">unterstützte Versionen der Authentifikation (X...).</documentation> - <documentation xml:lang="en">supported version for authentication (X...).</documentation> - </annotation> - <simpleType> - <list itemType="ebics:AuthenticationVersionType"/> - </simpleType> - </element> - <element name="Encryption"> - <annotation> - <documentation xml:lang="de">unterstützte Versionen der Verschlüsselung (E...).</documentation> - <documentation xml:lang="en">supported version for encryption (E...).</documentation> - </annotation> - <simpleType> - <list itemType="ebics:EncryptionVersionType"/> - </simpleType> - </element> - <element name="Signature"> - <annotation> - <documentation xml:lang="de">unterstützte EU-Versionen (A...).</documentation> - <documentation xml:lang="en">supported version for ES (A...).</documentation> - </annotation> - <simpleType> - <list itemType="esig:SignatureVersionType"/> - </simpleType> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="HTDReponseOrderDataType"> - <annotation> - <documentation>Datentyp für Auftragsdaten für Auftragsart HTD (Antwort: Kunden- und Teilnehmerdaten des Teilnehmers abholen).</documentation> - <documentation xml:lang="en">Data type for order data for order type HTD (Response: Download partner- and user data).</documentation> - </annotation> - <sequence> - <element name="PartnerInfo" type="ebics:PartnerInfoType"> - <annotation> - <documentation xml:lang="de">Kundendaten.</documentation> - <documentation xml:lang="en">Customer data.</documentation> - </annotation> - </element> - <element name="UserInfo" type="ebics:UserInfoType"> - <annotation> - <documentation xml:lang="de">Teilnehmerdaten.</documentation> - <documentation xml:lang="en">User data.</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="HVDResponseOrderDataType"> - <annotation> - <documentation xml:lang="de">Datentyp für Auftragsdaten für Auftragsart HVD (Antwort: VEU-Status abrufen).</documentation> - <documentation xml:lang="en">Data type for order data for order type HVD (Response: EDS-status).</documentation> - </annotation> - <sequence> - <element name="DataDigest" type="ebics:DataDigestType"> - <annotation> - <documentation xml:lang="de">Hashwert der Auftragsdaten.</documentation> - <documentation xml:lang="en">Hash value of the order data.</documentation> - </annotation> - </element> - <element name="DisplayFile" type="base64Binary"> - <annotation> - <documentation xml:lang="de">Begleitzettel/"Displaydatei" (entspricht der Dateianzeige im Kundenprotokoll gemäß DFÜ-Abkommen).</documentation> - <documentation xml:lang="en">Accompanying ticket/"display file" (corresponds to the display file of the customer's journal according to the document "DFÜ-Abkommen").</documentation> - </annotation> - </element> - <element name="OrderDataAvailable" type="boolean"> - <annotation> - <documentation xml:lang="de">Kann die Auftragsdatei im Originalformat abgeholt werden? (HVT mit completeOrderData=true)</documentation> - <documentation xml:lang="en">Can the order file be downloaded in the original format? (HVT with completeOrderData=true)</documentation> - </annotation> - </element> - <element name="OrderDataSize" type="positiveInteger"> - <annotation> - <documentation xml:lang="de">Größe der unkomprimierten Auftragsdaten in Bytes.</documentation> - <documentation xml:lang="en">Size of the uncompressed order data (byte count).</documentation> - </annotation> - </element> - <element name="OrderDetailsAvailable" type="boolean"> - <annotation> - <documentation xml:lang="de">Können die Auftragsdetails als XML-Dokument HVTResponseOrderData abgeholt werden? (HVT mit completeOrderData=false)</documentation> - <documentation xml:lang="en">Can the order details be downloaded as XML document HVTResponseOrderData? (HVT with completeOrderData=false)</documentation> - </annotation> - </element> - <element name="BankSignature" type="ebics:SignatureType" minOccurs="0" maxOccurs="0"> - <annotation> - <documentation xml:lang="de">bankfachliche Elektronische Unterschrift des Kreditinstituts über Hashwert und Displaydatei.</documentation> - <documentation xml:lang="en">Digital Signature issued by the bank, covering the hash value and the accompanying ticket.</documentation> - </annotation> - </element> - <element name="SignerInfo" type="ebics:SignerInfoType" minOccurs="0" maxOccurs="unbounded"> - <annotation> - <documentation xml:lang="de">Informationen zu den bisherigen Unterzeichnern.</documentation> - <documentation xml:lang="en">Information about the already existing signers.</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="HVDOrderParamsType"> - <annotation> - <documentation xml:lang="de">Datentyp für zusätzliche Auftragsparameter für Auftragsart HVD.</documentation> - <documentation xml:lang="en">Data type for additional order parameters for order type HVD.</documentation> - </annotation> - <sequence> - <group ref="ebics:HVRequestStructure"/> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="HVEOrderParamsType"> - <annotation> - <documentation xml:lang="de">Datentyp für zusätzliche Auftragsparameter für Auftragsart HVE.</documentation> - <documentation xml:lang="en">Data type for additional order parameters for order type HVE.</documentation> - </annotation> - <sequence> - <group ref="ebics:HVRequestStructure"/> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="HVSOrderParamsType"> - <annotation> - <documentation xml:lang="de">Datentyp für zusätzliche Auftragsparameter für Auftragsart HVS.</documentation> - <documentation xml:lang="en">Data type for additional order parameters for order type HVS.</documentation> - </annotation> - <sequence> - <group ref="ebics:HVRequestStructure"/> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="HVSRequestOrderDataType"> - <annotation> - <documentation xml:lang="de">Datentyp für Auftragsdaten für Auftragsart HVS (Anfrage: VEU-Storno).</documentation> - <documentation xml:lang="en">Data type for order data for order type HVS (request: EDS cancellation).</documentation> - </annotation> - <sequence> - <element name="CancelledDataDigest" type="ebics:DataDigestType"> - <annotation> - <documentation xml:lang="de">Hashwert der Auftragsdaten des stornierten Auftrags.</documentation> - <documentation xml:lang="en">Hash value of order data of cancelled order.</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="HVTResponseOrderDataType"> - <annotation> - <documentation xml:lang="de">Datentyp für Antwort mit Einzelauftraginfos für Auftragsart HVT (Antwort VEU-Transaktionsdetails abrufen mit completeOrderData="false").</documentation> - <documentation xml:lang="en">Data type for a response containing information about single transactions for order type HVT (response: EDS transaction details with completeOrderData="false").</documentation> - </annotation> - <sequence> - <element name="NumOrderInfos" type="ebics:NumOrderInfosType"> - <annotation> - <documentation xml:lang="de">Gesamtanzahl der Einzelaufträge für den Auftrag.</documentation> - <documentation xml:lang="en">Total number of order infos for the order.</documentation> - </annotation> - </element> - <element name="OrderInfo" type="ebics:HVTOrderInfoType" maxOccurs="unbounded"> - <annotation> - <documentation xml:lang="de">Einzelauftragsinfos.</documentation> - <documentation xml:lang="en">Particular order content information requested for display matters.</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="HVTAccountInfoType"> - <annotation> - <documentation xml:lang="de">Datentyp für HVT-Konteninformationen.</documentation> - <documentation xml:lang="en">Data type for account information regarding order type HVT.</documentation> - </annotation> - <complexContent> - <extension base="ebics:AttributedAccountType"/> - </complexContent> - </complexType> - <complexType name="HVTOrderInfoType"> - <annotation> - <documentation xml:lang="de">//MODIFIED - Replaced Element OrderFormat with MsgName// Datentyp für HVT-Auftragsinformationen.</documentation> - </annotation> - <sequence> - <element name="MsgName" type="ebics:MessageType" minOccurs="0"/> - <element name="AccountInfo" type="ebics:HVTAccountInfoType" minOccurs="2" maxOccurs="3"> - <annotation> - <documentation xml:lang="de">kontobezogene Details des Auftrags (Auftraggeber, Empfänger etc.).</documentation> - <documentation xml:lang="en">account related details of the order (ordering party, receiver etc.).</documentation> - </annotation> - </element> - <element name="ExecutionDate" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Ausführungsdatum.</documentation> - <documentation xml:lang="en">Execution date.</documentation> - </annotation> - <complexType> - <simpleContent> - <extension base="date"/> - </simpleContent> - </complexType> - </element> - <element name="Amount"> - <annotation> - <documentation xml:lang="de">Betrag.</documentation> - <documentation xml:lang="en">Amount.</documentation> - </annotation> - <complexType> - <simpleContent> - <extension base="ebics:AmountValueType"> - <attribute name="isCredit" type="boolean" use="optional"> - <annotation> - <documentation xml:lang="de">Gutschrift (isCredit = "true") oder Lastschrift (isCredit = "false")?</documentation> - <documentation xml:lang="en">Credit (isCredit = "true") or debit (isCredit = "false")?</documentation> - </annotation> - </attribute> - <attribute name="Currency" type="ebics:CurrencyBaseType" use="optional"> - <annotation> - <documentation xml:lang="de">Währungscode.</documentation> - <documentation xml:lang="en">Currency code.</documentation> - </annotation> - </attribute> - </extension> - </simpleContent> - </complexType> - </element> - <element name="Description" minOccurs="0" maxOccurs="4"> - <annotation> - <documentation xml:lang="de">Textfeld zur weiteren Beschreibung der Transaktion (Verwendungszweck, Auftragsdetails, Kommentar).</documentation> - <documentation xml:lang="en">text field for additional descriptions regarding the transaction (remittance information, order details, annotations).</documentation> - </annotation> - <complexType> - <simpleContent> - <extension base="string"> - <attribute name="Type" use="required"> - <annotation> - <documentation xml:lang="de">Beschreibungstyp.</documentation> - <documentation xml:lang="en">Description type.</documentation> - </annotation> - <simpleType> - <restriction base="token"> - <enumeration value="Purpose"> - <annotation> - <documentation xml:lang="de">Verwendungszweck</documentation> - <documentation xml:lang="en">remittance information.</documentation> - </annotation> - </enumeration> - <enumeration value="Details"> - <annotation> - <documentation xml:lang="de">Auftragsdetails</documentation> - <documentation xml:lang="en">Order details.</documentation> - </annotation> - </enumeration> - <enumeration value="Comment"> - <annotation> - <documentation xml:lang="de">Kommentar</documentation> - <documentation xml:lang="en">Annotation.</documentation> - </annotation> - </enumeration> - </restriction> - </simpleType> - </attribute> - </extension> - </simpleContent> - </complexType> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="HVTOrderFlagsType"> - <annotation> - <documentation xml:lang="de">Datentyp für HVT-Auftragsflags.</documentation> - <documentation xml:lang="en">Data type for HVT order flags.</documentation> - </annotation> - <attribute name="completeOrderData" type="boolean" use="required"> - <annotation> - <documentation xml:lang="de">Sollen die Transaktionsdetails als Einzelauftragsinfos (completeOrderData=false) oder als komplette Originaldaten (completeOrderData=true) übertragen werden? (Vorschlag für Default=false)</documentation> - <documentation xml:lang="en">Are the transaction details so be transmitted as particular order content information requested for display matters or in complete order data file form? (Proposal for Default=false)</documentation> - </annotation> - </attribute> - <attribute name="fetchLimit" use="required"> - <annotation> - <documentation xml:lang="de">Limit für die zu liefernden Transaktionsdetails, bei completeOrderData=false maximale Anzahl zu liefernder Einzelauftragsinfos, 0 für unbegrenzt (Vorschlag für Default=100).</documentation> - <documentation xml:lang="en">Limit for the transaction details to be transmitted; if completeOrderData=false, maximum number of details of a particular order; 0 for unlimited number of details (Proposal for Default=100).</documentation> - </annotation> - <simpleType> - <restriction base="nonNegativeInteger"> - <totalDigits value="10"/> - </restriction> - </simpleType> - </attribute> - <attribute name="fetchOffset" use="required"> - <annotation> - <documentation xml:lang="de">Offset vom Anfang der Originalauftragsdatei für die zu liefernden Transaktionsdetails, bei completeOrderData=false bezogen auf laufende Nummer des Einzelauftrags (Vorschlag für Default=0).</documentation> - <documentation xml:lang="en">Offset position in the original order file which marks the starting point for the transaction details to be transmitted; applies to the sequential number of a particular order if completeOrderData=false (Proposal for Default=0).</documentation> - </annotation> - <simpleType> - <restriction base="nonNegativeInteger"> - <totalDigits value="10"/> - </restriction> - </simpleType> - </attribute> - <anyAttribute namespace="##targetNamespace" processContents="strict"/> - </complexType> - <complexType name="HVTOrderParamsType"> - <annotation> - <documentation xml:lang="de">Datentyp für zusätzliche Auftragsparameter für Auftragsart HVT.</documentation> - <documentation xml:lang="en">Data type for additional order parameters for order type HVT.</documentation> - </annotation> - <sequence> - <group ref="ebics:HVRequestStructure"/> - <element name="OrderFlags"> - <annotation> - <documentation xml:lang="de">spezielle Flags für HVT-Aufträge.</documentation> - <documentation xml:lang="en">Special order flags for orders of type HVT.</documentation> - </annotation> - <complexType> - <complexContent> - <extension base="ebics:HVTOrderFlagsType"/> - </complexContent> - </complexType> - </element> - <element ref="ebics:Parameter" minOccurs="0" maxOccurs="unbounded"> - <annotation> - <documentation xml:lang="de">Generische Schlüssel-Wert-Parameter</documentation> - <documentation xml:lang="en">Generic key-value parameters</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="HVUResponseOrderDataType"> - <annotation> - <documentation xml:lang="de">Datentyp für Auftragsdaten für Auftragsart HVU (Antwort: VEU-Übersicht abholen).</documentation> - <documentation xml:lang="en">Data type for order data for order type HVU (Response: Download EDS overview).</documentation> - </annotation> - <sequence> - <annotation> - <documentation xml:lang="de"/> - </annotation> - <element name="OrderDetails" type="ebics:HVUOrderDetailsType" maxOccurs="unbounded"> - <annotation> - <documentation xml:lang="de">Auftragsinformationen.</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="HVUOrderDetailsType"> - <annotation> - <documentation xml:lang="de">Datentyp für HVU-Auftragsdetails.</documentation> - <documentation xml:lang="en">Data type for HVU order details.</documentation> - </annotation> - <sequence> - <element name="Service" type="ebics:RestrictedServiceType"> - <annotation> - <documentation xml:lang="de">Auftragsart lt. DFÜ-Abkommen des ausgewählten Auftrags.</documentation> - <documentation xml:lang="en">Type of the order.</documentation> - </annotation> - </element> - <element name="OrderID" type="ebics:OrderIDType"> - <annotation> - <documentation xml:lang="de">Auftragsnummer lt. DFÜ-Abkommen des ausgewählten Auftrags.</documentation> - <documentation xml:lang="en">Order number.</documentation> - </annotation> - </element> - <element name="OrderDataSize" type="positiveInteger"> - <annotation> - <documentation xml:lang="de">Größe der unkomprimierten Auftragsdaten in Bytes.</documentation> - <documentation xml:lang="en">Order data size in bytes.</documentation> - </annotation> - </element> - <element name="SigningInfo" type="ebics:HVUSigningInfoType"> - <annotation> - <documentation xml:lang="de">Informationen zu den Unterschriftsmodalitäten.</documentation> - <documentation xml:lang="en">Signing information.</documentation> - </annotation> - </element> - <element name="SignerInfo" type="ebics:SignerInfoType" minOccurs="0" maxOccurs="unbounded"> - <annotation> - <documentation xml:lang="de">Informationen zu den bisherigen Unterzeichnern.</documentation> - <documentation xml:lang="en">Information regarding the signer.</documentation> - </annotation> - </element> - <element name="OriginatorInfo" type="ebics:HVUOriginatorInfoType"> - <annotation> - <documentation xml:lang="de">Informationen zum Einreicher.</documentation> - <documentation xml:lang="en">Information regarding the originator.</documentation> - </annotation> - </element> - <element name="AdditionalOrderInfo" type="ebics:String255Type" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Additional Information about the order (unstructured, up to 255 characters).</documentation> - <documentation xml:lang="en">Additional Information about the order (unstructured, up to 255 characters).</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="HVUOrderParamsType"> - <annotation> - <documentation xml:lang="de">Datentyp für zusätzliche Auftragsparameter für Auftragsart HVU.</documentation> - <documentation xml:lang="en">Data type for additional order parameters for order type HVU.</documentation> - </annotation> - <sequence> - <element name="ServiceFilter" type="ebics:ServiceType" minOccurs="0" maxOccurs="unbounded"> - <annotation> - <documentation xml:lang="de">Liste von Auftragsarten, für die zur Unterschrift vorliegende Aufträge abgerufen werden sollen; falls nicht angegeben, werden sämtliche für den Teilnehmer unterschriftsfähigen Aufträge abgerufen.</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="HVZOrderParamsType"> - <annotation> - <documentation xml:lang="de">Datentyp für zusätzliche Auftragsparameter für Auftragsart HVZ.</documentation> - <documentation xml:lang="en">Data type for additional order parameters for order type HVZ.</documentation> - </annotation> - <sequence> - <element name="ServiceFilter" type="ebics:ServiceType" minOccurs="0" maxOccurs="unbounded"> - <annotation> - <documentation xml:lang="de">Liste von Auftragsarten, für die zur Unterschrift vorliegende Aufträge abgerufen werden sollen; falls nicht angegeben, werden sämtliche für den Teilnehmer unterschriftsfähigen Aufträge abgerufen.</documentation> - <documentation xml:lang="de">List of order types that the orders ready to be signed by the requesting user should match; if not specified, a list of all orders ready to be signed by the requesting user is returned.</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="HVUSigningInfoType"> - <annotation> - <documentation xml:lang="de">Datentyp für Informationen zu den HVU-Unterschriftsmodalitäten.</documentation> - </annotation> - <attribute name="readyToBeSigned" type="boolean" use="required"> - <annotation> - <documentation xml:lang="de">Ist der Auftrag unterschriftsreif ("true") oder bereits vom Teilnehmer unterschrieben ("false")?</documentation> - </annotation> - </attribute> - <attribute name="NumSigRequired" type="positiveInteger" use="required"> - <annotation> - <documentation xml:lang="de">Anzahl der insgesamt zur Freigabe erforderlichen EUs.</documentation> - </annotation> - </attribute> - <attribute name="NumSigDone" type="nonNegativeInteger" use="required"> - <annotation> - <documentation xml:lang="de">Anzahl der bereits geleisteten EUs.</documentation> - </annotation> - </attribute> - </complexType> - <complexType name="HVUOriginatorInfoType"> - <annotation> - <documentation xml:lang="de">Datentyp für Informationen zum Ersteller eines HVU-Auftrags.</documentation> - </annotation> - <sequence> - <element name="PartnerID" type="ebics:PartnerIDType"> - <annotation> - <documentation xml:lang="de">Kunden-ID des Einreichers.</documentation> - </annotation> - </element> - <element name="UserID" type="ebics:UserIDType"> - <annotation> - <documentation xml:lang="de">Teilnehmer-ID des Einreichers.</documentation> - </annotation> - </element> - <element name="Name" type="ebics:NameType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Name des Einreichers.</documentation> - </annotation> - </element> - <element name="Timestamp" type="ebics:TimestampType"> - <annotation> - <documentation xml:lang="de">Zeitstempel der Einreichung (d.h. der Übertragung der Auftragsdatei).</documentation> - </annotation> - </element> - <any namespace="##targetNamespace" processContents="strict" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="HVZResponseOrderDataType"> - <annotation> - <documentation xml:lang="de">Datentyp für Auftragsdaten für Auftragsart HVZ (Antwort: VEU-Übersicht mit Zusatzinformationen abholen).</documentation> - <documentation xml:lang="en">Order data for order type HVZ (response: receive summary of orders currently stored in the distributed signature processing unit with additional informations).</documentation> - </annotation> - <sequence> - <annotation> - <documentation xml:lang="de"/> - </annotation> - <element name="OrderDetails" type="ebics:HVZOrderDetailsType" maxOccurs="unbounded"> - <annotation> - <documentation xml:lang="de">Auftragsinformationen.</documentation> - <documentation xml:lang="en">Summary of order information.</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="HVZOrderDetailsType"> - <annotation> - <documentation xml:lang="de">Datentyp für HVZ-Auftragsdetails.</documentation> - </annotation> - <sequence> - <element name="Service" type="ebics:RestrictedServiceType"> - <annotation> - <documentation xml:lang="de">BTF Service Parameter-Struktur des ausgewählten Auftrags.</documentation> - <documentation xml:lang="en">Type of the order.</documentation> - </annotation> - </element> - <element name="OrderID" type="ebics:OrderIDType"> - <annotation> - <documentation xml:lang="de">Auftragsnummer lt. DFÜ-Abkommen des ausgewählten Auftrags.</documentation> - <documentation xml:lang="en">ID number of the order.</documentation> - </annotation> - </element> - <element name="DataDigest" type="ebics:DataDigestType"> - <annotation> - <documentation xml:lang="de">Hashwert der Auftragsdaten.</documentation> - <documentation xml:lang="en">Hash value of the order data.</documentation> - </annotation> - </element> - <element name="OrderDataAvailable" type="boolean"> - <annotation> - <documentation xml:lang="de">Kann die Auftragsdatei im Originalformat abgeholt werden? (HVT mit completeOrderData=true).</documentation> - <documentation xml:lang="en">Can the order file be downloaded in the original format? (HVT with completeOrderData=true)</documentation> - </annotation> - </element> - <element name="OrderDataSize" type="positiveInteger"> - <annotation> - <documentation xml:lang="de">Größe der unkomprimierten Auftragsdaten in Bytes.</documentation> - <documentation xml:lang="en">Size of uncompressed order data in Bytes.</documentation> - </annotation> - </element> - <element name="OrderDetailsAvailable" type="boolean"> - <annotation> - <documentation xml:lang="de">Können die Auftragsdetails als XML-Dokument HVTResponseOrderData abgeholt werden? (HVT mit completeOrderData=false).</documentation> - <documentation xml:lang="en">Can the order details be downloaded as XML document HVTResponseOrderData? (HVT with completeOrderData=false)</documentation> - </annotation> - </element> - <group ref="ebics:HVZPaymentOrderDetailsStructure" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Zusätzliche Auftragsdetails nur für Zahlungsaufträge.</documentation> - <documentation xml:lang="en">Order details related to payment orders only.</documentation> - </annotation> - </group> - <element name="SigningInfo" type="ebics:HVUSigningInfoType"> - <annotation> - <documentation xml:lang="de">Informationen zu den Unterschriftsmodalitäten.</documentation> - <documentation xml:lang="en">Information regarding the signing modalities of the order.</documentation> - </annotation> - </element> - <element name="SignerInfo" type="ebics:SignerInfoType" minOccurs="0" maxOccurs="unbounded"> - <annotation> - <documentation xml:lang="de">Informationen zu den bisherigen Unterzeichnern.</documentation> - <documentation xml:lang="en">Information regarding the users who already signed the order.</documentation> - </annotation> - </element> - <element name="OriginatorInfo" type="ebics:HVUOriginatorInfoType"> - <annotation> - <documentation xml:lang="de">Informationen zum Einreicher.</documentation> - <documentation xml:lang="en">Information regarding the originator of the order.</documentation> - </annotation> - </element> - <element name="AdditionalOrderInfo" type="ebics:String255Type" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Additional Information about the order (unstructured, up to 255 characters).</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <group name="HVZPaymentOrderDetailsStructure"> - <annotation> - <documentation xml:lang="de">Standard-Requeststruktur für HVx-Aufträge (HVD, HVT, HVE, HVS).</documentation> - <documentation xml:lang="en">Standard structure for HVZ OrderDetails related to payment orders</documentation> - </annotation> - <sequence> - <element name="TotalOrders" type="nonNegativeInteger" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Anzahl der Zahlungssätze über alle logische Dateien entsprechend Dateianzeige.</documentation> - <documentation xml:lang="en">Total transaction number for all logical files (from dispay file).</documentation> - </annotation> - </element> - <element name="TotalAmount" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Summe der Beträge über alle logische Dateien entsprechend Dateianzeige.</documentation> - <documentation xml:lang="en">Total transaction amount for all logical files (from dispay file).</documentation> - </annotation> - <complexType> - <simpleContent> - <extension base="ebics:AmountValueType"> - <attribute name="isCredit" type="boolean" use="optional"> - <annotation> - <documentation xml:lang="de">Nur Gutschriften (isCredit = "true") oder nur Lastschriften (isCredit = "false")? Sonst keine Nutzung des Elements.</documentation> - </annotation> - </attribute> - </extension> - </simpleContent> - </complexType> - </element> - <element name="Currency" type="ebics:CurrencyBaseType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Auftragswährung (nur bei sortenreinen Zahlungen, sonst keine Angabe).</documentation> - <documentation xml:lang="en">Order currency (only if identical across all transactions, ship otherwise).</documentation> - </annotation> - </element> - <element name="FirstOrderInfo" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Informationen aus Dateianzeige der ersten logischen Datei.</documentation> - <documentation xml:lang="en">Order details from display file for first logical file.</documentation> - </annotation> - <complexType> - <sequence> - <element name="OrderPartyInfo" type="normalizedString" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Auftraggeber entsprechend Dateianzeige.</documentation> - <documentation xml:lang="en">Order party information (from display file).</documentation> - </annotation> - </element> - <element name="AccountInfo" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Erstes Auftraggeberkonto entsprechend Dateianzeige.</documentation> - <documentation xml:lang="en">First order party account (from display file).</documentation> - </annotation> - <complexType> - <sequence> - <choice maxOccurs="2"> - <element name="AccountNumber"> - <annotation> - <documentation xml:lang="de">Kontonummer (deutsches Format oder international als IBAN).</documentation> - <documentation xml:lang="en">Account number (German format or international as IBAN).</documentation> - </annotation> - <complexType> - <simpleContent> - <extension base="ebics:AccountNumberType"> - <attribute name="international" type="boolean" use="optional" default="false"> - <annotation> - <documentation xml:lang="de">Ist die Kontonummer im deutschen Format (international=false) oder im internationalen Format (international=true, IBAN) angegeben?</documentation> - <documentation xml:lang="en">Account number given in German format (international=false) or in international format (international=true, IBAN)?</documentation> - </annotation> - </attribute> - </extension> - </simpleContent> - </complexType> - </element> - <element name="NationalAccountNumber"> - <annotation> - <documentation xml:lang="de">Kontonummer im freien Format.</documentation> - <documentation xml:lang="en">Account number in free format.</documentation> - </annotation> - <complexType> - <simpleContent> - <extension base="ebics:NationalAccountNumberType"> - <attribute name="format" type="token" use="required"> - <annotation> - <documentation xml:lang="de">Formatkennung.</documentation> - <documentation xml:lang="en">Format type.</documentation> - </annotation> - </attribute> - </extension> - </simpleContent> - </complexType> - </element> - </choice> - <choice minOccurs="0" maxOccurs="2"> - <element name="BankCode"> - <annotation> - <documentation xml:lang="de">Bankleitzahl (deutsches Format oder international als SWIFT-BIC).</documentation> - <documentation xml:lang="en">Bank sort code (German format or international as SWIFT-BIC).</documentation> - </annotation> - <complexType> - <simpleContent> - <extension base="ebics:BankCodeType"> - <attribute name="international" type="boolean" use="optional" default="false"> - <annotation> - <documentation xml:lang="de">Ist die Bankleitzahl im deutschen Format (international=false, BLZ) oder im internationalen Format (international=true, SWIFT-BIC) angegeben?</documentation> - <documentation xml:lang="en">Bank sort code given in German format (international=false) or in international format (international=true, SWIFT-BIC)?</documentation> - </annotation> - </attribute> - <attribute name="Prefix" type="ebics:BankCodePrefixType" use="optional"> - <annotation> - <documentation xml:lang="de">nationales Präfix für Bankleitzahlen.</documentation> - <documentation xml:lang="en">National prefix for bank sort code.</documentation> - </annotation> - </attribute> - </extension> - </simpleContent> - </complexType> - </element> - <element name="NationalBankCode"> - <annotation> - <documentation xml:lang="de">Bankleitzahl im freien Format.</documentation> - <documentation xml:lang="en">Bank sort code in free format.</documentation> - </annotation> - <complexType> - <simpleContent> - <extension base="ebics:NationalBankCodeType"> - <attribute name="format" type="token" use="required"> - <annotation> - <documentation xml:lang="de">Formatkennung.</documentation> - <documentation xml:lang="en">Format type.</documentation> - </annotation> - </attribute> - </extension> - </simpleContent> - </complexType> - </element> - </choice> - </sequence> - </complexType> - </element> - </sequence> - </complexType> - </element> - </sequence> - </group> - <complexType name="SignerInfoType"> - <annotation> - <documentation xml:lang="de">Datentyp für Informationen zu einem Unterzeichner eines VEU-Auftrags (HVU, HVD).</documentation> - </annotation> - <sequence> - <element name="PartnerID" type="ebics:PartnerIDType"> - <annotation> - <documentation xml:lang="de">Kunden-ID des Unterzeichners.</documentation> - </annotation> - </element> - <element name="UserID" type="ebics:UserIDType"> - <annotation> - <documentation xml:lang="de">Teilnehmer-ID des Unterzeichners.</documentation> - </annotation> - </element> - <element name="Name" type="ebics:NameType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Name des Unterzeichners.</documentation> - </annotation> - </element> - <element name="Timestamp" type="ebics:TimestampType"> - <annotation> - <documentation xml:lang="de">Zeitstempel der Unterzeichnung (d.h. der Übertragung der Unterschrift).</documentation> - </annotation> - </element> - <element name="Permission"> - <annotation> - <documentation xml:lang="de">zusätzliche Informationen zu den Berechtigungen des Teilnehmers, der unterzeichnet hat.</documentation> - </annotation> - <complexType> - <attributeGroup ref="ebics:SignerPermission"/> - </complexType> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="UserPermissionType"> - <annotation> - <documentation xml:lang="de">Datentyp für VEU-Berechtigungsinformationen des Teilnehmers (HKD, HTD).</documentation> - </annotation> - <sequence> - <element name="AdminOrderType" type="ebics:OrderTBaseType"> - <annotation> - <documentation xml:lang="de">Liste von Auftragsarten, für die die Berechtigung des Teilnehmers gültig ist.</documentation> - <documentation xml:lang="en">List of order types which the user's permission belongs to.</documentation> - </annotation> - </element> - <element name="Service" type="ebics:RestrictedServiceType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">BTF Service Parameter struktur im Falle von BTU/BTD</documentation> - <documentation xml:lang="en">Identification of the file format in the case of FUL/FDL</documentation> - </annotation> - </element> - <element name="AccountID" type="ebics:AccountIDType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Verweis auf den Identifikationscode des berechtigten Kontos.</documentation> - <documentation xml:lang="en">Identification codes of the affected accounts.</documentation> - </annotation> - </element> - <element name="MaxAmount" type="ebics:AmountType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Betragshöchstgrenze, bis zu der die Berechtigung des Teilnehmers gültig ist.</documentation> - <documentation xml:lang="en">Maximum total amount which the user's permission is valid for.</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - <attribute name="AuthorisationLevel" type="ebics:AuthorisationLevelType"> - <annotation> - <documentation xml:lang="de">Unterschriftsklasse, für die der Teilnehmer berechtigt ist; nicht anzugeben bei Download-Auftragsarten.</documentation> - <documentation xml:lang="en">Authorization level of the user who signed the order; to be omitted for orders of type "download".</documentation> - </annotation> - </attribute> - <anyAttribute namespace="##targetNamespace" processContents="strict"/> - </complexType> - <complexType name="PartnerInfoType"> - <annotation> - <documentation xml:lang="de">Datentyp für VEU-Partnerdaten (HKD, HTD).</documentation> - <documentation xml:lang="en">Data type for customer data with regard to distributed signatures (order types HKD, HTD).</documentation> - </annotation> - <sequence> - <element name="AddressInfo" type="ebics:AddressInfoType"> - <annotation> - <documentation xml:lang="de">Informationen zur Adresse des Kunden.</documentation> - <documentation xml:lang="en">Information about the customer's adress.</documentation> - </annotation> - </element> - <element name="BankInfo" type="ebics:BankInfoType"> - <annotation> - <documentation xml:lang="de">Informationen zur Kreditinstitutsanbindung des Kunden.</documentation> - <documentation xml:lang="en">Information about the customer's banking access paramters.</documentation> - </annotation> - </element> - <element name="AccountInfo" minOccurs="0" maxOccurs="unbounded"> - <annotation> - <documentation xml:lang="de">Informationen zu den Konten des Kunden.</documentation> - <documentation xml:lang="en">Information about the customer's accounts.</documentation> - </annotation> - <complexType> - <complexContent> - <extension base="ebics:AccountType"> - <sequence> - <element name="UsageOrderTypes" type="ebics:UsageOrderType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">//MODIFIED//Liste der Auftragsartenbeschränkungen; falls nicht angegeben, gibt es keine Auftragsartenbeschränkungen; falls das Element ohne Service-Element geliefert wird, ist das Konto für keine Auftragsart freigegeben.</documentation> - <documentation xml:lang="en">List containing the order types which contain this account is restricted to; if omitted, the account is unrestricted; if the list is empty the account is blocked for any order type.</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - <attribute name="ID" type="ebics:AccountIDType" use="required"> - <annotation> - <documentation xml:lang="de">Identifikationscode des Kontos.</documentation> - </annotation> - </attribute> - </extension> - </complexContent> - </complexType> - </element> - <element name="OrderInfo" type="ebics:AuthOrderInfoType" maxOccurs="unbounded"> - <annotation> - <documentation xml:lang="de">Informationen zu den Auftragsarten, für die der Kunde berechtigt ist.</documentation> - <documentation xml:lang="en">Information about order types which the customer is authorised to use.</documentation> - </annotation> - </element> - </sequence> - </complexType> - <complexType name="AddressInfoType"> - <annotation> - <documentation xml:lang="de">Datentyp für VEU-Adressinformationen (HKD, HTD).</documentation> - <documentation xml:lang="en">Data type for address information with regard to distributed signature (order types HKD, HTD).</documentation> - </annotation> - <sequence> - <element name="Name" type="ebics:NameType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Name des Kunden.</documentation> - <documentation xml:lang="en">Customer's name.</documentation> - </annotation> - </element> - <element name="Street" type="ebics:NameType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Straße und Hausnummer.</documentation> - <documentation xml:lang="en">Street and house number.</documentation> - </annotation> - </element> - <element name="PostCode" type="token" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Postleitzahl.</documentation> - <documentation xml:lang="en">Postal code.</documentation> - </annotation> - </element> - <element name="City" type="ebics:NameType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Stadt.</documentation> - <documentation xml:lang="en">City.</documentation> - </annotation> - </element> - <element name="Region" type="ebics:NameType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Region / Bundesland / Bundesstaat.</documentation> - <documentation xml:lang="en">Region / province / federal state.</documentation> - </annotation> - </element> - <element name="Country" type="ebics:NameType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Land.</documentation> - <documentation xml:lang="en">Country.</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="BankInfoType"> - <annotation> - <documentation xml:lang="de">Datentyp für VEU-Kreditinstitutsinformationen (HKD, HTD).</documentation> - </annotation> - <sequence> - <element name="HostID" type="ebics:HostIDType"> - <annotation> - <documentation xml:lang="de">Banksystem-ID.</documentation> - </annotation> - </element> - <element ref="ebics:Parameter" minOccurs="0" maxOccurs="unbounded"/> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="UserInfoType"> - <annotation> - <documentation xml:lang="de">Datentyp für VEU-Teilnehmerinformationen (HKD, HTD).</documentation> - </annotation> - <sequence> - <element name="UserID"> - <annotation> - <documentation xml:lang="de">Teilnehmer-ID.</documentation> - </annotation> - <complexType> - <simpleContent> - <extension base="ebics:UserIDType"> - <attribute name="Status" type="ebics:UserStatusType" use="required"> - <annotation> - <documentation xml:lang="de">Status des Teilnehmers.</documentation> - </annotation> - </attribute> - </extension> - </simpleContent> - </complexType> - </element> - <element name="Name" type="ebics:NameType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Name des Teilnehmers.</documentation> - </annotation> - </element> - <element name="Permission" type="ebics:UserPermissionType" maxOccurs="unbounded"> - <annotation> - <documentation xml:lang="de">Informationen zu den Berechtigungen des Teilnehmers.</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="AuthOrderInfoType"> - <annotation> - <documentation xml:lang="de">Datentyp für VEU-Berechtigungsinformationen zu Auftragsarten (HKD, HTD).</documentation> - <documentation xml:lang="en">Data type for user permissions with regard to distributed signatures (order types HKD, HTD).</documentation> - </annotation> - <sequence> - <element name="AdminOrderType" type="ebics:OrderTBaseType"> - <annotation> - <documentation xml:lang="de">Administrative EBICS Auftragsart.</documentation> - </annotation> - </element> - <element name="Service" type="ebics:RestrictedServiceType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">BTF Service Parameter struktur im Falle von BTU/BTD</documentation> - <documentation xml:lang="en">Identification of the file format in the case of FUL/FDL</documentation> - </annotation> - </element> - <element name="Description" type="ebics:OrderDescriptionType"> - <annotation> - <documentation xml:lang="de">Beschreibung der Auftragsart.</documentation> - </annotation> - </element> - <element name="NumSigRequired" type="nonNegativeInteger" default="0" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Anzahl erforderlicher EUs (Default=0).</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="StandardOrderParamsType"> - <annotation> - <documentation xml:lang="de">Datentyp für zusätzliche Auftragsparameter bei Standard-Auftragsarten.</documentation> - </annotation> - <sequence> - <element name="DateRange" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Datumsbereich (von-bis).</documentation> - </annotation> - <complexType> - <sequence> - <element name="Start" type="ebics:DateType"> - <annotation> - <documentation xml:lang="de">Startdatum (inkl.).</documentation> - </annotation> - </element> - <element name="End" type="ebics:DateType"> - <annotation> - <documentation xml:lang="de">Enddatum (inkl.).</documentation> - </annotation> - </element> - </sequence> - </complexType> - </element> - </sequence> - </complexType> - <attributeGroup name="VersionAttrGroup"> - <annotation> - <documentation xml:lang="de">Attribute zur EBICS-Protokollversion und -revision.</documentation> - <documentation xml:lang="en">Attributes regarding the protocol version and revision of EBICS.</documentation> - </annotation> - <attribute name="Version" type="ebics:ProtocolVersionType" use="required"> - <annotation> - <documentation xml:lang="de">Version des EBICS-Protokolls (z.B. "H00x").</documentation> - <documentation xml:lang="en">Version of the EBICS protocol (e.g. "H00x").</documentation> - </annotation> - </attribute> - <attribute name="Revision" type="ebics:ProtocolRevisionType" use="optional"> - <annotation> - <documentation xml:lang="de">Revision des EBICS-Protokolls (z.B. 1).</documentation> - <documentation xml:lang="en">Revision of the EBICS protocol (e.g. 1).</documentation> - </annotation> - </attribute> - </attributeGroup> - <!--Following ComplexTypes are used for BTF--> - <element name="BTDOrderParams" type="ebics:BTDParamsType" substitutionGroup="ebics:OrderParams"> - <annotation> - <documentation>zusätzliche Auftragsparameter für Auftragsart BTD.</documentation> - <documentation xml:lang="en">additional order parameters for order type BTD.</documentation> - </annotation> - </element> - <element name="BTUOrderParams" type="ebics:BTUParamsType" substitutionGroup="ebics:OrderParams"> - <annotation> - <documentation>zusätzliche Auftragsparameter für Auftragsart BTU.</documentation> - <documentation xml:lang="en">additional order parameters for order type BTU.</documentation> - </annotation> - </element> - <complexType name="BTDParamsType"> - <annotation> - <documentation>Datentyp für BTF Download Parameter</documentation> - </annotation> - <complexContent> - <restriction base="ebics:BTFParamsTyp"> - <sequence> - <element name="Service" type="ebics:RestrictedServiceType"> - <annotation> - <documentation>Service name - target system for the further processing of the order</documentation> - </annotation> - </element> - <element name="DateRange" type="ebics:DateRangeType" minOccurs="0"/> - <element ref="ebics:Parameter" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - <attribute name="fileName" type="ebics:FileNameStringType" use="prohibited"> - <annotation> - <documentation>The file name on the client It can be transmitted optionally</documentation> - </annotation> - </attribute> - </restriction> - </complexContent> - </complexType> - <complexType name="BTUParamsType"> - <annotation> - <documentation>Datentyp für BTF Upload Parameter</documentation> - </annotation> - <complexContent> - <restriction base="ebics:BTFParamsTyp"> - <sequence> - <element name="Service" type="ebics:RestrictedServiceType"> - <annotation> - <documentation>Service name - target system for the further processing of the order</documentation> - </annotation> - </element> - <element name="SignatureFlag" type="ebics:SignatureFlagType" minOccurs="0"> - <annotation> - <documentation>If not present the order doesn't contain any ES and shall be authorised outside EBICS -If present the order shall be autorised within EBICS: -1. If the attribute VEU is also present the sender desires spooling into the VEU - hence in this case the order is not rejected in the case of not sufficient number of ES -2. If the attribute is not present all necessary ES must be inside the order (else: rejection of the order) </documentation> - </annotation> - </element> - <element ref="ebics:Parameter" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - <attribute name="fileName" type="ebics:FileNameStringType"> - <annotation> - <documentation>The file name on the client It can be transmitted optionally</documentation> - </annotation> - </attribute> - </restriction> - </complexContent> - </complexType> - <complexType name="BTFParamsTyp" abstract="true"> - <annotation> - <documentation>Abstract Type containing all BTF params structures</documentation> - </annotation> - <sequence> - <element name="Service" type="ebics:RestrictedServiceType"> - <annotation> - <documentation>Service name - target system for the further processing of the order</documentation> - </annotation> - </element> - <element name="SignatureFlag" type="ebics:SignatureFlagType" minOccurs="0"> - <annotation> - <documentation>If not present the order doesn't contain any ES and shall be authorised outside EBICS -If present the order shall be autorised within EBICS: -1. If the attribute VEU is also present the sender desires spooling into the VEU - hence in this case the order is not rejected in the case of not sufficient number of ES -2. If the attribute is not present all necessary ES must be inside the order (else: rejection of the order) </documentation> - </annotation> - </element> - <element name="DateRange" type="ebics:DateRangeType" minOccurs="0"/> - <element ref="ebics:Parameter" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - <attribute name="fileName" type="ebics:FileNameStringType"> - <annotation> - <documentation>The file name on the client It can be transmitted optionally</documentation> - </annotation> - </attribute> - </complexType> - <complexType name="DateRangeType"> - <annotation> - <documentation>Datentyp für die Angabe eines (Berichts-) Zeitraums</documentation> - </annotation> - <sequence> - <element name="Start" type="ebics:DateType"/> - <element name="End" type="ebics:DateType"/> - </sequence> - </complexType> - <complexType name="FlagAttribType"> - <annotation> - <documentation>Basis-Datentyp für Kennzeichen mit optionalem Attribut</documentation> - </annotation> - <anyAttribute/> - </complexType> - <complexType name="MessageType"> - <annotation> - <documentation>Datentyp für Meldungstyp-String mit optionalen Attributen</documentation> - </annotation> - <simpleContent> - <extension base="ebics:MessageNameStringType"> - <attribute name="variant" type="ebics:NumStringType" use="optional"> - <annotation> - <documentation>Variant number of the message type (usable for ISO20022 messages)</documentation> - </annotation> - </attribute> - <attribute name="version" type="ebics:NumStringType" use="optional"> - <annotation> - <documentation>Version number of the message type (usable for ISO20022 messages)</documentation> - </annotation> - </attribute> - <attribute name="format" use="optional"> - <annotation> - <documentation>Encoding format of the message (e.g. XML, ASN1, JSON, PDF)</documentation> - </annotation> - <simpleType> - <restriction base="ebics:CodeStringType"> - <maxLength value="4"/> - </restriction> - </simpleType> - </attribute> - </extension> - </simpleContent> - </complexType> - <complexType name="ServiceType"> - <annotation> - <documentation>Basisdatentyp für BTF-Service Parameter Set</documentation> - </annotation> - <sequence> - <element name="ServiceName" type="ebics:ServiceNameStringType" minOccurs="0"> - <annotation> - <documentation>Service Code name: External list specified and maintained by EBICS. Basis is the "SWIFT-list" for the field "description" (SCT, DCT, XCT, SDD, DDD, STM, REP...) plus additional codes needed for further services </documentation> - </annotation> - </element> - <element name="Scope" type="ebics:ScopeStringType" minOccurs="0"> - <annotation> - <documentation>Specifies whose rules have to be taken into account for the service. This means which market / comminity has defined the rules. -If the element is absent a global definition for the service is assumed. -External list specified and maintained by EBICS. In addition the following codes may be used: -2-character ISO country code or a 3-character issuer code (defined by EBICS)</documentation> - </annotation> - </element> - <element name="ServiceOption" type="ebics:ServiceOptionStringType" minOccurs="0"> - <annotation> - <documentation>Service Option Code -Additional option for the service (also depends on used scope)</documentation> - </annotation> - </element> - <element name="Container" type="ebics:ContainerFlagType" minOccurs="0"> - <annotation> - <documentation>Container flag. If present, data is provided/requested in a container format specified in the attribute of the flag</documentation> - </annotation> - </element> - <element name="MsgName" type="ebics:MessageType" minOccurs="0"> - <annotation> - <documentation>Name of the message, e.g. pain.001 or mt101 National message names (issued by DK, CFONB or SIC are also allowed)</documentation> - </annotation> - </element> - </sequence> - </complexType> - <complexType name="RestrictedServiceType"> - <annotation> - <documentation>Type is arestriction of the generic ServiceType, defining the mandatory elements</documentation> - </annotation> - <complexContent> - <restriction base="ebics:ServiceType"> - <sequence> - <element name="ServiceName" type="ebics:ServiceNameStringType"> - <annotation> - <documentation>Service Code name: External list specified and maintained by EBICS. Basis is the "SWIFT-list" for the field "description" (SCT, DCT, XCT, SDD, DDD, STM, REP...) plus additional codes needed for further services </documentation> - </annotation> - </element> - <element name="Scope" type="ebics:ScopeStringType" minOccurs="0"> - <annotation> - <documentation>Specifies whose rules have to be taken into account for the service. This means which market / comminity has defined the rules. -If the element is absent a global definition for the service is assumed. -External list specified and maintained by EBICS. -In addition the following codes may be used: -2-character ISO country code or a 3-character issuer code (defined by EBICS)</documentation> - </annotation> - </element> - <element name="ServiceOption" type="ebics:ServiceOptionStringType" minOccurs="0"> - <annotation> - <documentation>Service Option Code - Additional option for the service (also depends on used scope)</documentation> - </annotation> - </element> - <element name="Container" type="ebics:ContainerFlagType" minOccurs="0"> - <annotation> - <documentation>Container flag. If present, data is provided/requested in a container format specified in the attribute of the flag</documentation> - </annotation> - </element> - <element name="MsgName" type="ebics:MessageType"> - <annotation> - <documentation>Name of the message, e.g. pain.001 or mt101 National message names (issued by DK, CFONB or SIC are also allowed)</documentation> - </annotation> - </element> - </sequence> - </restriction> - </complexContent> - </complexType> - <complexType name="ContainerFlagType"> - <annotation> - <documentation>Container flag. If present, data is provided/requested in a container format specified in the attribute of the flag</documentation> - </annotation> - <complexContent> - <restriction base="ebics:FlagAttribType"> - <attribute name="containerType" type="ebics:ContainerStringType" use="required"> - <annotation> - <documentation>Specifies the container type - External Codelist defined by EBICS (starting values: XML, ZIP, SVC)</documentation> - </annotation> - </attribute> - </restriction> - </complexContent> - </complexType> - <complexType name="SignatureFlagType"> - <annotation> - <documentation>Datentyp für BTF Signatur-Flag (ersetzt Orderkennzeichen)</documentation> - </annotation> - <complexContent> - <restriction base="ebics:FlagAttribType"> - <attribute name="requestEDS" type="boolean" use="optional"> - <annotation> - <documentation>If present the sender desires spooling into EBICS distributed signature queue, only "true" is allowed</documentation> - </annotation> - </attribute> - </restriction> - </complexContent> - </complexType> - <complexType name="UsageOrderType"> - <annotation> - <documentation>Datentyp zur Kennzeichnung von Auftragsartenbeschränkungen</documentation> - </annotation> - <sequence> - <element name="Service" type="ebics:RestrictedServiceType" minOccurs="0" maxOccurs="unbounded"> - <annotation> - <documentation>Service Parameter-Sets von nicht unterstützten BTF-Auftragsarten</documentation> - </annotation> - </element> - </sequence> - </complexType> - <!--END BTF--> -</schema> diff --git a/libeufin-common/src/main/resources/xsd/ebics_request_H004.xsd b/libeufin-common/src/main/resources/xsd/ebics_request_H004.xsd @@ -1,355 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> - <!-- Mit XMLSpy v2008 rel. 2 (http://www.altova.com) von Sabine Wenzel (SIZ Bonn) bearbeitet --> -<schema xmlns="http://www.w3.org/2001/XMLSchema" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:ebics="urn:org:ebics:H004" targetNamespace="urn:org:ebics:H004" elementFormDefault="qualified" attributeFormDefault="unqualified" version="1.0"> -<annotation> - <documentation xml:lang="de">ebics_request_H004.xsd ist das EBICS-Protokollschema für Anfragen.</documentation> - <documentation xml:lang="en">ebics_request_H004.xsd is the appropriate EBICS protocol schema for standard requests.</documentation> -</annotation> -<include schemaLocation="ebics_types_H004.xsd"/> -<include schemaLocation="ebics_orders_H004.xsd"/> -<import namespace="http://www.w3.org/2000/09/xmldsig#" schemaLocation="xmldsig-core-schema.xsd"/> -<element name="ebicsRequest"> - <annotation> - <documentation xml:lang="de">Electronic Banking Internet Communication Standard of the EBICS SCRL: Multibankfähige Schnittstelle zur internetbasierten Kommunikation.</documentation> - <documentation xml:lang="en">Electronic Banking Internet Communication Standard der EBICS SCRL: multi-bank capable interface for internet-based communication.</documentation> - </annotation> - <complexType> - <sequence> - <element name="header"> - <annotation> - <documentation xml:lang="de">enthält die technischen Transaktionsdaten.</documentation> - <documentation xml:lang="en">contains the transaction-driven data.</documentation> - </annotation> - <complexType> - <sequence> - <element name="static" type="ebics:StaticHeaderType"> - <annotation> - <documentation xml:lang="de">enhält alle festen Headereinträge.</documentation> - <documentation xml:lang="en">contains the static header entries.</documentation> - </annotation> - </element> - <element name="mutable" type="ebics:MutableHeaderType"> - <annotation> - <documentation xml:lang="de">enthält alle variablen Headereinträge.</documentation> - <documentation xml:lang="en">contains the mutable header entries.</documentation> - </annotation> - </element> - </sequence> - <attributeGroup ref="ebics:AuthenticationMarker"/> - </complexType> - </element> - <element ref="ebics:AuthSignature"/> - <element name="body"> - <annotation> - <documentation xml:lang="de">enthält die Auftragsdaten, EU(s) und weitere Nutzdaten.</documentation> - <documentation xml:lang="en">contains order data, order signature(s) and further data referring to the current order.</documentation> - </annotation> - <complexType> - <sequence> - <annotation> - <documentation xml:lang="de"/> - </annotation> - <element ref="ds:X509Data" minOccurs="0" maxOccurs="0"> - <annotation> - <documentation xml:lang="de">X.509-Daten des Teilnehmers.</documentation> - <documentation xml:lang="en">X.509 data of the user.</documentation> - </annotation> - </element> - <choice> - <annotation> - <documentation xml:lang="de">Welche Transaktionsphase?</documentation> - <documentation xml:lang="en">Which transaction phase?</documentation> - </annotation> - <sequence> - <annotation> - <documentation xml:lang="de">Initialisierungs- und Transferphase.</documentation> - <documentation xml:lang="en">Initialisation or transfer phase.</documentation> - </annotation> - <element name="PreValidation" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Daten zur Vorabprüfung; nur anzugeben in der Initialisierungsphase bei Uploads mit Auftragsattribut OZH (EUs + Auftragsdaten).</documentation> - <documentation xml:lang="en">Data sent for pre-validation; mandatory for initialisation phase during uploads using order attribute OZH (order signature(s) + order data).</documentation> - </annotation> - <complexType> - <complexContent> - <extension base="ebics:PreValidationRequestType"> - <attributeGroup ref="ebics:AuthenticationMarker"/> - </extension> - </complexContent> - </complexType> - </element> - <element name="DataTransfer" type="ebics:DataTransferRequestType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Transfer von Signatur- bzw. Auftragsdaten; nur bei Upload anzugeben.</documentation> - <documentation xml:lang="en">Transfer of signature or order data; mandatory for uploads only.</documentation> - </annotation> - </element> - </sequence> - <sequence> - <annotation> - <documentation xml:lang="de">Quittierungsphase nach Download.</documentation> - <documentation xml:lang="en">Receipt phase after download.</documentation> - </annotation> - <element name="TransferReceipt"> - <annotation> - <documentation xml:lang="de">Quittierung des Transfers.</documentation> - <documentation xml:lang="en">Receipt of transfer.</documentation> - </annotation> - <complexType> - <complexContent> - <extension base="ebics:TransferReceiptRequestType"> - <attributeGroup ref="ebics:AuthenticationMarker"/> - </extension> - </complexContent> - </complexType> - </element> - </sequence> - </choice> - </sequence> - </complexType> - </element> - </sequence> - <attributeGroup ref="ebics:VersionAttrGroup"/> - <anyAttribute namespace="##targetNamespace" processContents="strict"/> - </complexType> -</element> -<complexType name="StaticHeaderType"> - <annotation> - <documentation xml:lang="de">Datentyp für den statischen EBICS-Header.</documentation> - <documentation xml:lang="en">Data type for the static EBICS header.</documentation> - </annotation> - <sequence> - <element name="HostID" type="ebics:HostIDType"> - <annotation> - <documentation xml:lang="de">Hostname des Banksystems.</documentation> - </annotation> - </element> - <choice> - <annotation> - <documentation xml:lang="de">Transaktionsphase?</documentation> - <documentation xml:lang="en">Transaction phase?</documentation> - </annotation> - <sequence> - <annotation> - <documentation xml:lang="de">Initialisierungsphase.</documentation> - <documentation xml:lang="en">Initialisation phase.</documentation> - </annotation> - <element name="Nonce" type="ebics:NonceType"> - <annotation> - <documentation xml:lang="de">Zufallswert; damit wird die Initialisierungsnachricht des Clients einzigartig.</documentation> - <documentation xml:lang="en">Random value, ensures the uniqueness of the client's message during initialisation phase.</documentation> - </annotation> - </element> - <element name="Timestamp" type="ebics:TimestampType"> - <annotation> - <documentation xml:lang="de">aktueller Zeitstempel zur Begrenzung der serverseitigen Nonce-Speicherung.</documentation> - <documentation xml:lang="en">current timestamp, used to limit storage space for nonces on the server.</documentation> - </annotation> - </element> - <element name="PartnerID" type="ebics:PartnerIDType"> - <annotation> - <documentation xml:lang="de">Kunden-ID des serverseitig administrierten Kunden.</documentation> - <documentation xml:lang="en">ID of the partner = customer, administered on the server.</documentation> - </annotation> - </element> - <element name="UserID" type="ebics:UserIDType"> - <annotation> - <documentation xml:lang="de">Teilnehmer-ID des serverseitig zu diesem Kunden administrierten Teilnehmers.</documentation> - <documentation xml:lang="en">ID of the user that is assigned to the given customer, administered on the server.</documentation> - </annotation> - </element> - <element name="SystemID" type="ebics:UserIDType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">technische User-ID für Multi-User-Systeme.</documentation> - <documentation xml:lang="en">ID of the system for multi-user systems.</documentation> - </annotation> - </element> - <element name="Product" nillable="true" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Kennung des Kundenprodukts bzw. Herstellerkennung oder Name.</documentation> - <documentation xml:lang="en">software ID / manufacturer ID / manufacturer's name of the customer's software package.</documentation> - </annotation> - <complexType> - <simpleContent> - <extension base="ebics:ProductType"> - <attribute name="Language" type="ebics:LanguageType" use="required"> - <annotation> - <documentation xml:lang="de">Sprachkennzeichen der Kundenproduktversion (gemäß ISO 639).</documentation> - <documentation xml:lang="en">Language code of the customer's software package according to ISO 639.</documentation> - </annotation> - </attribute> - <attribute name="InstituteID" type="ebics:InstituteIDType" use="optional"> - <annotation> - <documentation xml:lang="de">Kennung des Herausgebers des Kundenprodukts bzw. des betreuenden Kreditinstituts.</documentation> - <documentation xml:lang="en">ID of the manufacturer / financial institute providing support for the customer's software package.</documentation> - </annotation> - </attribute> - </extension> - </simpleContent> - </complexType> - </element> - <element name="OrderDetails" type="ebics:StaticHeaderOrderDetailsType"> - <annotation> - <documentation xml:lang="de">Auftragsdetails.</documentation> - <documentation xml:lang="en">order details.</documentation> - </annotation> - </element> - <element name="BankPubKeyDigests"> - <annotation> - <documentation xml:lang="de">Hashwerte der erwarteten öffentlichen Schlüssel (Verschlüsselung, Signatur, Authentifikation) des Kreditinstituts.</documentation> - <documentation xml:lang="en">Digest values of the expected public keys (authentication, encryption, signature) owned by the financial institute.</documentation> - </annotation> - <complexType> - <sequence> - <element name="Authentication"> - <annotation> - <documentation xml:lang="de">Hashwert des Authentifikationsschlüssels.</documentation> - <documentation xml:lang="en">Digest value of the public authentication key.</documentation> - </annotation> - <complexType> - <simpleContent> - <extension base="ebics:PubKeyDigestType"> - <attribute name="Version" type="ebics:AuthenticationVersionType" use="required"> - <annotation> - <documentation xml:lang="de">Version des Authentifikationsverfahrens.</documentation> - <documentation xml:lang="en">Version of the algorithm used for authentication.</documentation> - </annotation> - </attribute> - </extension> - </simpleContent> - </complexType> - </element> - <element name="Encryption"> - <annotation> - <documentation xml:lang="de">Hashwert des Verschlüsselungsschlüssels.</documentation> - <documentation xml:lang="en">Digest value of the public encryption key.</documentation> - </annotation> - <complexType> - <simpleContent> - <extension base="ebics:PubKeyDigestType"> - <attribute name="Version" type="ebics:EncryptionVersionType" use="required"> - <annotation> - <documentation xml:lang="de">Version des Verschlüsselungsverfahrens.</documentation> - <documentation xml:lang="en">Version of the algorithm used for encryption.</documentation> - </annotation> - </attribute> - </extension> - </simpleContent> - </complexType> - </element> - <element name="Signature" minOccurs="0" maxOccurs="0"> - <annotation> - <documentation xml:lang="de">Hashwert des Signaturschlüssels.</documentation> - <documentation xml:lang="en">Digest value of the public signature key.</documentation> - </annotation> - <complexType> - <simpleContent> - <extension base="ebics:PubKeyDigestType"> - <attribute name="Version" type="ebics:SignatureVersionType" use="required"> - <annotation> - <documentation xml:lang="de">Version des Signaturverfahrens.</documentation> - <documentation xml:lang="en">Version of the algorithm used for signature creation.</documentation> - </annotation> - </attribute> - </extension> - </simpleContent> - </complexType> - </element> - </sequence> - </complexType> - </element> - <element name="SecurityMedium" type="ebics:SecurityMediumType"> - <annotation> - <documentation xml:lang="de">Angabe des Sicherheitsmediums, das der Kunde verwendet.</documentation> - <documentation xml:lang="en">Classification of the security medium used by the customer.</documentation> - </annotation> - </element> - <element name="NumSegments" type="ebics:NumSegmentsType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Gesamtsegmentanzahl für diese Transaktion; nur bei Uploads anzugeben.</documentation> - <documentation xml:lang="en">Total number of segments for this transaction; mandatory for uploads only.</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - <sequence> - <annotation> - <documentation xml:lang="de">Transfer- und Quittierungsphase.</documentation> - <documentation xml:lang="en">Transfer or receipt phase.</documentation> - </annotation> - <element name="TransactionID" type="ebics:TransactionIDType"> - <annotation> - <documentation xml:lang="de">eindeutige, technische Transaktions-ID; wird vom Server vergeben.</documentation> - <documentation xml:lang="en">unique transaction ID, provided by the server.</documentation> - </annotation> - </element> - </sequence> - </choice> - </sequence> -</complexType> -<complexType name="MutableHeaderType"> - <annotation> - <documentation xml:lang="de">Datentyp für den variablen EBICS-Header.</documentation> - <documentation xml:lang="en">Data type for the mutable EBICS header.</documentation> - </annotation> - <sequence> - <element name="TransactionPhase" type="ebics:TransactionPhaseType"> - <annotation> - <documentation xml:lang="de">Phase, in der sich die Transaktion gerade befindet; wird bei jedem Transaktionsschritt vom Client gesetzt und vom Server übernommen.</documentation> - <documentation xml:lang="en">Current phase of the transaction; this information is provided by the client for each step of the transaction, and the server adopts the setting.</documentation> - </annotation> - </element> - <element name="SegmentNumber" nillable="true" minOccurs="0"> - <annotation> - <documentation xml:lang="de">enthält die Nummer des aktuellen Segments, welches gerade übertragen oder angefordert wird; nur anzugeben bei TransactionPhase=Transfer.</documentation> - <documentation xml:lang="en">contains the number of the segment which is currently being transmitted or requested; mandatory for transaction phase 'Transfer' only.</documentation> - </annotation> - <complexType> - <simpleContent> - <extension base="ebics:SegmentNumberType"> - <attribute name="lastSegment" type="boolean" use="required"> - <annotation> - <documentation xml:lang="de">Ist dies das letzte Segment der Übertragung?</documentation> - <documentation xml:lang="en">Is this segment meant to be the last one regarding this transmission?</documentation> - </annotation> - </attribute> - </extension> - </simpleContent> - </complexType> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> -</complexType> -<complexType name="StaticHeaderOrderDetailsType"> - <annotation> - <documentation xml:lang="de">Datentyp für Auftragsdetails im statischen EBICS-Header.</documentation> - <documentation xml:lang="en">Data type for order details stored in the static EBICS header.</documentation> - </annotation> - <sequence> - <element name="OrderType"> - <annotation> - <documentation xml:lang="de">Auftragsart.</documentation> - <documentation xml:lang="en">type code of the order.</documentation> - </annotation> - <complexType> - <simpleContent> - <extension base="ebics:OrderTBaseType"/> - </simpleContent> - </complexType> - </element> - <element name="OrderID" type="ebics:OrderIDType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Auftragsnummer für Sendeaufträge gemäß DFÜ-Abkommen.</documentation> - <documentation xml:lang="en">ID of the (upload) order, formatted in accordance with the document "DFÜ-Abkommen".</documentation> - </annotation> - </element> - <element name="OrderAttribute" type="ebics:OrderAttributeType"> - <annotation> - <documentation xml:lang="de">Auftragsattribut.</documentation> - <documentation xml:lang="en">attribute describing the order contents.</documentation> - </annotation> - </element> - <element ref="ebics:OrderParams"/> - </sequence> -</complexType> -</schema> diff --git a/libeufin-common/src/main/resources/xsd/ebics_request_H005.xsd b/libeufin-common/src/main/resources/xsd/ebics_request_H005.xsd @@ -1,349 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- edited with XMLSpy v2016 sp1 (x64) (http://www.altova.com) by EBICS Working Group - March 2017 --> -<schema xmlns="http://www.w3.org/2001/XMLSchema" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:ebics="urn:org:ebics:H005" targetNamespace="urn:org:ebics:H005" elementFormDefault="qualified" attributeFormDefault="unqualified" version="1.0"> - <annotation> - <documentation xml:lang="de">ebics_request_H005.xsd ist das EBICS-Protokollschema für Anfragen.</documentation> - <documentation xml:lang="en">ebics_request_H005.xsd is the appropriate EBICS protocol schema for standard requests.</documentation> - </annotation> - <include schemaLocation="ebics_types_H005.xsd"/> - <include schemaLocation="ebics_orders_H005.xsd"/> - <import namespace="http://www.w3.org/2000/09/xmldsig#" schemaLocation="xmldsig-core-schema.xsd"/> - <element name="ebicsRequest"> - <annotation> - <documentation xml:lang="de">Electronic Banking Internet Communication Standard of the EBICS SCRL: Multibankfähige Schnittstelle zur internetbasierten Kommunikation.</documentation> - <documentation xml:lang="en">Electronic Banking Internet Communication Standard der EBICS SCRL: multi-bank capable interface for internet-based communication.</documentation> - </annotation> - <complexType> - <sequence> - <element name="header"> - <annotation> - <documentation xml:lang="de">enthält die technischen Transaktionsdaten.</documentation> - <documentation xml:lang="en">contains the transaction-driven data.</documentation> - </annotation> - <complexType> - <sequence> - <element name="static" type="ebics:StaticHeaderType"> - <annotation> - <documentation xml:lang="de">enhält alle festen Headereinträge.</documentation> - <documentation xml:lang="en">contains the static header entries.</documentation> - </annotation> - </element> - <element name="mutable" type="ebics:MutableHeaderType"> - <annotation> - <documentation xml:lang="de">enthält alle variablen Headereinträge.</documentation> - <documentation xml:lang="en">contains the mutable header entries.</documentation> - </annotation> - </element> - </sequence> - <attributeGroup ref="ebics:AuthenticationMarker"/> - </complexType> - </element> - <element ref="ebics:AuthSignature"/> - <element name="body"> - <annotation> - <documentation xml:lang="de">enthält die Auftragsdaten, EU(s) und weitere Nutzdaten.</documentation> - <documentation xml:lang="en">contains order data, order signature(s) and further data referring to the current order.</documentation> - </annotation> - <complexType> - <sequence> - <annotation> - <documentation xml:lang="de"/> - </annotation> - <element ref="ds:X509Data" minOccurs="0" maxOccurs="0"> - <annotation> - <documentation xml:lang="de">X.509-Daten des Teilnehmers.</documentation> - <documentation xml:lang="en">X.509 data of the user.</documentation> - </annotation> - </element> - <choice> - <annotation> - <documentation xml:lang="de">Welche Transaktionsphase?</documentation> - <documentation xml:lang="en">Which transaction phase?</documentation> - </annotation> - <sequence> - <annotation> - <documentation xml:lang="de">Initialisierungs- und Transferphase.</documentation> - <documentation xml:lang="en">Initialisation or transfer phase.</documentation> - </annotation> - <element name="PreValidation" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Daten zur Vorabprüfung; nur anzugeben in der Initialisierungsphase bei Uploads mit Auftragsattribut OZH (EUs + Auftragsdaten).</documentation> - <documentation xml:lang="en">Data sent for pre-validation; mandatory for initialisation phase during uploads using order attribute OZH (order signature(s) + order data).</documentation> - </annotation> - <complexType> - <complexContent> - <extension base="ebics:PreValidationRequestType"> - <attributeGroup ref="ebics:AuthenticationMarker"/> - </extension> - </complexContent> - </complexType> - </element> - <element name="DataTransfer" type="ebics:DataTransferRequestType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Transfer von Signatur- bzw. Auftragsdaten; nur bei Upload anzugeben.</documentation> - <documentation xml:lang="en">Transfer of signature or order data; mandatory for uploads only.</documentation> - </annotation> - </element> - </sequence> - <sequence> - <annotation> - <documentation xml:lang="de">Quittierungsphase nach Download.</documentation> - <documentation xml:lang="en">Receipt phase after download.</documentation> - </annotation> - <element name="TransferReceipt"> - <annotation> - <documentation xml:lang="de">Quittierung des Transfers.</documentation> - <documentation xml:lang="en">Receipt of transfer.</documentation> - </annotation> - <complexType> - <complexContent> - <extension base="ebics:TransferReceiptRequestType"> - <attributeGroup ref="ebics:AuthenticationMarker"/> - </extension> - </complexContent> - </complexType> - </element> - </sequence> - </choice> - </sequence> - </complexType> - </element> - </sequence> - <attributeGroup ref="ebics:VersionAttrGroup"/> - <anyAttribute namespace="##targetNamespace" processContents="strict"/> - </complexType> - </element> - <complexType name="StaticHeaderType"> - <annotation> - <documentation xml:lang="de">Datentyp für den statischen EBICS-Header.</documentation> - <documentation xml:lang="en">Data type for the static EBICS header.</documentation> - </annotation> - <sequence> - <element name="HostID" type="ebics:HostIDType"> - <annotation> - <documentation xml:lang="de">Hostname des Banksystems.</documentation> - </annotation> - </element> - <choice> - <annotation> - <documentation xml:lang="de">Transaktionsphase?</documentation> - <documentation xml:lang="en">Transaction phase?</documentation> - </annotation> - <sequence> - <annotation> - <documentation xml:lang="de">Initialisierungsphase.</documentation> - <documentation xml:lang="en">Initialisation phase.</documentation> - </annotation> - <element name="Nonce" type="ebics:NonceType"> - <annotation> - <documentation xml:lang="de">Zufallswert; damit wird die Initialisierungsnachricht des Clients einzigartig.</documentation> - <documentation xml:lang="en">Random value, ensures the uniqueness of the client's message during initialisation phase.</documentation> - </annotation> - </element> - <element name="Timestamp" type="ebics:TimestampType"> - <annotation> - <documentation xml:lang="de">aktueller Zeitstempel zur Begrenzung der serverseitigen Nonce-Speicherung.</documentation> - <documentation xml:lang="en">current timestamp, used to limit storage space for nonces on the server.</documentation> - </annotation> - </element> - <element name="PartnerID" type="ebics:PartnerIDType"> - <annotation> - <documentation xml:lang="de">Kunden-ID des serverseitig administrierten Kunden.</documentation> - <documentation xml:lang="en">ID of the partner = customer, administered on the server.</documentation> - </annotation> - </element> - <element name="UserID" type="ebics:UserIDType"> - <annotation> - <documentation xml:lang="de">Teilnehmer-ID des serverseitig zu diesem Kunden administrierten Teilnehmers.</documentation> - <documentation xml:lang="en">ID of the user that is assigned to the given customer, administered on the server.</documentation> - </annotation> - </element> - <element name="SystemID" type="ebics:UserIDType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">technische User-ID für Multi-User-Systeme.</documentation> - <documentation xml:lang="en">ID of the system for multi-user systems.</documentation> - </annotation> - </element> - <element name="Product" nillable="true" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Kennung des Kundenprodukts bzw. Herstellerkennung oder Name.</documentation> - <documentation xml:lang="en">software ID / manufacturer ID / manufacturer's name of the customer's software package.</documentation> - </annotation> - <complexType> - <simpleContent> - <extension base="ebics:ProductType"> - <attribute name="Language" type="ebics:LanguageType" use="required"> - <annotation> - <documentation xml:lang="de">Sprachkennzeichen der Kundenproduktversion (gemäß ISO 639).</documentation> - <documentation xml:lang="en">Language code of the customer's software package according to ISO 639.</documentation> - </annotation> - </attribute> - <attribute name="InstituteID" type="ebics:InstituteIDType" use="optional"> - <annotation> - <documentation xml:lang="de">Kennung des Herausgebers des Kundenprodukts bzw. des betreuenden Kreditinstituts.</documentation> - <documentation xml:lang="en">ID of the manufacturer / financial institute providing support for the customer's software package.</documentation> - </annotation> - </attribute> - </extension> - </simpleContent> - </complexType> - </element> - <element name="OrderDetails" type="ebics:StaticHeaderOrderDetailsType"> - <annotation> - <documentation xml:lang="de">Auftragsdetails.</documentation> - <documentation xml:lang="en">order details.</documentation> - </annotation> - </element> - <element name="BankPubKeyDigests"> - <annotation> - <documentation xml:lang="de">Hashwerte der erwarteten öffentlichen Schlüssel (Verschlüsselung, Signatur, Authentifikation) des Kreditinstituts.</documentation> - <documentation xml:lang="en">Digest values of the expected public keys (authentication, encryption, signature) owned by the financial institute.</documentation> - </annotation> - <complexType> - <sequence> - <element name="Authentication"> - <annotation> - <documentation xml:lang="de">Hashwert des Authentifikationsschlüssels.</documentation> - <documentation xml:lang="en">Digest value of the public authentication key.</documentation> - </annotation> - <complexType> - <simpleContent> - <extension base="ebics:PubKeyDigestType"> - <attribute name="Version" type="ebics:AuthenticationVersionType" use="required"> - <annotation> - <documentation xml:lang="de">Version des Authentifikationsverfahrens.</documentation> - <documentation xml:lang="en">Version of the algorithm used for authentication.</documentation> - </annotation> - </attribute> - </extension> - </simpleContent> - </complexType> - </element> - <element name="Encryption"> - <annotation> - <documentation xml:lang="de">Hashwert des Verschlüsselungsschlüssels.</documentation> - <documentation xml:lang="en">Digest value of the public encryption key.</documentation> - </annotation> - <complexType> - <simpleContent> - <extension base="ebics:PubKeyDigestType"> - <attribute name="Version" type="ebics:EncryptionVersionType" use="required"> - <annotation> - <documentation xml:lang="de">Version des Verschlüsselungsverfahrens.</documentation> - <documentation xml:lang="en">Version of the algorithm used for encryption.</documentation> - </annotation> - </attribute> - </extension> - </simpleContent> - </complexType> - </element> - <element name="Signature" minOccurs="0" maxOccurs="0"> - <annotation> - <documentation xml:lang="de">Hashwert des Signaturschlüssels.</documentation> - <documentation xml:lang="en">Digest value of the public signature key.</documentation> - </annotation> - <complexType> - <simpleContent> - <extension base="ebics:PubKeyDigestType"> - <attribute name="Version" type="ebics:SignatureVersionType" use="required"> - <annotation> - <documentation xml:lang="de">Version des Signaturverfahrens.</documentation> - <documentation xml:lang="en">Version of the algorithm used for signature creation.</documentation> - </annotation> - </attribute> - </extension> - </simpleContent> - </complexType> - </element> - </sequence> - </complexType> - </element> - <element name="SecurityMedium" type="ebics:SecurityMediumType"> - <annotation> - <documentation xml:lang="de">Angabe des Sicherheitsmediums, das der Kunde verwendet.</documentation> - <documentation xml:lang="en">Classification of the security medium used by the customer.</documentation> - </annotation> - </element> - <element name="NumSegments" type="ebics:NumSegmentsType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Gesamtsegmentanzahl für diese Transaktion; nur bei Uploads anzugeben.</documentation> - <documentation xml:lang="en">Total number of segments for this transaction; mandatory for uploads only.</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - <sequence> - <annotation> - <documentation xml:lang="de">Transfer- und Quittierungsphase.</documentation> - <documentation xml:lang="en">Transfer or receipt phase.</documentation> - </annotation> - <element name="TransactionID" type="ebics:TransactionIDType"> - <annotation> - <documentation xml:lang="de">eindeutige, technische Transaktions-ID; wird vom Server vergeben.</documentation> - <documentation xml:lang="en">unique transaction ID, provided by the server.</documentation> - </annotation> - </element> - </sequence> - </choice> - </sequence> - </complexType> - <complexType name="MutableHeaderType"> - <annotation> - <documentation xml:lang="de">Datentyp für den variablen EBICS-Header.</documentation> - <documentation xml:lang="en">Data type for the mutable EBICS header.</documentation> - </annotation> - <sequence> - <element name="TransactionPhase" type="ebics:TransactionPhaseType"> - <annotation> - <documentation xml:lang="de">Phase, in der sich die Transaktion gerade befindet; wird bei jedem Transaktionsschritt vom Client gesetzt und vom Server übernommen.</documentation> - <documentation xml:lang="en">Current phase of the transaction; this information is provided by the client for each step of the transaction, and the server adopts the setting.</documentation> - </annotation> - </element> - <element name="SegmentNumber" nillable="true" minOccurs="0"> - <annotation> - <documentation xml:lang="de">enthält die Nummer des aktuellen Segments, welches gerade übertragen oder angefordert wird; nur anzugeben bei TransactionPhase=Transfer.</documentation> - <documentation xml:lang="en">contains the number of the segment which is currently being transmitted or requested; mandatory for transaction phase 'Transfer' only.</documentation> - </annotation> - <complexType> - <simpleContent> - <extension base="ebics:SegmentNumberType"> - <attribute name="lastSegment" type="boolean" use="required"> - <annotation> - <documentation xml:lang="de">Ist dies das letzte Segment der Übertragung?</documentation> - <documentation xml:lang="en">Is this segment meant to be the last one regarding this transmission?</documentation> - </annotation> - </attribute> - </extension> - </simpleContent> - </complexType> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="StaticHeaderOrderDetailsType"> - <annotation> - <documentation xml:lang="de">//MODIFIED - Removed OrderAtribute ELEMENT// Datentyp für Auftragsdetails im statischen EBICS-Header.</documentation> - <documentation xml:lang="en">Data type for order details stored in the static EBICS header.</documentation> - </annotation> - <sequence> - <element name="AdminOrderType"> - <annotation> - <documentation xml:lang="de">//MODIFIED - Umbenannt von OrderType// Auftragsart.</documentation> - <documentation xml:lang="en">type code of the order.</documentation> - </annotation> - <complexType> - <simpleContent> - <extension base="ebics:OrderTBaseType"/> - </simpleContent> - </complexType> - </element> - <element name="OrderID" type="ebics:OrderIDType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Auftragsnummer für Sendeaufträge gemäß DFÜ-Abkommen.</documentation> - <documentation xml:lang="en">ID of the (upload) order, formatted in accordance with the document "DFÜ-Abkommen".</documentation> - </annotation> - </element> - <element ref="ebics:OrderParams"/> - </sequence> - </complexType> -</schema> diff --git a/libeufin-common/src/main/resources/xsd/ebics_response_H004.xsd b/libeufin-common/src/main/resources/xsd/ebics_response_H004.xsd @@ -1,166 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<schema xmlns="http://www.w3.org/2001/XMLSchema" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:ebics="urn:org:ebics:H004" targetNamespace="urn:org:ebics:H004" elementFormDefault="qualified" attributeFormDefault="unqualified" version="1.0"> - <annotation> - <documentation xml:lang="de">ebics_response_H004.xsd ist das EBICS-Protokollschema für Antwortnachrichten.</documentation> - <documentation xml:lang="en">ebics_response_H004.xsd is the appropriate EBICS protocol schema for standard responses.</documentation> - </annotation> - <import namespace="http://www.w3.org/2000/09/xmldsig#" schemaLocation="xmldsig-core-schema.xsd"> - <annotation> - <documentation xml:lang="de">XML-Signature.</documentation> - </annotation> - </import> - <include schemaLocation="ebics_types_H004.xsd"/> - <include schemaLocation="ebics_orders_H004.xsd"/> - <element name="ebicsResponse"> - <annotation> - <documentation xml:lang="de">Electronic Banking Internet Communication Standard des Zentralen Kreditausschusses (ZKA): Multibankfähige Schnittstelle zur internetbasierten Kommunikation.</documentation> - <documentation xml:lang="en">Electronic Banking Internet Communication Standard of the "Zentraler Kreditausschuss (ZKA)": multi-bank capable interface for internet-based communication.</documentation> - </annotation> - <complexType> - <sequence> - <element name="header"> - <annotation> - <documentation xml:lang="de">enthält die technischen Transaktionsdaten.</documentation> - <documentation xml:lang="en">contains the transaction-driven data.</documentation> - </annotation> - <complexType> - <sequence> - <element name="static" type="ebics:ResponseStaticHeaderType"> - <annotation> - <documentation xml:lang="de">enhält alle festen Headereinträge.</documentation> - <documentation xml:lang="en">contains the static header entries.</documentation> - </annotation> - </element> - <element name="mutable" type="ebics:ResponseMutableHeaderType"> - <annotation> - <documentation xml:lang="de">enthält alle variablen Headereinträge.</documentation> - <documentation xml:lang="en">contains the mutable header entries.</documentation> - </annotation> - </element> - </sequence> - <attributeGroup ref="ebics:AuthenticationMarker"/> - </complexType> - </element> - <element ref="ebics:AuthSignature"> - <annotation> - <documentation xml:lang="de">Authentifikationssignatur.</documentation> - <documentation xml:lang="en">Authentication signature.</documentation> - </annotation> - </element> - <element name="body"> - <annotation> - <documentation xml:lang="de">enthält die Auftragsdaten, EU(s) und weitere Nutzdaten.</documentation> - <documentation xml:lang="en">contains order data, order signature(s) and further data referring to the current order.</documentation> - </annotation> - <complexType> - <sequence> - <element name="DataTransfer" type="ebics:DataTransferResponseType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Transfer von Auftragsdaten; nur bei Download anzugeben.</documentation> - <documentation xml:lang="en">Transfer of signature or order data; mandatory for downloads only.</documentation> - </annotation> - </element> - <element name="ReturnCode"> - <annotation> - <documentation xml:lang="de">fachlicher Antwortcode für den vorangegangenen Request.</documentation> - <documentation xml:lang="en">order-related return code of the previous request.</documentation> - </annotation> - <complexType> - <simpleContent> - <extension base="ebics:ReturnCodeType"> - <attributeGroup ref="ebics:AuthenticationMarker"/> - </extension> - </simpleContent> - </complexType> - </element> - <element name="TimestampBankParameter" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Zeitstempel der letzten Aktualisierung der Bankparameter; nur in der Initialisierungsphase anzugeben.</documentation> - <documentation xml:lang="en">timestamp indicating the latest update of the bank parameters; may be set during initialisation phase only.</documentation> - </annotation> - <complexType> - <simpleContent> - <extension base="ebics:TimestampType"> - <attributeGroup ref="ebics:AuthenticationMarker"/> - </extension> - </simpleContent> - </complexType> - </element> - </sequence> - </complexType> - </element> - </sequence> - <attributeGroup ref="ebics:VersionAttrGroup"/> - <anyAttribute namespace="##targetNamespace" processContents="strict"/> - </complexType> - </element> - <complexType name="ResponseStaticHeaderType"> - <annotation> - <documentation xml:lang="de">Datentyp für den statischen EBICS-Header.</documentation> - <documentation xml:lang="en">Data type for the static EBICS header.</documentation> - </annotation> - <sequence> - <element name="TransactionID" type="ebics:TransactionIDType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">eindeutige, technische Transaktions-ID; wird vom Server vergeben, falls OrderAttribute entweder gleich "OZHNN" oder gleich "DZHNN" ist und falls tatsächlich eine Transaktion erzeugt wurde.</documentation> - <documentation xml:lang="en">unique transaction ID, provided by the server if and only if the order attribute is set to either "OZHNN" or "DZHNN" and if a transaction has been established actually.</documentation> - </annotation> - </element> - <element name="NumSegments" type="ebics:SegmentNumberType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Gesamtsegmentanzahl für diese Transaktion; nur bei Downloads in der Initialisierungsphase anzugeben.</documentation> - <documentation xml:lang="en">Total number of segments for this transaction; mandatory for downloads in initialisation phase only.</documentation> - </annotation> - </element> - </sequence> - </complexType> - <complexType name="ResponseMutableHeaderType"> - <annotation> - <documentation xml:lang="de">Datentyp für den variablen EBICS-Header.</documentation> - <documentation xml:lang="en">Data type for the mutable EBICS header.</documentation> - </annotation> - <sequence> - <element name="TransactionPhase" type="ebics:TransactionPhaseType"> - <annotation> - <documentation xml:lang="de">Phase, in der sich die Transaktion gerade befindet; wird bei jedem Transaktionsschritt vom Client gesetzt und vom Server übernommen.</documentation> - <documentation xml:lang="en">Current phase of the transaction; this information is provided by the client for each step of the transaction, and the server adopts the setting.</documentation> - </annotation> - </element> - <element name="SegmentNumber" minOccurs="0"> - <annotation> - <documentation xml:lang="de">enthält die Nummer des aktuellen Segments, welches gerade übertragen oder angefordert wird; nur anzugeben bei TransactionPhase=Transfer und (bei Download) TransactionPhase=Initialisation.</documentation> - <documentation xml:lang="en">contains the number of the segment which is currently being transmitted or requested; mandatory for transaction phases 'Transfer' and (for downloads) 'Initialisation' only.</documentation> - </annotation> - <complexType> - <simpleContent> - <extension base="ebics:SegmentNumberType"> - <attribute name="lastSegment" type="boolean" use="required"> - <annotation> - <documentation xml:lang="de">Ist dies das letzte Segment der Übertragung?</documentation> - </annotation> - </attribute> - </extension> - </simpleContent> - </complexType> - </element> - <element name="OrderID" type="ebics:OrderIDType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Auftragsnummer von Sendeaufträgen gemäß DFÜ-Abkommen.</documentation> - </annotation> - </element> - <element name="ReturnCode" type="ebics:ReturnCodeType"> - <annotation> - <documentation xml:lang="de">Rückmeldung des technischen Status mit einer eindeutigen Fehlernummer.</documentation> - <documentation xml:lang="en">Return code indicating the technical status.</documentation> - </annotation> - </element> - <element name="ReportText" type="ebics:ReportTextType"> - <annotation> - <documentation xml:lang="de">Klartext der Rückmeldung des technischen Status.</documentation> - <documentation xml:lang="en">Textual interpretation of the returned technical status code.</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> -</schema> diff --git a/libeufin-common/src/main/resources/xsd/ebics_response_H005.xsd b/libeufin-common/src/main/resources/xsd/ebics_response_H005.xsd @@ -1,167 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- edited with XMLSpy v2016 sp1 (x64) (http://www.altova.com) by EBICS Working Group - October 2016 --> -<schema xmlns="http://www.w3.org/2001/XMLSchema" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:ebics="urn:org:ebics:H005" targetNamespace="urn:org:ebics:H005" elementFormDefault="qualified" attributeFormDefault="unqualified" version="1.0"> - <annotation> - <documentation xml:lang="de">ebics_response_H005.xsd ist das EBICS-Protokollschema für Antwortnachrichten.</documentation> - <documentation xml:lang="en">ebics_response_H005.xsd is the appropriate EBICS protocol schema for standard responses.</documentation> - </annotation> - <import namespace="http://www.w3.org/2000/09/xmldsig#" schemaLocation="xmldsig-core-schema.xsd"> - <annotation> - <documentation xml:lang="de">XML-Signature.</documentation> - </annotation> - </import> - <include schemaLocation="ebics_types_H005.xsd"/> - <include schemaLocation="ebics_orders_H005.xsd"/> - <element name="ebicsResponse"> - <annotation> - <documentation xml:lang="de">Electronic Banking Internet Communication Standard des Zentralen Kreditausschusses (ZKA): Multibankfähige Schnittstelle zur internetbasierten Kommunikation.</documentation> - <documentation xml:lang="en">Electronic Banking Internet Communication Standard of the "Zentraler Kreditausschuss (ZKA)": multi-bank capable interface for internet-based communication.</documentation> - </annotation> - <complexType> - <sequence> - <element name="header"> - <annotation> - <documentation xml:lang="de">enthält die technischen Transaktionsdaten.</documentation> - <documentation xml:lang="en">contains the transaction-driven data.</documentation> - </annotation> - <complexType> - <sequence> - <element name="static" type="ebics:ResponseStaticHeaderType"> - <annotation> - <documentation xml:lang="de">enhält alle festen Headereinträge.</documentation> - <documentation xml:lang="en">contains the static header entries.</documentation> - </annotation> - </element> - <element name="mutable" type="ebics:ResponseMutableHeaderType"> - <annotation> - <documentation xml:lang="de">enthält alle variablen Headereinträge.</documentation> - <documentation xml:lang="en">contains the mutable header entries.</documentation> - </annotation> - </element> - </sequence> - <attributeGroup ref="ebics:AuthenticationMarker"/> - </complexType> - </element> - <element ref="ebics:AuthSignature"> - <annotation> - <documentation xml:lang="de">Authentifikationssignatur.</documentation> - <documentation xml:lang="en">Authentication signature.</documentation> - </annotation> - </element> - <element name="body"> - <annotation> - <documentation xml:lang="de">enthält die Auftragsdaten, EU(s) und weitere Nutzdaten.</documentation> - <documentation xml:lang="en">contains order data, order signature(s) and further data referring to the current order.</documentation> - </annotation> - <complexType> - <sequence> - <element name="DataTransfer" type="ebics:DataTransferResponseType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Transfer von Auftragsdaten; nur bei Download anzugeben.</documentation> - <documentation xml:lang="en">Transfer of signature or order data; mandatory for downloads only.</documentation> - </annotation> - </element> - <element name="ReturnCode"> - <annotation> - <documentation xml:lang="de">fachlicher Antwortcode für den vorangegangenen Request.</documentation> - <documentation xml:lang="en">order-related return code of the previous request.</documentation> - </annotation> - <complexType> - <simpleContent> - <extension base="ebics:ReturnCodeType"> - <attributeGroup ref="ebics:AuthenticationMarker"/> - </extension> - </simpleContent> - </complexType> - </element> - <element name="TimestampBankParameter" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Zeitstempel der letzten Aktualisierung der Bankparameter; nur in der Initialisierungsphase anzugeben.</documentation> - <documentation xml:lang="en">timestamp indicating the latest update of the bank parameters; may be set during initialisation phase only.</documentation> - </annotation> - <complexType> - <simpleContent> - <extension base="ebics:TimestampType"> - <attributeGroup ref="ebics:AuthenticationMarker"/> - </extension> - </simpleContent> - </complexType> - </element> - </sequence> - </complexType> - </element> - </sequence> - <attributeGroup ref="ebics:VersionAttrGroup"/> - <anyAttribute namespace="##targetNamespace" processContents="strict"/> - </complexType> - </element> - <complexType name="ResponseStaticHeaderType"> - <annotation> - <documentation xml:lang="de">//TODO - Modify anotation TransactionID// Datentyp für den statischen EBICS-Header.</documentation> - <documentation xml:lang="en">Data type for the static EBICS header.</documentation> - </annotation> - <sequence> - <element name="TransactionID" type="ebics:TransactionIDType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">eindeutige, technische Transaktions-ID; wird vom Server vergeben, falls OrderAttribute entweder gleich "OZHNN" oder gleich "DZHNN" ist und falls tatsächlich eine Transaktion erzeugt wurde.</documentation> - <documentation xml:lang="en">unique transaction ID, provided by the server if and only if the order attribute is set to either "OZHNN" or "DZHNN" and if a transaction has been established actually.</documentation> - </annotation> - </element> - <element name="NumSegments" type="ebics:SegmentNumberType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Gesamtsegmentanzahl für diese Transaktion; nur bei Downloads in der Initialisierungsphase anzugeben.</documentation> - <documentation xml:lang="en">Total number of segments for this transaction; mandatory for downloads in initialisation phase only.</documentation> - </annotation> - </element> - </sequence> - </complexType> - <complexType name="ResponseMutableHeaderType"> - <annotation> - <documentation xml:lang="de">Datentyp für den variablen EBICS-Header.</documentation> - <documentation xml:lang="en">Data type for the mutable EBICS header.</documentation> - </annotation> - <sequence> - <element name="TransactionPhase" type="ebics:TransactionPhaseType"> - <annotation> - <documentation xml:lang="de">Phase, in der sich die Transaktion gerade befindet; wird bei jedem Transaktionsschritt vom Client gesetzt und vom Server übernommen.</documentation> - <documentation xml:lang="en">Current phase of the transaction; this information is provided by the client for each step of the transaction, and the server adopts the setting.</documentation> - </annotation> - </element> - <element name="SegmentNumber" minOccurs="0"> - <annotation> - <documentation xml:lang="de">enthält die Nummer des aktuellen Segments, welches gerade übertragen oder angefordert wird; nur anzugeben bei TransactionPhase=Transfer und (bei Download) TransactionPhase=Initialisation.</documentation> - <documentation xml:lang="en">contains the number of the segment which is currently being transmitted or requested; mandatory for transaction phases 'Transfer' and (for downloads) 'Initialisation' only.</documentation> - </annotation> - <complexType> - <simpleContent> - <extension base="ebics:SegmentNumberType"> - <attribute name="lastSegment" type="boolean" use="required"> - <annotation> - <documentation xml:lang="de">Ist dies das letzte Segment der Übertragung?</documentation> - </annotation> - </attribute> - </extension> - </simpleContent> - </complexType> - </element> - <element name="OrderID" type="ebics:OrderIDType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Auftragsnummer von Sendeaufträgen gemäß DFÜ-Abkommen.</documentation> - </annotation> - </element> - <element name="ReturnCode" type="ebics:ReturnCodeType"> - <annotation> - <documentation xml:lang="de">Rückmeldung des technischen Status mit einer eindeutigen Fehlernummer.</documentation> - <documentation xml:lang="en">Return code indicating the technical status.</documentation> - </annotation> - </element> - <element name="ReportText" type="ebics:ReportTextType"> - <annotation> - <documentation xml:lang="de">Klartext der Rückmeldung des technischen Status.</documentation> - <documentation xml:lang="en">Textual interpretation of the returned technical status code.</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> -</schema> diff --git a/libeufin-common/src/main/resources/xsd/ebics_signature_S002.xsd b/libeufin-common/src/main/resources/xsd/ebics_signature_S002.xsd @@ -1,177 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- edited with XMLSpy v2016 sp1 (x64) (http://www.altova.com) by EBICS Working Group - October 2016 --> -<schema xmlns="http://www.w3.org/2001/XMLSchema" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:esig="http://www.ebics.org/S002" targetNamespace="http://www.ebics.org/S002" elementFormDefault="qualified" attributeFormDefault="unqualified" version="1.0"> - <import namespace="http://www.w3.org/2000/09/xmldsig#" schemaLocation="xmldsig-core-schema.xsd"/> - <annotation> - <documentation xml:lang="de">ebics_signature enthält Typdefinitionen für elektronische Unterschriften der Versionen A005, A006 und folgende.</documentation> - <documentation xml:lang="en">ebics_EU contains type definitions for electronic signatures: versions A005, A006 and et sqq.</documentation> - </annotation> - <!-- Elementdefinitionen für die EU. Die XML-Klartext-Struktur wird im EBICS-Signaturdatenkontext binär interpretiert.--> - <element name="EBICSSignatureData" abstract="true"> - <annotation> - <documentation xml:lang="de">XML-Strukturen für bankfachliche Elektronische Unterschriften (EUs).</documentation> - <documentation xml:lang="en">contains the digital signatures.</documentation> - </annotation> - </element> - <element name="UserSignatureData" type="esig:UserSignatureDataSigBookType" substitutionGroup="esig:EBICSSignatureData"> - <annotation> - <documentation xml:lang="de">enthält die EUs der Teilnehmer.</documentation> - <documentation xml:lang="en">contains the digital signatures.</documentation> - </annotation> - </element> - <complexType name="UserSignatureDataSigBookType"> - <annotation> - <documentation xml:lang="de">Datentyp für Signaturdaten des Teilnehmers beim EU-Transfer.</documentation> - <documentation xml:lang="en">Data type for digital signature data transferred using EBICS.</documentation> - </annotation> - <sequence> - <element name="OrderSignatureData" type="esig:OrderSignatureDataType" maxOccurs="unbounded"> - <annotation> - <documentation xml:lang="de">bankfachliche Elektronische Unterschrift oder Transportunterschrift (strukturiertes Format).</documentation> - <documentation xml:lang="en">Digital signature (either autorising an order or applied for transportation), structured format.</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <simpleType name="OrderSignatureType"> - <annotation> - <documentation xml:lang="de">Datentyp für kryptographische Unterschriften.</documentation> - </annotation> - <restriction base="base64Binary"/> - </simpleType> - <element name="OrderSignatureData" type="esig:OrderSignatureDataType"> - <annotation> - <documentation xml:lang="de">bankfachliche Elektronische Unterschrift oder Transportunterschrift (strukturiertes Format).</documentation> - <documentation xml:lang="en">Digital signature (either autorising an order or applied for transportation), structured format.</documentation> - </annotation> - </element> - <complexType name="OrderSignatureDataType"> - <annotation> - <documentation xml:lang="de">Datentyp für bankfachliche Elektronische Unterschrift oder Transportunterschrift (strukturiertes Format).</documentation> - <documentation xml:lang="en">Data type according for a digital signature (either autorising an order or applied for transportation), structured format.</documentation> - </annotation> - <sequence> - <element name="SignatureVersion" type="esig:SignatureVersionType"> - <annotation> - <documentation xml:lang="de">Version des Signaturverfahrens.</documentation> - <documentation xml:lang="en">Version of the algorithm used for signature creation.</documentation> - </annotation> - </element> - <element name="SignatureValue" type="base64Binary"> - <annotation> - <documentation xml:lang="de">Digitale Signatur.</documentation> - <documentation xml:lang="en">Digital signature.</documentation> - </annotation> - </element> - <element name="PartnerID" type="esig:PartnerIDType"> - <annotation> - <documentation xml:lang="de">Kunden-ID des Unterzeichners.</documentation> - <documentation xml:lang="en">Customer ID of the signer.</documentation> - </annotation> - </element> - <element name="UserID" type="esig:UserIDType"> - <annotation> - <documentation xml:lang="de">Teilnehmer-ID.</documentation> - <documentation xml:lang="en">User ID.</documentation> - </annotation> - </element> - <element ref="ds:X509Data" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Parameter zur X.509-Funktionalität</documentation> - <documentation xml:lang="en">Parameter for X509Data</documentation> - </annotation> - </element> - </sequence> - </complexType> - <simpleType name="PartnerIDType"> - <annotation> - <documentation xml:lang="de">Datentyp für eine Kunden-ID.</documentation> - </annotation> - <restriction base="token"> - <maxLength value="35"/> - </restriction> - </simpleType> - <simpleType name="UserIDType"> - <annotation> - <documentation xml:lang="de">Datentyp für eine Teilnehmer-ID.</documentation> - </annotation> - <restriction base="token"> - <maxLength value="35"/> - </restriction> - </simpleType> - <simpleType name="SignatureVersionType"> - <annotation> - <documentation xml:lang="de">Datentyp für Versionsnummern zur Elektronischen Unterschrift (EU).</documentation> - </annotation> - <restriction base="token"> - <length value="4"/> - <pattern value="A\d{3}"/> - </restriction> - </simpleType> - <!-- Definitionen für die Uebertragung von oeffentlichen Signierschlüsseln. z.B. ueber EBICS mit INI, PUB, HCS.--> - <element name="SignaturePubKeyOrderData" type="esig:SignaturePubKeyOrderDataType"> - <annotation> - <documentation xml:lang="de">Element für Public Key Dateien unabhängig von der Auftragsart / Geschäftsvorfall.</documentation> - </annotation> - </element> - <complexType name="SignaturePubKeyOrderDataType"> - <annotation> - <documentation xml:lang="de">Datentyp für Public Key Dateien unabhängig von der Auftragsart / Geschäftsvorfall.</documentation> - </annotation> - <sequence> - <element name="SignaturePubKeyInfo" type="esig:SignaturePubKeyInfoType"> - <annotation> - <documentation xml:lang="de">öffentlicher Signaturschlüssel.</documentation> - </annotation> - </element> - <element name="PartnerID" type="esig:PartnerIDType"> - <annotation> - <documentation xml:lang="de">Kunden-ID.</documentation> - </annotation> - </element> - <element name="UserID" type="esig:UserIDType"> - <annotation> - <documentation xml:lang="de">Teilnehmer-ID.</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <element name="SignaturePubKeyInfo" type="esig:SignaturePubKeyInfoType"> - <annotation> - <documentation xml:lang="de">öffentlicher Signaturschlüssel.</documentation> - </annotation> - </element> - <complexType name="SignaturePubKeyInfoType"> - <annotation> - <documentation xml:lang="de">Datentyp für öffentliche bankfachliche Schlüssel.</documentation> - </annotation> - <complexContent> - <extension base="esig:PubKeyInfoType"> - <sequence> - <element name="SignatureVersion" type="esig:SignatureVersionType"> - <annotation> - <documentation xml:lang="de">Version des EU-Signaturverfahrens.</documentation> - </annotation> - </element> - </sequence> - </extension> - </complexContent> - </complexType> - <complexType name="PubKeyInfoType"> - <annotation> - <documentation xml:lang="de">Datentyp für die Darstellung eines öffentlichen RSA-Schlüssels als Exponent-Modulus-Kombination oder als X509-Zertifikat.</documentation> - </annotation> - <sequence> - <element ref="ds:X509Data"/> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <simpleType name="TimestampType"> - <annotation> - <documentation xml:lang="de">Datentyp für Zeitstempel.</documentation> - </annotation> - <restriction base="dateTime"/> - </simpleType> -</schema> diff --git a/libeufin-common/src/main/resources/xsd/ebics_signatures.xsd b/libeufin-common/src/main/resources/xsd/ebics_signatures.xsd @@ -1,217 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<schema xmlns="http://www.w3.org/2001/XMLSchema" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:esig="http://www.ebics.org/S001" targetNamespace="http://www.ebics.org/S001" elementFormDefault="qualified" attributeFormDefault="unqualified" version="1.0"> - <import namespace="http://www.w3.org/2000/09/xmldsig#" schemaLocation="xmldsig-core-schema.xsd"/> - <annotation> - <documentation xml:lang="de">ebics_signature enthält Typdefinitionen für elektronische Unterschriften der Versionen A004, A005, A006 und folgende.</documentation> - <documentation xml:lang="en">ebics_EU contains type definitions for electronic signatures: versions A005, A006 and et sqq.</documentation> - </annotation> - <!-- Elementdefinitionen für die EU. Die XML-Klartext-Struktur wird im EBICS-Signaturdatenkontext binär interpretiert.--> - <element name="EBICSSignatureData" abstract="true"> - <annotation> - <documentation xml:lang="de">XML-Strukturen für bankfachliche Elektronische Unterschriften (EUs).</documentation> - <documentation xml:lang="en">contains the digital signatures.</documentation> - </annotation> - </element> - <element name="UserSignatureData" type="esig:UserSignatureDataSigBookType" substitutionGroup="esig:EBICSSignatureData"> - <annotation> - <documentation xml:lang="de">enthält die EUs der Teilnehmer.</documentation> - <documentation xml:lang="en">contains the digital signatures.</documentation> - </annotation> - </element> - <complexType name="UserSignatureDataSigBookType"> - <annotation> - <documentation xml:lang="de">Datentyp für Signaturdaten des Teilnehmers beim EU-Transfer.</documentation> - <documentation xml:lang="en">Data type for digital signature data transferred using EBICS.</documentation> - </annotation> - <sequence> - <choice maxOccurs="unbounded"> - <element name="OrderSignature"> - <annotation> - <documentation xml:lang="de">bankfachliche Elektronische Unterschrift oder Transportunterschrift (Binärformat).</documentation> - <documentation xml:lang="en">Digital signature (either autorising an order or applied for transportation), binary format.</documentation> - </annotation> - <complexType> - <simpleContent> - <extension base="esig:OrderSignatureType"> - <attribute name="PartnerID" use="required"> - <annotation> - <documentation xml:lang="de">Kunden-ID des Unterzeichners.</documentation> - <documentation xml:lang="en">Customer ID of the signer.</documentation> - </annotation> - </attribute> - </extension> - </simpleContent> - </complexType> - </element> - <element name="OrderSignatureData" type="esig:OrderSignatureDataType"> - <annotation> - <documentation xml:lang="de">bankfachliche Elektronische Unterschrift oder Transportunterschrift (strukturiertes Format).</documentation> - <documentation xml:lang="en">Digital signature (either autorising an order or applied for transportation), structured format.</documentation> - </annotation> - </element> - </choice> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <simpleType name="OrderSignatureType"> - <annotation> - <documentation xml:lang="de">Datentyp für kryptographische Unterschriften.</documentation> - </annotation> - <restriction base="base64Binary"/> - </simpleType> - <element name="OrderSignatureData" type="esig:OrderSignatureDataType"> - <annotation> - <documentation xml:lang="de">bankfachliche Elektronische Unterschrift oder Transportunterschrift (strukturiertes Format).</documentation> - <documentation xml:lang="en">Digital signature (either autorising an order or applied for transportation), structured format.</documentation> - </annotation> - </element> - <complexType name="OrderSignatureDataType"> - <annotation> - <documentation xml:lang="de">Datentyp für bankfachliche Elektronische Unterschrift oder Transportunterschrift (strukturiertes Format).</documentation> - <documentation xml:lang="en">Data type according for a digital signature (either autorising an order or applied for transportation), structured format.</documentation> - </annotation> - <sequence> - <element name="SignatureVersion" type="esig:SignatureVersionType"> - <annotation> - <documentation xml:lang="de">Version des Signaturverfahrens.</documentation> - <documentation xml:lang="en">Version of the algorithm used for signature creation.</documentation> - </annotation> - </element> - <element name="SignatureValue" type="base64Binary"> - <annotation> - <documentation xml:lang="de">Digitale Signatur.</documentation> - <documentation xml:lang="en">Digital signature.</documentation> - </annotation> - </element> - <element name="PartnerID" type="esig:PartnerIDType"> - <annotation> - <documentation xml:lang="de">Kunden-ID des Unterzeichners.</documentation> - <documentation xml:lang="en">Customer ID of the signer.</documentation> - </annotation> - </element> - <element name="UserID" type="esig:UserIDType"> - <annotation> - <documentation xml:lang="de">Teilnehmer-ID.</documentation> - <documentation xml:lang="en">User ID.</documentation> - </annotation> - </element> - <element ref="ds:X509Data" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Parameter zur X.509-Funktionalität</documentation> - <documentation xml:lang="en">Parameter for X509Data</documentation> - </annotation> - </element> - </sequence> - </complexType> - <simpleType name="PartnerIDType"> - <annotation> - <documentation xml:lang="de">Datentyp für eine Kunden-ID.</documentation> - </annotation> - <restriction base="token"> - <maxLength value="35"/> - </restriction> - </simpleType> - <simpleType name="UserIDType"> - <annotation> - <documentation xml:lang="de">Datentyp für eine Teilnehmer-ID.</documentation> - </annotation> - <restriction base="token"> - <maxLength value="35"/> - </restriction> - </simpleType> - <simpleType name="SignatureVersionType"> - <annotation> - <documentation xml:lang="de">Datentyp für Versionsnummern zur Elektronischen Unterschrift (EU).</documentation> - </annotation> - <restriction base="token"> - <length value="4"/> - <pattern value="A\d{3}"/> - </restriction> - </simpleType> - <!-- Definitionen für die Uebertragung von oeffentlichen Signierschlüsseln. z.B. ueber EBICS mit INI, PUB, HCS.--> - <element name="SignaturePubKeyOrderData" type="esig:SignaturePubKeyOrderDataType"> - <annotation> - <documentation xml:lang="de">Element für Public Key Dateien unabhängig von der Auftragsart / Geschäftsvorfall.</documentation> - </annotation> - </element> - <complexType name="SignaturePubKeyOrderDataType"> - <annotation> - <documentation xml:lang="de">Datentyp für Public Key Dateien unabhängig von der Auftragsart / Geschäftsvorfall.</documentation> - </annotation> - <sequence> - <element name="SignaturePubKeyInfo" type="esig:SignaturePubKeyInfoType"> - <annotation> - <documentation xml:lang="de">öffentlicher Signaturschlüssel.</documentation> - </annotation> - </element> - <element name="PartnerID" type="esig:PartnerIDType"> - <annotation> - <documentation xml:lang="de">Kunden-ID.</documentation> - </annotation> - </element> - <element name="UserID" type="esig:UserIDType"> - <annotation> - <documentation xml:lang="de">Teilnehmer-ID.</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <element name="SignaturePubKeyInfo" type="esig:SignaturePubKeyInfoType"> - <annotation> - <documentation xml:lang="de">öffentlicher Signaturschlüssel.</documentation> - </annotation> - </element> - <complexType name="SignaturePubKeyInfoType"> - <annotation> - <documentation xml:lang="de">Datentyp für öffentliche bankfachliche Schlüssel.</documentation> - </annotation> - <complexContent> - <extension base="esig:PubKeyInfoType"> - <sequence> - <element name="SignatureVersion" type="esig:SignatureVersionType"> - <annotation> - <documentation xml:lang="de">Version des EU-Signaturverfahrens.</documentation> - </annotation> - </element> - </sequence> - </extension> - </complexContent> - </complexType> - <complexType name="PubKeyInfoType"> - <annotation> - <documentation xml:lang="de">Datentyp für die Darstellung eines öffentlichen RSA-Schlüssels als Exponent-Modulus-Kombination oder als X509-Zertifikat.</documentation> - </annotation> - <sequence> - <sequence> - <element ref="ds:X509Data" minOccurs="0"/> - <element name="PubKeyValue" type="esig:PubKeyValueType"> - <annotation> - <documentation xml:lang="de">Darstellung als Exponent-Modulus-Kombination.</documentation> - </annotation> - </element> - </sequence> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <complexType name="PubKeyValueType"> - <annotation> - <documentation xml:lang="de">Datentyp für die Exponent-Modulus-Darstellung eines öffentlichen RSA-Schlüssels.</documentation> - </annotation> - <sequence> - <element ref="ds:RSAKeyValue"/> - <element name="TimeStamp" type="esig:TimestampType" minOccurs="0"> - <annotation> - <documentation xml:lang="de">Zeitpunkt der Generierung des Schlüssels.</documentation> - </annotation> - </element> - <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> - </sequence> - </complexType> - <simpleType name="TimestampType"> - <annotation> - <documentation xml:lang="de">Datentyp für Zeitstempel.</documentation> - </annotation> - <restriction base="dateTime"/> - </simpleType> -</schema> diff --git a/libeufin-common/src/main/resources/xsd/ebics_types_H004.xsd b/libeufin-common/src/main/resources/xsd/ebics_types_H004.xsd @@ -1,2426 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- Mit XMLSpy v2008 rel. 2 (http://www.altova.com) von Sabine Wenzel (SIZ Bonn) bearbeitet --> -<schema xmlns="http://www.w3.org/2001/XMLSchema" xmlns:esig="http://www.ebics.org/S001" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:ebics="urn:org:ebics:H004" targetNamespace="urn:org:ebics:H004" elementFormDefault="qualified" attributeFormDefault="unqualified" version="1.0"> - <import namespace="http://www.ebics.org/S001" schemaLocation="ebics_signatures.xsd"/> - <import namespace="http://www.w3.org/2000/09/xmldsig#" schemaLocation="xmldsig-core-schema.xsd"/> - <annotation> - <documentation xml:lang="de">ebics_types_H004.xsd enthält einfache Typdefinitionen für EBICS.</documentation> - </annotation> - <simpleType name="ProtocolVersionType"> - <annotation> - <documentation xml:lang="de">Datentyp für EBICS-Versionsnummern.</documentation> - </annotation> - <restriction base="token"> - <length value="4"/> - <pattern value="H\d{3}"/> - </restriction> - </simpleType> - <simpleType name="ProtocolRevisionType"> - <annotation> - <documentation xml:lang="de">Datentyp für EBICS-Revisionsnummern.</documentation> - </annotation> - <restriction base="positiveInteger"> - <maxInclusive value="99"/> - </restriction> - </simpleType> - <simpleType name="EncryptionVersionType"> - <annotation> - <documentation xml:lang="de">Datentyp für Versionsnummern zur Verschlüsselung.</documentation> - </annotation> - <restriction base="token"> - <pattern value="E\d{3}"/> - </restriction> - </simpleType> - <simpleType name="SignatureVersionType"> - <annotation> - <documentation xml:lang="de">Datentyp für Versionsnummern zur Elektronischen Unterschrift (EU).</documentation> - </annotation> - <restriction base="token"> - <length value="4"/> - <pattern value="A\d{3}"/> - </restriction> - </simpleType> - <simpleType name="AuthenticationVersionType"> - <annotation> - <documentation xml:lang="de">Datentyp für Versionsnummern zur Authentifikation.</documentation> - </annotation> - <restriction base="token"> - <length value="4"/> - <pattern value="X\d{3}"/> - </restriction> - </simpleType> - <simpleType name="CryptoVersionType"> - <annotation> - <documentation xml:lang="de">Datentyp für Versionsnummern zur Verschlüsselung, Signatur und Authentifkation.</documentation> - </annotation> - <union memberTypes="ebics:EncryptionVersionType ebics:SignatureVersionType ebics:AuthenticationVersionType"/> - </simpleType> - <simpleType name="CurrencyBaseType"> - <annotation> - <documentation xml:lang="de">Datentyp für Währungen (Grundtyp).</documentation> - </annotation> - <restriction base="token"> - <length value="3"/> - <pattern value="[A-Z]{3}"/> - </restriction> - </simpleType> - <simpleType name="CurrencyCode"> - <annotation> - <documentation xml:lang="de">dreistelliger Währungscode gemäß ISO 4217.</documentation> - </annotation> - <restriction base="ebics:CurrencyBaseType"> - <length value="3"/> - <enumeration value="AFN"> - <annotation> - <documentation xml:lang="de">Afghanistan: Afghani</documentation> - </annotation> - </enumeration> - <enumeration value="ALL"> - <annotation> - <documentation xml:lang="de">Albanien: Lek</documentation> - </annotation> - </enumeration> - <enumeration value="AMD"> - <annotation> - <documentation xml:lang="de">Armenien: Dram</documentation> - </annotation> - </enumeration> - <enumeration value="ANG"> - <annotation> - <documentation xml:lang="de">Niederländische Antillen: Gulden</documentation> - </annotation> - </enumeration> - <enumeration value="AOA"> - <annotation> - <documentation xml:lang="de">Angola: Kwanza</documentation> - </annotation> - </enumeration> - <enumeration value="ARS"> - <annotation> - <documentation xml:lang="de">Argentinien: Peso</documentation> - </annotation> - </enumeration> - <enumeration value="AUD"> - <annotation> - <documentation xml:lang="de">Australien: Dollar</documentation> - </annotation> - </enumeration> - <enumeration value="AWG"> - <annotation> - <documentation xml:lang="de">Aruba: Florin</documentation> - </annotation> - </enumeration> - <enumeration value="AZM"> - <annotation> - <documentation xml:lang="de">Aserbaidschan: Manat</documentation> - </annotation> - </enumeration> - <enumeration value="BAM"> - <annotation> - <documentation xml:lang="de">Bosnien und Herzegowina: Konvertible Mark</documentation> - </annotation> - </enumeration> - <enumeration value="BBD"> - <annotation> - <documentation xml:lang="de">Barbados: Dollar</documentation> - </annotation> - </enumeration> - <enumeration value="BDT"> - <annotation> - <documentation xml:lang="de">Bangladesch: Taka</documentation> - </annotation> - </enumeration> - <enumeration value="BGN"> - <annotation> - <documentation xml:lang="de">Bulgarien: Lew</documentation> - </annotation> - </enumeration> - <enumeration value="BHD"> - <annotation> - <documentation xml:lang="de">Bahrain: Dinar</documentation> - </annotation> - </enumeration> - <enumeration value="BMD"> - <annotation> - <documentation xml:lang="de">Bermuda: Dollar</documentation> - </annotation> - </enumeration> - <enumeration value="BND"> - <annotation> - <documentation xml:lang="de">Brunei: Dollar</documentation> - </annotation> - </enumeration> - <enumeration value="BOB"> - <annotation> - <documentation xml:lang="de">Bolivien: Boliviano</documentation> - </annotation> - </enumeration> - <enumeration value="BRL"> - <annotation> - <documentation xml:lang="de">Brasilien: Real</documentation> - </annotation> - </enumeration> - <enumeration value="BSD"> - <annotation> - <documentation xml:lang="de">Bahamas: Dollar</documentation> - </annotation> - </enumeration> - <enumeration value="BTN"> - <annotation> - <documentation xml:lang="de">Bhutan: Ngultrum</documentation> - </annotation> - </enumeration> - <enumeration value="BWP"> - <annotation> - <documentation xml:lang="de">Botswana: Pula</documentation> - </annotation> - </enumeration> - <enumeration value="BYR"> - <annotation> - <documentation xml:lang="de">Weißrussland (Belarus): Rubel</documentation> - </annotation> - </enumeration> - <enumeration value="BZD"> - <annotation> - <documentation xml:lang="de">Belize: Dollar</documentation> - </annotation> - </enumeration> - <enumeration value="CAD"> - <annotation> - <documentation xml:lang="de">Kanada: Dollar</documentation> - </annotation> - </enumeration> - <enumeration value="CDF"> - <annotation> - <documentation xml:lang="de">Demokratische Republik Kongo: Franc</documentation> - </annotation> - </enumeration> - <enumeration value="CHF"> - <annotation> - <documentation xml:lang="de">Schweiz: Franken</documentation> - </annotation> - </enumeration> - <enumeration value="CLP"> - <annotation> - <documentation xml:lang="de">Chile: Peso</documentation> - </annotation> - </enumeration> - <enumeration value="CNY"> - <annotation> - <documentation xml:lang="de">China (Volksrepublik): Renminbi Yuan</documentation> - </annotation> - </enumeration> - <enumeration value="COP"> - <annotation> - <documentation xml:lang="de">Kolumbien: Peso</documentation> - </annotation> - </enumeration> - <enumeration value="CRC"> - <annotation> - <documentation xml:lang="de">Costa Rica: Colón</documentation> - </annotation> - </enumeration> - <enumeration value="CSD"> - <annotation> - <documentation xml:lang="de">Serbien: Dinar</documentation> - </annotation> - </enumeration> - <enumeration value="CUP"> - <annotation> - <documentation xml:lang="de">Kuba: Peso</documentation> - </annotation> - </enumeration> - <enumeration value="CVE"> - <annotation> - <documentation xml:lang="de">Kap Verde: Escudo</documentation> - </annotation> - </enumeration> - <enumeration value="CYP"> - <annotation> - <documentation xml:lang="de">Zypern (griechischer Teil): Pfund</documentation> - </annotation> - </enumeration> - <enumeration value="CZK"> - <annotation> - <documentation xml:lang="de">Tschechien: Krone</documentation> - </annotation> - </enumeration> - <enumeration value="DJV"> - <annotation> - <documentation xml:lang="de">Dschibuti: Franc</documentation> - </annotation> - </enumeration> - <enumeration value="DKK"> - <annotation> - <documentation xml:lang="de">Dänemark: Krone</documentation> - </annotation> - </enumeration> - <enumeration value="DOP"> - <annotation> - <documentation xml:lang="de">Dominikanische Republik: Peso</documentation> - </annotation> - </enumeration> - <enumeration value="DZD"> - <annotation> - <documentation xml:lang="de">Algerien: Dinar</documentation> - </annotation> - </enumeration> - <enumeration value="ECS"> - <annotation> - <documentation xml:lang="de">Ecuador (bis 2000): Sucre</documentation> - </annotation> - </enumeration> - <enumeration value="EEK"> - <annotation> - <documentation xml:lang="de">Estland: Krone</documentation> - </annotation> - </enumeration> - <enumeration value="EGP"> - <annotation> - <documentation xml:lang="de">Ägypten: Pfund</documentation> - </annotation> - </enumeration> - <enumeration value="ETB"> - <annotation> - <documentation xml:lang="de">Äthiopien: Birr</documentation> - </annotation> - </enumeration> - <enumeration value="EUR"> - <annotation> - <documentation xml:lang="de">Europäische Währungsunion: Euro</documentation> - </annotation> - </enumeration> - <enumeration value="FJD"> - <annotation> - <documentation xml:lang="de">Fidschi: Dollar</documentation> - </annotation> - </enumeration> - <enumeration value="FKP"> - <annotation> - <documentation xml:lang="de">Falklandinseln: Pfund</documentation> - </annotation> - </enumeration> - <enumeration value="GBP"> - <annotation> - <documentation xml:lang="de">Vereinigtes Königreich: Pfund</documentation> - </annotation> - </enumeration> - <enumeration value="GEL"> - <annotation> - <documentation xml:lang="de">Georgien: Lari</documentation> - </annotation> - </enumeration> - <enumeration value="GHC"> - <annotation> - <documentation xml:lang="de">Ghana: Cedi</documentation> - </annotation> - </enumeration> - <enumeration value="GIP"> - <annotation> - <documentation xml:lang="de">Gibraltar: Pfund</documentation> - </annotation> - </enumeration> - <enumeration value="GMD"> - <annotation> - <documentation xml:lang="de">Gambia: Dalasi</documentation> - </annotation> - </enumeration> - <enumeration value="GNF"> - <annotation> - <documentation xml:lang="de">Guinea: Franc</documentation> - </annotation> - </enumeration> - <enumeration value="GTQ"> - <annotation> - <documentation xml:lang="de">Guatemala: Quetzal</documentation> - </annotation> - </enumeration> - <enumeration value="GYD"> - <annotation> - <documentation xml:lang="de">Guyana: Dollar</documentation> - </annotation> - </enumeration> - <enumeration value="HKD"> - <annotation> - <documentation xml:lang="de">Hongkong: Dollar</documentation> - </annotation> - </enumeration> - <enumeration value="HNL"> - <annotation> - <documentation xml:lang="de">Honduras: Lempira</documentation> - </annotation> - </enumeration> - <enumeration value="HRK"> - <annotation> - <documentation xml:lang="de">Kroatien: Kuna</documentation> - </annotation> - </enumeration> - <enumeration value="HTG"> - <annotation> - <documentation xml:lang="de">Haiti: Gourde</documentation> - </annotation> - </enumeration> - <enumeration value="HUF"> - <annotation> - <documentation xml:lang="de">Ungarn: Forint</documentation> - </annotation> - </enumeration> - <enumeration value="IDR"> - <annotation> - <documentation xml:lang="de">Indonesien: Rupiah</documentation> - </annotation> - </enumeration> - <enumeration value="ILS"> - <annotation> - <documentation xml:lang="de">Israel: Schekel</documentation> - </annotation> - </enumeration> - <enumeration value="INR"> - <annotation> - <documentation xml:lang="de">Indien: Rupie</documentation> - </annotation> - </enumeration> - <enumeration value="IQD"> - <annotation> - <documentation xml:lang="de">Irak: Dinar</documentation> - </annotation> - </enumeration> - <enumeration value="IRR"> - <annotation> - <documentation xml:lang="de">Iran: Rial</documentation> - </annotation> - </enumeration> - <enumeration value="ISK"> - <annotation> - <documentation xml:lang="de">Island: Krone</documentation> - </annotation> - </enumeration> - <enumeration value="JMD"> - <annotation> - <documentation xml:lang="de">Jamaika: Dollar</documentation> - </annotation> - </enumeration> - <enumeration value="JOD"> - <annotation> - <documentation xml:lang="de">Jordanien: Dinar</documentation> - </annotation> - </enumeration> - <enumeration value="JPY"> - <annotation> - <documentation xml:lang="de">Japan: Yen</documentation> - </annotation> - </enumeration> - <enumeration value="KES"> - <annotation> - <documentation xml:lang="de">Kenia: Schilling</documentation> - </annotation> - </enumeration> - <enumeration value="KGS"> - <annotation> - <documentation xml:lang="de">Kirgisistan: Som</documentation> - </annotation> - </enumeration> - <enumeration value="KHR"> - <annotation> - <documentation xml:lang="de">Kambodscha: Riel</documentation> - </annotation> - </enumeration> - <enumeration value="KMF"> - <annotation> - <documentation xml:lang="de">Komoren: Franc</documentation> - </annotation> - </enumeration> - <enumeration value="KPW"> - <annotation> - <documentation xml:lang="de">Nordkorea: Won</documentation> - </annotation> - </enumeration> - <enumeration value="KRW"> - <annotation> - <documentation xml:lang="de">Südkorea: Won</documentation> - </annotation> - </enumeration> - <enumeration value="KWD"> - <annotation> - <documentation xml:lang="de">Kuwait: Dinar</documentation> - </annotation> - </enumeration> - <enumeration value="KYD"> - <annotation> - <documentation xml:lang="de">Kaimaninseln: Dollar</documentation> - </annotation> - </enumeration> - <enumeration value="KZT"> - <annotation> - <documentation xml:lang="de">Kasachstan: Tenge</documentation> - </annotation> - </enumeration> - <enumeration value="LAK"> - <annotation> - <documentation xml:lang="de">Laos: Kip</documentation> - </annotation> - </enumeration> - <enumeration value="LBP"> - <annotation> - <documentation xml:lang="de">Libanon: Pfund</documentation> - </annotation> - </enumeration> - <enumeration value="LKR"> - <annotation> - <documentation xml:lang="de">Sri Lanka: Rupie</documentation> - </annotation> - </enumeration> - <enumeration value="LRD"> - <annotation> - <documentation xml:lang="de">Liberia: Dollar</documentation> - </annotation> - </enumeration> - <enumeration value="LSL"> - <annotation> - <documentation xml:lang="de">Lesotho: Loti</documentation> - </annotation> - </enumeration> - <enumeration value="LTL"> - <annotation> - <documentation xml:lang="de">Litauen: Litas</documentation> - </annotation> - </enumeration> - <enumeration value="LVL"> - <annotation> - <documentation xml:lang="de">Lettland: Lats</documentation> - </annotation> - </enumeration> - <enumeration value="LYD"> - <annotation> - <documentation xml:lang="de">Libyen: Dinar</documentation> - </annotation> - </enumeration> - <enumeration value="MAD"> - <annotation> - <documentation xml:lang="de">Marokko: Dirham</documentation> - </annotation> - </enumeration> - <enumeration value="MDL"> - <annotation> - <documentation xml:lang="de">Moldawien: Leu</documentation> - </annotation> - </enumeration> - <enumeration value="MGF"> - <annotation> - <documentation xml:lang="de">Madagaskar: Franc</documentation> - </annotation> - </enumeration> - <enumeration value="MKD"> - <annotation> - <documentation xml:lang="de">Mazedonien: Denar</documentation> - </annotation> - </enumeration> - <enumeration value="MMK"> - <annotation> - <documentation xml:lang="de">Myanmar: Kyat</documentation> - </annotation> - </enumeration> - <enumeration value="MNT"> - <annotation> - <documentation xml:lang="de">Mongolei: Tugrik</documentation> - </annotation> - </enumeration> - <enumeration value="MOP"> - <annotation> - <documentation xml:lang="de">Macau: Pataca</documentation> - </annotation> - </enumeration> - <enumeration value="MRO"> - <annotation> - <documentation xml:lang="de">Mauretanien: Ouguiya</documentation> - </annotation> - </enumeration> - <enumeration value="MTL"> - <annotation> - <documentation xml:lang="de">Malta: Lira</documentation> - </annotation> - </enumeration> - <enumeration value="MUR"> - <annotation> - <documentation xml:lang="de">Mauritius: Rupie</documentation> - </annotation> - </enumeration> - <enumeration value="MVR"> - <annotation> - <documentation xml:lang="de">Malediven: Rufiyaa</documentation> - </annotation> - </enumeration> - <enumeration value="MWK"> - <annotation> - <documentation xml:lang="de">Malawi: Kwacha</documentation> - </annotation> - </enumeration> - <enumeration value="MXN"> - <annotation> - <documentation xml:lang="de">Mexiko: Peso</documentation> - </annotation> - </enumeration> - <enumeration value="MYR"> - <annotation> - <documentation xml:lang="de">Malaysia: Ringgit</documentation> - </annotation> - </enumeration> - <enumeration value="MZM"> - <annotation> - <documentation xml:lang="de">Mosambik: Metical</documentation> - </annotation> - </enumeration> - <enumeration value="NAD"> - <annotation> - <documentation xml:lang="de">Namibia: Dollar</documentation> - </annotation> - </enumeration> - <enumeration value="NGN"> - <annotation> - <documentation xml:lang="de">Nigeria: Naira</documentation> - </annotation> - </enumeration> - <enumeration value="NIO"> - <annotation> - <documentation xml:lang="de">Nicaragua: Cordoba Oro</documentation> - </annotation> - </enumeration> - <enumeration value="NOK"> - <annotation> - <documentation xml:lang="de">Norwegen: Krone</documentation> - </annotation> - </enumeration> - <enumeration value="NPR"> - <annotation> - <documentation xml:lang="de">Nepal: Rupie</documentation> - </annotation> - </enumeration> - <enumeration value="NZD"> - <annotation> - <documentation xml:lang="de">Neuseeland: Dollar</documentation> - </annotation> - </enumeration> - <enumeration value="OMR"> - <annotation> - <documentation xml:lang="de">Oman: Rial</documentation> - </annotation> - </enumeration> - <enumeration value="PAB"> - <annotation> - <documentation xml:lang="de">Panama: Balboa</documentation> - </annotation> - </enumeration> - <enumeration value="PEN"> - <annotation> - <documentation xml:lang="de">Peru: Nuevo Sol</documentation> - </annotation> - </enumeration> - <enumeration value="PGK"> - <annotation> - <documentation xml:lang="de">Papua-Neuguinea: Kina</documentation> - </annotation> - </enumeration> - <enumeration value="PHP"> - <annotation> - <documentation xml:lang="de">Philippinen: Peso</documentation> - </annotation> - </enumeration> - <enumeration value="PKR"> - <annotation> - <documentation xml:lang="de">Pakistan: Rupie</documentation> - </annotation> - </enumeration> - <enumeration value="PLN"> - <annotation> - <documentation xml:lang="de">Polen: Zloty</documentation> - </annotation> - </enumeration> - <enumeration value="PYG"> - <annotation> - <documentation xml:lang="de">Paraguay: Guaraní</documentation> - </annotation> - </enumeration> - <enumeration value="QAR"> - <annotation> - <documentation xml:lang="de">Katar: Riyal</documentation> - </annotation> - </enumeration> - <enumeration value="ROL"> - <annotation> - <documentation xml:lang="de">Rumänien: Leu</documentation> - </annotation> - </enumeration> - <enumeration value="RUB"> - <annotation> - <documentation xml:lang="de">Russland: Rubel</documentation> - </annotation> - </enumeration> - <enumeration value="RWF"> - <annotation> - <documentation xml:lang="de">Ruanda: Franc</documentation> - </annotation> - </enumeration> - <enumeration value="SAR"> - <annotation> - <documentation xml:lang="de">Saudi-Arabien: Riyal</documentation> - </annotation> - </enumeration> - <enumeration value="SBD"> - <annotation> - <documentation xml:lang="de">Salomonen: Dollar</documentation> - </annotation> - </enumeration> - <enumeration value="SCR"> - <annotation> - <documentation xml:lang="de">Seychellen: Rupie</documentation> - </annotation> - </enumeration> - <enumeration value="SDD"> - <annotation> - <documentation xml:lang="de">Sudan: Dinar</documentation> - </annotation> - </enumeration> - <enumeration value="SEK"> - <annotation> - <documentation xml:lang="de">Schweden: Krone</documentation> - </annotation> - </enumeration> - <enumeration value="SGD"> - <annotation> - <documentation xml:lang="de">Singapur: Dollar</documentation> - </annotation> - </enumeration> - <enumeration value="SHP"> - <annotation> - <documentation xml:lang="de">St. Helena: Pfund</documentation> - </annotation> - </enumeration> - <enumeration value="SIT"> - <annotation> - <documentation xml:lang="de">Slowenien: Tolar</documentation> - </annotation> - </enumeration> - <enumeration value="SKK"> - <annotation> - <documentation xml:lang="de">Slowakei: Krone</documentation> - </annotation> - </enumeration> - <enumeration value="SLL"> - <annotation> - <documentation xml:lang="de">Sierra Leone: Leone</documentation> - </annotation> - </enumeration> - <enumeration value="SOS"> - <annotation> - <documentation xml:lang="de">Somalia: Schilling</documentation> - </annotation> - </enumeration> - <enumeration value="SRD"> - <annotation> - <documentation xml:lang="de">Suriname: Dollar</documentation> - </annotation> - </enumeration> - <enumeration value="STD"> - <annotation> - <documentation xml:lang="de">São Tomé und Príncipe: Dobra</documentation> - </annotation> - </enumeration> - <enumeration value="SVC"> - <annotation> - <documentation xml:lang="de">El Salvador: Colón</documentation> - </annotation> - </enumeration> - <enumeration value="SYP"> - <annotation> - <documentation xml:lang="de">Syrien: Pfund</documentation> - </annotation> - </enumeration> - <enumeration value="SZL"> - <annotation> - <documentation xml:lang="de">Swasiland: Lilangeni</documentation> - </annotation> - </enumeration> - <enumeration value="THB"> - <annotation> - <documentation xml:lang="de">Thailand: Baht</documentation> - </annotation> - </enumeration> - <enumeration value="TJS"> - <annotation> - <documentation xml:lang="de">Tadschikistan: Somoni</documentation> - </annotation> - </enumeration> - <enumeration value="TMM"> - <annotation> - <documentation xml:lang="de">Turkmenistan: Manat</documentation> - </annotation> - </enumeration> - <enumeration value="TND"> - <annotation> - <documentation xml:lang="de">Tunesien: Dinar</documentation> - </annotation> - </enumeration> - <enumeration value="TOP"> - <annotation> - <documentation xml:lang="de">Tonga: Pa'anga</documentation> - </annotation> - </enumeration> - <enumeration value="TRL"> - <annotation> - <documentation xml:lang="de">Türkei: Lira</documentation> - </annotation> - </enumeration> - <enumeration value="TRY"> - <annotation> - <documentation xml:lang="de">Türkei: Neue Lira (ab 2005)</documentation> - </annotation> - </enumeration> - <enumeration value="TTD"> - <annotation> - <documentation xml:lang="de">Trinidad und Tobago: Dollar</documentation> - </annotation> - </enumeration> - <enumeration value="TWD"> - <annotation> - <documentation xml:lang="de">Taiwan: Dollar</documentation> - </annotation> - </enumeration> - <enumeration value="TZS"> - <annotation> - <documentation xml:lang="de">Tansania: Schilling</documentation> - </annotation> - </enumeration> - <enumeration value="UAH"> - <annotation> - <documentation xml:lang="de">Ukraine: Hrywnja</documentation> - </annotation> - </enumeration> - <enumeration value="UGX"> - <annotation> - <documentation xml:lang="de">Uganda: Shilling</documentation> - </annotation> - </enumeration> - <enumeration value="USD"> - <annotation> - <documentation xml:lang="de">USA: Dollar</documentation> - </annotation> - </enumeration> - <enumeration value="UYU"> - <annotation> - <documentation xml:lang="de">Uruguay: Peso</documentation> - </annotation> - </enumeration> - <enumeration value="UZS"> - <annotation> - <documentation xml:lang="de">Usbekistan: Sum</documentation> - </annotation> - </enumeration> - <enumeration value="VEB"> - <annotation> - <documentation xml:lang="de">Venezuela: Bolivar</documentation> - </annotation> - </enumeration> - <enumeration value="VND"> - <annotation> - <documentation xml:lang="de">Vietnam: Dong</documentation> - </annotation> - </enumeration> - <enumeration value="VUV"> - <annotation> - <documentation xml:lang="de">Vanuatu: Vatu</documentation> - </annotation> - </enumeration> - <enumeration value="WST"> - <annotation> - <documentation xml:lang="de">Samoa: Tala</documentation> - </annotation> - </enumeration> - <enumeration value="XAF"> - <annotation> - <documentation xml:lang="de">Zentralafrikanische Wirtschafts- und Währungsunion: CFA-Franc</documentation> - </annotation> - </enumeration> - <enumeration value="XCD"> - <annotation> - <documentation xml:lang="de">Ostkaribische Währungsunion: Dollar</documentation> - </annotation> - </enumeration> - <enumeration value="XOF"> - <annotation> - <documentation xml:lang="de">Westafrikanische Wirtschafts- und Währungsunion: CFA-Franc</documentation> - </annotation> - </enumeration> - <enumeration value="XPF"> - <annotation> - <documentation xml:lang="de">Neukaledonien: CFP-Franc</documentation> - </annotation> - </enumeration> - <enumeration value="XTS"> - <annotation> - <documentation xml:lang="de">Spezialcode für Testzwecke; keine existierende Währung</documentation> - </annotation> - </enumeration> - <enumeration value="XXX"> - <annotation> - <documentation xml:lang="de">keine Währung</documentation> - </annotation> - </enumeration> - <enumeration value="YER"> - <annotation> - <documentation xml:lang="de">Jemen: Rial</documentation> - </annotation> - </enumeration> - <enumeration value="ZAR"> - <annotation> - <documentation xml:lang="de">Südafrika: Rand</documentation> - </annotation> - </enumeration> - <enumeration value="ZMK"> - <annotation> - <documentation xml:lang="de">Sambia: Kwacha</documentation> - </annotation> - </enumeration> - <enumeration value="ZWD"> - <annotation> - <documentation xml:lang="de">Simbabwe: Dollar</documentation> - </annotation> - </enumeration> - </restriction> - </simpleType> - <simpleType name="AmountValueType"> - <annotation> - <documentation xml:lang="de">Datentyp für einen Betragswert (ohne Währung).</documentation> - </annotation> - <restriction base="decimal"> - <totalDigits value="24"/> - <fractionDigits value="4"/> - </restriction> - </simpleType> - <complexType name="AmountType"> - <annotation> - <documentation xml:lang="de">Datentyp für einen Betrag inkl. Währungscode-Attribut (Default = "EUR").</documentation> - </annotation> - <simpleContent> - <extension base="ebics:AmountValueType"> - <attribute name="Currency" type="ebics:CurrencyBaseType" use="optional" default="EUR"> - <annotation> - <documentation xml:lang="de">Währungscode, Default="EUR".</documentation> - <documentation xml:lang="en">Currency code, default setting is "EUR".</documentation> - </annotation> - </attribute> - </extension> - </simpleContent> - </complexType> - <simpleType name="TransactionIDType"> - <annotation> - <documentation xml:lang="de">Datentyp für die Transaktions-ID.</documentation> - </annotation> - <restriction base="hexBinary"> - <length value="16"/> - </restriction> - </simpleType> - <simpleType name="NonceType"> - <annotation> - <documentation xml:lang="de">Datentyp für Nonces.</documentation> - </annotation> - <restriction base="hexBinary"> - <length value="16"/> - </restriction> - </simpleType> - <simpleType name="InstituteIDType"> - <annotation> - <documentation xml:lang="de">Datentyp für die Instituts-ID.</documentation> - </annotation> - <restriction base="normalizedString"> - <maxLength value="64"/> - </restriction> - </simpleType> - <simpleType name="HostIDType"> - <annotation> - <documentation xml:lang="de">Datentyp für die Host-ID.</documentation> - </annotation> - <restriction base="token"> - <maxLength value="35"/> - </restriction> - </simpleType> - <simpleType name="ProductType"> - <annotation> - <documentation xml:lang="de">Datentyp für die Kundenprodukt-ID.</documentation> - </annotation> - <restriction base="normalizedString"> - <maxLength value="64"/> - </restriction> - </simpleType> - <simpleType name="LanguageType"> - <annotation> - <documentation xml:lang="de">Datentyp für das Sprachkennzeichen des Kundenprodukts.</documentation> - </annotation> - <restriction base="language"> - <length value="2"/> - </restriction> - </simpleType> - <simpleType name="OrderTBaseType"> - <annotation> - <documentation xml:lang="de">Datentyp für allgemeine Auftragsarten (Grundtyp).</documentation> - </annotation> - <restriction base="token"> - <length value="3"/> - <pattern value="[A-Z0-9]{3}"/> - </restriction> - </simpleType> - <simpleType name="OrderTListType"> - <annotation> - <documentation xml:lang="de">Listentyp für allgemeine Auftragsarten.</documentation> - </annotation> - <list itemType="ebics:OrderTBaseType"/> - </simpleType> - <simpleType name="OrderTType"> - <annotation> - <documentation xml:lang="de">Datentyp für zulässige Auftragsarten im EBICS-Kontext.</documentation> - </annotation> - <restriction base="ebics:OrderTBaseType"> - <length value="3"/> - <enumeration value="HSA"> - <annotation> - <documentation xml:lang="de">Senden der Public Keys für Authentifikation und Verschlüsselung, bankfachlich signiert mit FTAM-Signaturschlüssel</documentation> - </annotation> - </enumeration> - <enumeration value="HIA"> - <annotation> - <documentation xml:lang="de">Senden der Public Keys zur Authentifikation und zur Verschlüsselung</documentation> - </annotation> - </enumeration> - <enumeration value="HPB"> - <annotation> - <documentation xml:lang="de">Abholen der Public Keys der Bank</documentation> - </annotation> - </enumeration> - <enumeration value="HCA"> - <annotation> - <documentation xml:lang="de">Ändern der Public Keys zur Authentifikation und zur Verschlüsselung</documentation> - </annotation> - </enumeration> - <enumeration value="HPD"> - <annotation> - <documentation xml:lang="de">Abholen Bankparameter für internetbasierten Standard</documentation> - </annotation> - </enumeration> - <enumeration value="HVU"> - <annotation> - <documentation xml:lang="de">Abholen VEU Übersicht</documentation> - </annotation> - </enumeration> - <enumeration value="HVD"> - <annotation> - <documentation xml:lang="de">Abholen VEU Auftragsdaten (Daten-trägerbegleitzettel)</documentation> - </annotation> - </enumeration> - <enumeration value="HVT"> - <annotation> - <documentation xml:lang="de">Abholen VEU Auftragsdaten ( Transakti-onsdetails gemäß Parametervorgabe)</documentation> - </annotation> - </enumeration> - <enumeration value="HVE"> - <annotation> - <documentation xml:lang="de">Senden EU zu bestehendem VEU-Auftrag</documentation> - </annotation> - </enumeration> - <enumeration value="HVS"> - <annotation> - <documentation xml:lang="de">Senden Stornierung für bestehenden VEU-Auftrag</documentation> - </annotation> - </enumeration> - <enumeration value="HKD"> - <annotation> - <documentation xml:lang="de">Abholen Konfigurationsdaten des Teilnehmers</documentation> - </annotation> - </enumeration> - <enumeration value="HAA"> - <annotation> - <documentation xml:lang="de">Abholen Übersicht zu abrufbaren Aufträgen</documentation> - </annotation> - </enumeration> - <enumeration value="HVU"> - <annotation> - <documentation xml:lang="de">VEU-Übersicht abholen</documentation> - </annotation> - </enumeration> - <enumeration value="HVD"> - <annotation> - <documentation xml:lang="de">VEU-Status abrufen</documentation> - </annotation> - </enumeration> - <enumeration value="HVT"> - <annotation> - <documentation xml:lang="de">VEU-Transaktion-Details abrufen</documentation> - </annotation> - </enumeration> - <enumeration value="HVE"> - <annotation> - <documentation xml:lang="de">EU hinzufügen</documentation> - </annotation> - </enumeration> - <enumeration value="HVS"> - <annotation> - <documentation xml:lang="de">VEU-Stornierung</documentation> - </annotation> - </enumeration> - <enumeration value="AAE"> - <annotation> - <documentation xml:lang="de">Senden Importakkreditiv Änderung</documentation> - </annotation> - </enumeration> - <enumeration value="AEA"> - <annotation> - <documentation xml:lang="de">Senden Exportakkreditive</documentation> - </annotation> - </enumeration> - <enumeration value="AIA"> - <annotation> - <documentation xml:lang="de">Senden Import-Akkreditive Avisierung</documentation> - </annotation> - </enumeration> - <enumeration value="AKA"> - <annotation> - <documentation xml:lang="de">Abholen Import-Akkreditive</documentation> - </annotation> - </enumeration> - <enumeration value="AWV"> - <annotation> - <documentation xml:lang="de">AWV-Meldung senden</documentation> - </annotation> - </enumeration> - <enumeration value="AZM"> - <annotation> - <documentation xml:lang="de">AZV im Magnetbandformat senden (Satzlänge variabel)</documentation> - </annotation> - </enumeration> - <enumeration value="AZV"> - <annotation> - <documentation xml:lang="de">AZV im Diskettenformat senden</documentation> - </annotation> - </enumeration> - <enumeration value="AZ2"> - <annotation> - <documentation xml:lang="de">AZV im Magnetbandformat senden (Satzlängenfeld 2 Bytes)</documentation> - </annotation> - </enumeration> - <enumeration value="AZ4"> - <annotation> - <documentation xml:lang="de">AZV im Magnetbandformat senden (Satzlängenfeld 4 Bytes)</documentation> - </annotation> - </enumeration> - <enumeration value="DDG"> - <annotation> - <documentation xml:lang="de">Abholen Devisenhandelsbestätigung</documentation> - </annotation> - </enumeration> - <enumeration value="DHB"> - <annotation> - <documentation xml:lang="de">Senden Devisenhandelsbestätigung</documentation> - </annotation> - </enumeration> - <enumeration value="DTE"> - <annotation> - <documentation xml:lang="de">Eilauftrag (IZV im DTAUS0-Format) senden</documentation> - </annotation> - </enumeration> - <enumeration value="DTI"> - <annotation> - <documentation xml:lang="de">IZV-Datei abholen</documentation> - </annotation> - </enumeration> - <enumeration value="DTM"> - <annotation> - <documentation xml:lang="de">MCV-Datei abholen (Format analog MCV)</documentation> - </annotation> - </enumeration> - <enumeration value="DTV"> - <annotation> - <documentation xml:lang="de">Zahlungsverkehrsdateien von Service-Rechenzentren senden</documentation> - </annotation> - </enumeration> - <enumeration value="DT2"> - <annotation> - <documentation xml:lang="de">MC2-Datei abholen (Format analog MC2)</documentation> - </annotation> - </enumeration> - <enumeration value="DT4"> - <annotation> - <documentation xml:lang="de">MC4-Datei abholen (Format analog MC4)</documentation> - </annotation> - </enumeration> - <enumeration value="EAB"> - <annotation> - <documentation xml:lang="de">Exportakkreditive abholen</documentation> - </annotation> - </enumeration> - <enumeration value="ECS"> - <annotation> - <documentation xml:lang="de">Senden electronic-cash Lastschriftdatei</documentation> - </annotation> - </enumeration> - <enumeration value="EDC"> - <annotation> - <documentation xml:lang="de">Senden Maestro-Lastschriftdatei</documentation> - </annotation> - </enumeration> - <enumeration value="EEA"> - <annotation> - <documentation xml:lang="de">EDIFACT abholen ASCII</documentation> - </annotation> - </enumeration> - <enumeration value="EEZ"> - <annotation> - <documentation xml:lang="de">EDIFACT abholen EBCDIC</documentation> - </annotation> - </enumeration> - <enumeration value="EIB"> - <annotation> - <documentation xml:lang="de">Ausführungsanzeige (Exportinkasso) Bank an Kunde abholen</documentation> - </annotation> - </enumeration> - <enumeration value="EIK"> - <annotation> - <documentation xml:lang="de">Senden Exportinkassi</documentation> - </annotation> - </enumeration> - <enumeration value="ESA"> - <annotation> - <documentation xml:lang="de">EDIFACT senden ASCII</documentation> - </annotation> - </enumeration> - <enumeration value="ESM"> - <annotation> - <documentation xml:lang="de">EU-Standardüberweisung (Zahlungsart 13) im Magnetbandformat (Satzlängenfeld 4 Bytes)</documentation> - </annotation> - </enumeration> - <enumeration value="ESR"> - <annotation> - <documentation xml:lang="de">Einreichung von EDIFACT-Lastschriften</documentation> - </annotation> - </enumeration> - <enumeration value="ESZ"> - <annotation> - <documentation xml:lang="de">EDIFACT senden EBCDIC</documentation> - </annotation> - </enumeration> - <enumeration value="ESU"> - <annotation> - <documentation xml:lang="de">EU-Standardüberweisung (Zahlungsart 13)</documentation> - </annotation> - </enumeration> - <enumeration value="EUE"> - <annotation> - <documentation xml:lang="de">Taggleiche grenzüberschreitende Euro-Eilzahlung</documentation> - </annotation> - </enumeration> - <enumeration value="GAB"> - <annotation> - <documentation xml:lang="de">Abholen Garantien</documentation> - </annotation> - </enumeration> - <enumeration value="GAK"> - <annotation> - <documentation xml:lang="de">Senden Garantien</documentation> - </annotation> - </enumeration> - <enumeration value="GKT"> - <annotation> - <documentation xml:lang="de">GeldKarte-Umsatz senden (Datenaufbau gemäß GeldKarte-Spezifikation)</documentation> - </annotation> - </enumeration> - <enumeration value="IDD"> - <annotation> - <documentation xml:lang="de">Internationale Lastschriften</documentation> - </annotation> - </enumeration> - <enumeration value="IIB"> - <annotation> - <documentation xml:lang="de">Abholen Importinkassi</documentation> - </annotation> - </enumeration> - <enumeration value="IIK"> - <annotation> - <documentation xml:lang="de">Senden Importinkassi</documentation> - </annotation> - </enumeration> - <enumeration value="INT"> - <annotation> - <documentation xml:lang="de">Internationaler Zahlungsverkehr</documentation> - </annotation> - </enumeration> - <enumeration value="IZG"> - <annotation> - <documentation xml:lang="de">Inlandszahlungsverkehrsauftrag senden (nur Gutschriften)</documentation> - </annotation> - </enumeration> - <enumeration value="IZL"> - <annotation> - <documentation xml:lang="de">Inlandszahlungsverkehrsauftrag senden (nur Lastschriften)</documentation> - </annotation> - </enumeration> - <enumeration value="IZV"> - <annotation> - <documentation xml:lang="de">Inlandszahlungsverkehrsauftrag senden</documentation> - </annotation> - </enumeration> - <enumeration value="MAO"> - <annotation> - <documentation xml:lang="de">Abholen Magnetband-Datei aus optischer Beleglesung</documentation> - </annotation> - </enumeration> - <enumeration value="MCV"> - <annotation> - <documentation xml:lang="de">Senden IZV-Magnetbandformat (Satzlängenfeld 4 Bytes)</documentation> - </annotation> - </enumeration> - <enumeration value="MC2"> - <annotation> - <documentation xml:lang="de">Senden IZV-Magnetbandformat (Satzlängenfeld 2 Bytes)</documentation> - </annotation> - </enumeration> - <enumeration value="MC4"> - <annotation> - <documentation xml:lang="de">Senden IZV-Magnetbandformat (Satzlänge variabel)</documentation> - </annotation> - </enumeration> - <enumeration value="POZ"> - <annotation> - <documentation xml:lang="de">Senden POZ-Datei</documentation> - </annotation> - </enumeration> - <enumeration value="RDT"> - <annotation> - <documentation xml:lang="de">Rücklastschrift an Kunde</documentation> - </annotation> - </enumeration> - <enumeration value="RFT"> - <annotation> - <documentation xml:lang="de">Request for Transfer</documentation> - </annotation> - </enumeration> - <enumeration value="STA"> - <annotation> - <documentation xml:lang="de">Abholen Swift-Tagesauszüge</documentation> - </annotation> - </enumeration> - <enumeration value="VMK"> - <annotation> - <documentation xml:lang="de">Abholen kurzfristige Vormerkposten</documentation> - </annotation> - </enumeration> - <enumeration value="WPA"> - <annotation> - <documentation xml:lang="de">Abholen Wertpapierabrechnung</documentation> - </annotation> - </enumeration> - <enumeration value="WPB"> - <annotation> - <documentation xml:lang="de">Abholen Wertpapierausführungsanzeige</documentation> - </annotation> - </enumeration> - <enumeration value="WPC"> - <annotation> - <documentation xml:lang="de">Abholen Depotaufstellung</documentation> - </annotation> - </enumeration> - <enumeration value="WPD"> - <annotation> - <documentation xml:lang="de">Abholen sonstige WP-Umsätze</documentation> - </annotation> - </enumeration> - <enumeration value="INI"> - <annotation> - <documentation xml:lang="de">Initialisierung der bankfachlichen EU des Teilnehmers.</documentation> - </annotation> - </enumeration> - <enumeration value="PTK"> - <annotation> - <documentation xml:lang="de">Abholen Kundenprotokoll</documentation> - </annotation> - </enumeration> - <enumeration value="PUB"> - <annotation> - <documentation xml:lang="de">Senden Public Key zur Unterschriftenverifizierung</documentation> - </annotation> - </enumeration> - <enumeration value="HCA"> - <annotation> - <documentation xml:lang="de">Änderung der Teilnehmerschlüssel für Authentifikation und Verschlüsselung</documentation> - </annotation> - </enumeration> - <enumeration value="SPR"> - <annotation> - <documentation xml:lang="de">Sperren der Zugangsberechtigung</documentation> - </annotation> - </enumeration> - <enumeration value="VPB"> - <annotation> - <documentation xml:lang="de">Abholen Public Key der Bank zur Verschlüsselung</documentation> - </annotation> - </enumeration> - <enumeration value="FIN"> - <annotation> - <documentation xml:lang="de">EDIFACT-FINPAY senden</documentation> - </annotation> - </enumeration> - <enumeration value="IZS"> - <annotation> - <documentation xml:lang="de">Informationen von Zentralstellen</documentation> - </annotation> - </enumeration> - <enumeration value="SSP"> - <annotation> - <documentation xml:lang="de">ec-Karten-Sperrdatei</documentation> - </annotation> - </enumeration> - <enumeration value="ATA"> - <annotation> - <documentation xml:lang="de">Teilausnutzung Importakkreditiv (Kreditinstitut an Kunde)</documentation> - </annotation> - </enumeration> - <enumeration value="BKA"> - <annotation> - <documentation xml:lang="de">Auftragsart für elektronische Kontoauszüge</documentation> - </annotation> - </enumeration> - <enumeration value="BZK"> - <annotation> - <documentation xml:lang="de">Barzahlungskarte</documentation> - </annotation> - </enumeration> - <enumeration value="DKI"> - <annotation> - <documentation xml:lang="de">Devisenkursinformationen abholen (Euro)</documentation> - </annotation> - </enumeration> - <enumeration value="DMI"> - <annotation> - <documentation xml:lang="de">Abholen Devisenmarktinformationen</documentation> - </annotation> - </enumeration> - <enumeration value="DSW"> - <annotation> - <documentation xml:lang="de">Abholen Devisenswapinformationen</documentation> - </annotation> - </enumeration> - <enumeration value="ESG"> - <annotation> - <documentation xml:lang="de">ESG-Datei für Elektronische Zweitunterschrift abholen</documentation> - </annotation> - </enumeration> - <enumeration value="ESP"> - <annotation> - <documentation xml:lang="de">ESP-Datei für Elektronische Zweitunterschrift senden</documentation> - </annotation> - </enumeration> - <enumeration value="FTB"> - <annotation> - <documentation xml:lang="de">Abholen/Senden beliebige Datei</documentation> - </annotation> - </enumeration> - <enumeration value="FTD"> - <annotation> - <documentation xml:lang="de">Freie Textdatei senden/abholen</documentation> - </annotation> - </enumeration> - <enumeration value="IBK"> - <annotation> - <documentation xml:lang="de">Abholen Institutsbestätigungsdatei (Komplettbestand)</documentation> - </annotation> - </enumeration> - <enumeration value="IBW"> - <annotation> - <documentation xml:lang="de">Abholen Institutsbestätigungsdatei (Komplettbestand weitere Datei)</documentation> - </annotation> - </enumeration> - <enumeration value="IBU"> - <annotation> - <documentation xml:lang="de">Abholen Institutsbestätigungsdatei (tägliches Update)</documentation> - </annotation> - </enumeration> - <enumeration value="IKK"> - <annotation> - <documentation xml:lang="de">Senden Institutskonten (Komplettbestand begrenzt auf 170 MB)</documentation> - </annotation> - </enumeration> - <enumeration value="IKU"> - <annotation> - <documentation xml:lang="de">Senden Institutskonten (tägliches Update)</documentation> - </annotation> - </enumeration> - <enumeration value="IKW"> - <annotation>