libeufin

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

commit 3679a4b32b777767857dd10b2b19740ce7469ddd
parent 449d1bcf56972717f59d1ab3082f1adc28c341b4
Author: Antoine A <>
Date:   Thu, 23 Nov 2023 15:06:35 +0000

Improve bank and nexus integration

Diffstat:
MMakefile | 2+-
Mbank/conf/test.conf | 4+++-
Mbank/conf/test_no_tan.conf | 4+++-
Mbank/src/main/kotlin/tech/libeufin/bank/Config.kt | 2+-
Mbank/src/main/kotlin/tech/libeufin/bank/Main.kt | 57+++++++++++++++++++++++++++------------------------------
Adatabase-versioning/libeufin-conversion-drop.sql | 10++++++++++
Adatabase-versioning/libeufin-conversion-setup.sql | 79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ddatabase-versioning/libeufin-conversion.sql | 79-------------------------------------------------------------------------------
Mintegration/test/IntegrationTest.kt | 72++++++++++++++++++++++++++++++++++++++----------------------------------
9 files changed, 162 insertions(+), 147 deletions(-)

diff --git a/Makefile b/Makefile @@ -41,7 +41,7 @@ install-bank-files: install contrib/libeufin-bank.conf $(bank_config_dir)/ install contrib/currencies.conf $(bank_config_dir)/ install -D database-versioning/libeufin-bank*.sql -t $(bank_sql_dir) - install -D database-versioning/libeufin-conversion.sql -t $(bank_sql_dir) + install -D database-versioning/libeufin-conversion*.sql -t $(bank_sql_dir) install -D database-versioning/versioning.sql -t $(bank_sql_dir) .PHONY: install-bank diff --git a/bank/conf/test.conf b/bank/conf/test.conf @@ -5,7 +5,6 @@ DEFAULT_ADMIN_DEBT_LIMIT = KUDOS:10000 REGISTRATION_BONUS_ENABLED = NO SUGGESTED_WITHDRAWAL_EXCHANGE = https://exchange.example.com allow_conversion = YES -fiat_currency = EUR tan_sms = libeufin-tan-file.sh tan_email = libeufin-tan-fail.sh @@ -22,6 +21,9 @@ cashout_ratio = 1.25 cashout_fee = EUR:0.003 cashout_min_amount = KUDOS:0.1 +[nexus-ebics] +currency = EUR + [nexus-postgres] CONFIG = postgres:///libeufincheck diff --git a/bank/conf/test_no_tan.conf b/bank/conf/test_no_tan.conf @@ -5,7 +5,6 @@ DEFAULT_ADMIN_DEBT_LIMIT = KUDOS:10000 REGISTRATION_BONUS_ENABLED = NO SUGGESTED_WITHDRAWAL_EXCHANGE = https://exchange.example.com allow_conversion = YES -fiat_currency = EUR [libeufin-bankdb-postgres] SQL_DIR = $DATADIR/sql/ @@ -19,3 +18,6 @@ cashin_rounding_mode = nearest cashout_ratio = 1.25 cashout_fee = EUR:0.003 cashout_min_amount = KUDOS:0.1 + +[nexus-ebics] +currency = EUR diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Config.kt b/bank/src/main/kotlin/tech/libeufin/bank/Config.kt @@ -130,7 +130,7 @@ fun TalerConfig.loadBankConfig(): BankConfig = catchError { var conversionInfo: ConversionInfo? = null; val allowConversion = lookupBoolean("libeufin-bank", "allow_conversion") ?: false; if (allowConversion) { - fiatCurrency = requireString("libeufin-bank", "fiat_currency"); + fiatCurrency = requireString("nexus-ebics", "currency"); fiatCurrencySpec = currencySpecificationFor(fiatCurrency) conversionInfo = loadConversionInfo(regionalCurrency, fiatCurrency) } diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt @@ -321,35 +321,6 @@ class BankDbInit : CliktCommand("Initialize the libeufin-bank database", name = } } -class ConversionSetupCmd : CliktCommand("Setup conversion support", name = "conversion-setup") { - private val configFile by option( - "--config", "-c", - help = "set the configuration file" - ) - - override fun run() { - val config = talerConfig(configFile) - val cfg = config.loadDbConfig() - val ctx = config.loadBankConfig(); - val db = Database(cfg.dbConnStr, ctx.regionalCurrency, ctx.fiatCurrency) - runBlocking { - logger.info("doing DB initialization, sqldir ${cfg.sqlDir}, dbConnStr ${cfg.dbConnStr}") - val sqlProcedures = File("${cfg.sqlDir}/libeufin-conversion.sql") - if (!sqlProcedures.exists()) { - logger.info("Missing libeufin-conversion.sql file") - exitProcess(1) - } - pgDataSource(cfg.dbConnStr).pgConnection().execSQLUpdate(sqlProcedures.readText()) - - // Load conversion config - ctx.conversionInfo?.run { - logger.info("loading conversion config in DB") - db.conversion.updateConfig(this) - } - } - } -} - class ServeBank : CliktCommand("Run libeufin-bank HTTP server", name = "serve") { private val configFile by option( "--config", "-c", @@ -366,6 +337,32 @@ class ServeBank : CliktCommand("Run libeufin-bank HTTP server", name = "serve") exitProcess(1) } val db = Database(dbCfg.dbConnStr, ctx.regionalCurrency, ctx.fiatCurrency) + runBlocking { + if (ctx.allowConversion) { + logger.info("ensure conversion is enabled") + val sqlProcedures = File("${dbCfg.sqlDir}/libeufin-conversion-setup.sql") + if (!sqlProcedures.exists()) { + logger.info("Missing libeufin-conversion-setup.sql file") + exitProcess(1) + } + pgDataSource(dbCfg.dbConnStr).pgConnection().execSQLUpdate(sqlProcedures.readText()) + } else { + logger.info("ensure conversion is disabled") + val sqlProcedures = File("${dbCfg.sqlDir}/libeufin-conversion-drop.sql") + if (!sqlProcedures.exists()) { + logger.info("Missing libeufin-conversion-drop.sql file") + exitProcess(1) + } + pgDataSource(dbCfg.dbConnStr).pgConnection().execSQLUpdate(sqlProcedures.readText()) + // Remove conversion info from the database ? + } + + // Load conversion config + ctx.conversionInfo?.run { + logger.info("loading conversion config in DB") + db.conversion.updateConfig(this) + } + } embeddedServer(Netty, port = serverCfg.port) { corebankWebApp(db, ctx) }.start(wait = true) @@ -467,7 +464,7 @@ class BankConfigCmd : CliktCommand("Dump the configuration", name = "config") { class LibeufinBankCommand : CliktCommand() { init { versionOption(getVersion()) - subcommands(ServeBank(), BankDbInit(), ConversionSetupCmd(), ChangePw(), BankConfigCmd()) + subcommands(ServeBank(), BankDbInit(), ChangePw(), BankConfigCmd()) } override fun run() = Unit diff --git a/database-versioning/libeufin-conversion-drop.sql b/database-versioning/libeufin-conversion-drop.sql @@ -0,0 +1,9 @@ +BEGIN; +SET search_path TO libeufin_bank; + +DROP TRIGGER IF EXISTS cashin_link; +DROP FUNCTION IF EXISTS cashin_link; +DROP TRIGGER IF EXISTS cashout_link; +DROP FUNCTION IF EXISTS cashout_link; + +COMMIT; +\ No newline at end of file diff --git a/database-versioning/libeufin-conversion-setup.sql b/database-versioning/libeufin-conversion-setup.sql @@ -0,0 +1,78 @@ +BEGIN; +SET search_path TO libeufin_bank; + +CREATE OR REPLACE FUNCTION cashout_link() +RETURNS trigger +LANGUAGE plpgsql AS $$ + DECLARE + now_date BIGINT; + payto_uri TEXT; + BEGIN + IF NEW.local_transaction IS NOT NULL THEN + SELECT transaction_date INTO now_date + FROM libeufin_bank.bank_account_transactions + WHERE bank_transaction_id = NEW.local_transaction; + SELECT cashout_payto INTO payto_uri + FROM libeufin_bank.bank_accounts + JOIN libeufin_bank.customers ON customer_id=owning_customer_id + WHERE bank_account_id=NEW.bank_account; + INSERT INTO libeufin_nexus.initiated_outgoing_transactions ( + amount + ,wire_transfer_subject + ,credit_payto_uri + ,initiation_time + ,request_uid + ) VALUES ( + ((NEW.amount_credit).val, (NEW.amount_credit).frac)::libeufin_nexus.taler_amount + ,NEW.subject + ,payto_uri + ,now_date + ,LEFT(gen_random_uuid()::text, 35) + ); + END IF; + RETURN NEW; + END; +$$; + +CREATE OR REPLACE TRIGGER cashout_link BEFORE INSERT OR UPDATE ON cashout_operations + FOR EACH ROW EXECUTE FUNCTION cashout_link(); + +CREATE OR REPLACE FUNCTION cashin_link() +RETURNS trigger +LANGUAGE plpgsql AS $$ + DECLARE + now_date BIGINT; + payto_uri TEXT; + local_amount libeufin_bank.taler_amount; + subject TEXT; + too_small BOOLEAN; + balance_insufficient BOOLEAN; + no_account BOOLEAN; + BEGIN + SELECT (amount).val, (amount).frac, wire_transfer_subject, execution_time, debit_payto_uri + INTO local_amount.val, local_amount.frac, subject, now_date, payto_uri + FROM libeufin_nexus.incoming_transactions + WHERE incoming_transaction_id = NEW.incoming_transaction_id; + SET search_path TO libeufin_bank; + SELECT out_too_small, out_balance_insufficient, out_no_account + INTO too_small, balance_insufficient, no_account + FROM libeufin_bank.cashin(now_date, payto_uri, local_amount, subject); + SET search_path TO libeufin_nexus; + + IF no_account THEN + RAISE EXCEPTION 'TODO soft error bounce: unknown account'; + END IF; + IF too_small THEN + RAISE EXCEPTION 'TODO soft error bounce: too small amount'; + END IF; + IF balance_insufficient THEN + RAISE EXCEPTION 'TODO hard error bounce'; + END IF; + RETURN NEW; + END; +$$; + +CREATE OR REPLACE TRIGGER cashin_link BEFORE INSERT ON libeufin_nexus.talerable_incoming_transactions + FOR EACH ROW EXECUTE FUNCTION cashin_link(); + +COMMIT; +\ No newline at end of file diff --git a/database-versioning/libeufin-conversion.sql b/database-versioning/libeufin-conversion.sql @@ -1,78 +0,0 @@ -BEGIN; -SET search_path TO libeufin_conversion; - -CREATE OR REPLACE FUNCTION cashout() -RETURNS trigger -LANGUAGE plpgsql AS $$ - DECLARE - now_date BIGINT; - payto_uri TEXT; - BEGIN - IF NEW.local_transaction IS NOT NULL THEN - SELECT transaction_date INTO now_date - FROM libeufin_bank.bank_account_transactions - WHERE bank_transaction_id = NEW.local_transaction; - SELECT cashout_payto INTO payto_uri - FROM libeufin_bank.bank_accounts - JOIN libeufin_bank.customers ON customer_id=owning_customer_id - WHERE bank_account_id=NEW.bank_account; - INSERT INTO libeufin_nexus.initiated_outgoing_transactions ( - amount - ,wire_transfer_subject - ,credit_payto_uri - ,initiation_time - ,request_uid - ) VALUES ( - ((NEW.amount_credit).val, (NEW.amount_credit).frac)::libeufin_nexus.taler_amount - ,NEW.subject - ,payto_uri - ,now_date - ,'TODO' -- How to generate this - ); - END IF; - RETURN NEW; - END; -$$; - -CREATE OR REPLACE TRIGGER cashout BEFORE INSERT OR UPDATE ON libeufin_bank.cashout_operations - FOR EACH ROW EXECUTE FUNCTION cashout(); - -CREATE OR REPLACE FUNCTION cashin() -RETURNS trigger -LANGUAGE plpgsql AS $$ - DECLARE - now_date BIGINT; - payto_uri TEXT; - local_amount libeufin_bank.taler_amount; - subject TEXT; - too_small BOOLEAN; - balance_insufficient BOOLEAN; - no_account BOOLEAN; - BEGIN - SELECT (amount).val, (amount).frac, wire_transfer_subject, execution_time, debit_payto_uri - INTO local_amount.val, local_amount.frac, subject, now_date, payto_uri - FROM libeufin_nexus.incoming_transactions - WHERE incoming_transaction_id = NEW.incoming_transaction_id; - SET search_path TO libeufin_bank; - SELECT out_too_small, out_balance_insufficient, out_no_account - INTO too_small, balance_insufficient, no_account - FROM libeufin_bank.cashin(now_date, payto_uri, local_amount, subject); - SET search_path TO libeufin_conversion; - - IF no_account THEN - RAISE EXCEPTION 'TODO soft error bounce: unknown account'; - END IF; - IF too_small THEN - RAISE EXCEPTION 'TODO soft error bounce: too small amount'; - END IF; - IF balance_insufficient THEN - RAISE EXCEPTION 'TODO hard error bounce'; - END IF; - RETURN NEW; - END; -$$; - -CREATE OR REPLACE TRIGGER cashin BEFORE INSERT ON libeufin_nexus.talerable_incoming_transactions - FOR EACH ROW EXECUTE FUNCTION cashin(); - -COMMIT; -\ No newline at end of file diff --git a/integration/test/IntegrationTest.kt b/integration/test/IntegrationTest.kt @@ -69,7 +69,6 @@ class IntegrationTest { nexusCmd.run("dbinit -c ../bank/conf/test.conf -r") val bankCmd = LibeufinBankCommand(); bankCmd.run("dbinit -c ../bank/conf/test.conf -r") - bankCmd.run("conversion-setup -c ../bank/conf/test.conf") kotlin.concurrent.thread(isDaemon = true) { bankCmd.run("serve -c ../bank/conf/test.conf") } @@ -99,43 +98,48 @@ class IntegrationTest { }.assertCreated() // Cashin - val reservePub = randBytes(32); - nexusDb.incomingTalerablePaymentCreate(IncomingPayment( - amount = NexusAmount(44, 0, "EUR"), - debitPaytoUri = userPayTo.canonical, - wireTransferSubject = "cashin test", - executionTime = Instant.now(), - bankTransferId = "entropic"), - reservePub) - val converted = client.get("http://0.0.0.0:8080/conversion-info/cashin-rate?amount_debit=EUR:44.0") - .assertOkJson<ConversionResponse>().amount_credit - client.get("http://0.0.0.0:8080/accounts/customer/transactions") { - basicAuth("customer", "password") - }.assertOkJson<BankAccountTransactionsResponse> { - val tx = it.transactions[0] - assertEquals(userPayTo.canonical, tx.creditor_payto_uri) - assertEquals("cashin test", tx.subject) - assertEquals(converted, tx.amount) + repeat(3) { i -> + val reservePub = randBytes(32); + val amount = NexusAmount(20L + i, 0, "EUR") + nexusDb.incomingTalerablePaymentCreate(IncomingPayment( + amount = amount, + debitPaytoUri = userPayTo.canonical, + wireTransferSubject = "cashin test $i", + executionTime = Instant.now(), + bankTransferId = "entropic"), + reservePub) + val converted = client.get("http://0.0.0.0:8080/conversion-info/cashin-rate?amount_debit=EUR:${20 + i}") + .assertOkJson<ConversionResponse>().amount_credit + client.get("http://0.0.0.0:8080/accounts/customer/transactions") { + basicAuth("customer", "password") + }.assertOkJson<BankAccountTransactionsResponse> { + val tx = it.transactions.first() + assertEquals(userPayTo.canonical, tx.creditor_payto_uri) + assertEquals("cashin test $i", tx.subject) + assertEquals(converted, tx.amount) + } } // Cashout - val requestUid = randBytes(32); - val amount = BankAmount("KUDOS:25") - val convert = client.get("http://0.0.0.0:8080/conversion-info/cashout-rate?amount_debit=$amount") - .assertOkJson<ConversionResponse>().amount_credit; - client.post("http://0.0.0.0:8080/accounts/customer/cashouts") { - basicAuth("customer", "password") - json { - "request_uid" to ShortHashCode(requestUid) - "amount_debit" to amount - "amount_credit" to convert - } - }.assertOkJson<CashoutPending> { - val code = File("/tmp/tan-+99.txt").readText() - client.post("http://0.0.0.0:8080/accounts/customer/cashouts/${it.cashout_id}/confirm") { + repeat(3) { i -> + val requestUid = randBytes(32); + val amount = BankAmount("KUDOS:${10+i}") + val convert = client.get("http://0.0.0.0:8080/conversion-info/cashout-rate?amount_debit=$amount") + .assertOkJson<ConversionResponse>().amount_credit; + client.post("http://0.0.0.0:8080/accounts/customer/cashouts") { basicAuth("customer", "password") - json { "tan" to code } - }.assertNoContent() + json { + "request_uid" to ShortHashCode(requestUid) + "amount_debit" to amount + "amount_credit" to convert + } + }.assertOkJson<CashoutPending> { + val code = File("/tmp/tan-+99.txt").readText() + client.post("http://0.0.0.0:8080/accounts/customer/cashouts/${it.cashout_id}/confirm") { + basicAuth("customer", "password") + json { "tan" to code } + }.assertNoContent() + } } } }