summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile6
-rwxr-xr-xcontrib/libeufin-bank-dbinit107
-rw-r--r--database-versioning/new/drop-sandbox.sql7
-rw-r--r--database-versioning/new/libeufin-bank-0001.sql (renamed from database-versioning/new/sandbox-0001-refactor.sql)0
-rw-r--r--database-versioning/new/libeufin-bank-drop.sql9
-rw-r--r--database-versioning/new/procedures.sql87
-rw-r--r--database-versioning/new/versioning.sql298
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/DB_helpers.kt123
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt76
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt1
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/server/Helpers.kt72
-rw-r--r--nexus/src/test/kotlin/Iso20022Test.kt3
-rw-r--r--nexus/src/test/kotlin/PostFinance.kt5
-rw-r--r--nexus/src/test/kotlin/SandboxAccessApiTest.kt1
-rw-r--r--sandbox/src/main/kotlin/tech/libeufin/sandbox/Database.kt185
-rw-r--r--sandbox/src/test/kotlin/DatabaseTest.kt24
16 files changed, 797 insertions, 207 deletions
diff --git a/Makefile b/Makefile
index 920a8038..6ecb01f0 100644
--- a/Makefile
+++ b/Makefile
@@ -54,6 +54,12 @@ install-db-versioning:
@sed "s|__STATIC_PATCHES_LOCATION__|$(prefix)/share/libeufin/sql|" < contrib/$(LOAD_SQL_SCRIPT_NAME) > build/$(LOAD_SQL_SCRIPT_NAME)
@install -D database-versioning/*.sql -t $(prefix)/share/libeufin/sql
@install -D build/$(LOAD_SQL_SCRIPT_NAME) -t $(prefix)/bin
+ $(eval SANDBOX_DBINIT_SCRIPT := libeufin-bank-dbinit)
+ @sed "s|__SANDBOX_STATIC_PATCHES_LOCATION__|$(prefix)/share/libeufin/sql/bank|" < contrib/$(SANDBOX_DBINIT_SCRIPT) > build/$(SANDBOX_DBINIT_SCRIPT)
+ @install -D database-versioning/new/libeufin-bank*.sql -t $(prefix)/share/libeufin/sql/bank
+ @install -D database-versioning/new/versioning.sql -t $(prefix)/share/libeufin/sql/bank
+ @install -D database-versioning/new/procedures.sql -t $(prefix)/share/libeufin/sql/bank
+ @install -D build/$(SANDBOX_DBINIT_SCRIPT) -t $(prefix)/bin
.PHONY: assemble
assemble:
diff --git a/contrib/libeufin-bank-dbinit b/contrib/libeufin-bank-dbinit
new file mode 100755
index 00000000..ae973e86
--- /dev/null
+++ b/contrib/libeufin-bank-dbinit
@@ -0,0 +1,107 @@
+#!/bin/bash
+
+set -eu
+
+# The only CLI argument is 'nexus' or 'sandbox',
+# indicating which service will get its database prepared.
+
+fail () {
+ echo $1
+ exit 1
+}
+
+usage_and_exit () {
+ echo Usage: libeufin-bank-dbinit OPTIONS
+ echo
+ echo By default, this command creates and/or patches the Sandbox tables.
+ echo Pass '-r' to drop the tables before creating them again.
+ echo
+ echo 'Supported options:'
+ echo ' -d DB_CONN -- required. Pass DB_CONN as the postgres connection string. Passed verbatim to Psql'
+ echo ' -l LOC -- required. Pass LOC as the SQL files location. Typically $prefix/share/libeufin/sql'
+ echo ' -h -- print this help'
+ echo ' -r -- drop all the tables before creating them again'
+ exit 0
+}
+
+run_sql_file () {
+ # -q doesn't hide all the output, hence the
+ # redirection to /dev/null.
+ psql -d $DB_CONNECTION \
+ -q \
+ -f $1 \
+ --set ON_ERROR_STOP=1 > /dev/null
+}
+
+get_patch_path () {
+ echo "$PATCHES_LOCATION/$1"
+}
+
+# The real check happens (by the caller)
+# by checking the returned text.
+check_patch_applied () {
+ psql -d $DB_CONNECTION \
+ -t \
+ -c "SELECT applied_by FROM _v.patches WHERE patch_name = '$1' LIMIT 1"
+}
+
+# Iterates over the .sql migration files and applies
+# the new ones.
+iterate_over_patches () {
+ cd $PATCHES_LOCATION
+ for patch_filename in $(ls -1 -v libeufin-bank-[0-9][0-9][0-9][0-9].sql); do
+ patch_name=$(echo $patch_filename | cut -f1 -d.) # drops the final .sql
+ echo Checking patch: "$patch_name"
+ maybe_applied=$(check_patch_applied "$patch_name")
+ if test -n "$maybe_applied"; then continue; fi
+ # patch not applied, apply it.
+ echo Patch $patch_name not applied, applying it.
+ run_sql_file $patch_filename
+ done
+ cd - > /dev/null # cd to previous location.
+}
+
+if test $# -eq 0; then
+ usage_and_exit
+fi
+
+while getopts ":d:l:hr" OPTION; do
+ case "$OPTION" in
+ d)
+ DB_CONNECTION="$OPTARG" # only one required.
+ ;;
+ l)
+ PATCHES_LOCATION="$OPTARG"
+ ;;
+ r)
+ DROP="YES"
+ ;;
+ h)
+ usage_and_exit
+ ;;
+ ?)
+ fail 'Unrecognized command line option'
+ ;;
+ esac
+done
+
+# Checking required options.
+if test -z "${PATCHES_LOCATION:-}"; then
+ # This value is substituted by GNU make at installation time.
+ PATCHES_LOCATION=__SANDBOX_STATIC_PATCHES_LOCATION__
+fi
+if test -z "${DB_CONNECTION:-}"; then
+ fail "Required option '-d' was missing."
+fi
+
+run_sql_file $(get_patch_path "versioning.sql")
+if test "${DROP:-}" = "YES"; then
+ maybe_applied=$(check_patch_applied "libeufin-bank-0001")
+ if test -n "$maybe_applied"; then
+ run_sql_file $(get_patch_path "libeufin-bank-drop.sql")
+ else
+ echo "Nothing to drop"
+ fi
+fi
+iterate_over_patches
+run_sql_file $(get_patch_path "procedures.sql")
diff --git a/database-versioning/new/drop-sandbox.sql b/database-versioning/new/drop-sandbox.sql
deleted file mode 100644
index a1ab9765..00000000
--- a/database-versioning/new/drop-sandbox.sql
+++ /dev/null
@@ -1,7 +0,0 @@
-BEGIN;
-
-SELECT _v.unregister_patch('sandbox-0001');
-
-DROP SCHEMA sandbox CASCADE;
-
-COMMIT;
diff --git a/database-versioning/new/sandbox-0001-refactor.sql b/database-versioning/new/libeufin-bank-0001.sql
index b5492041..b5492041 100644
--- a/database-versioning/new/sandbox-0001-refactor.sql
+++ b/database-versioning/new/libeufin-bank-0001.sql
diff --git a/database-versioning/new/libeufin-bank-drop.sql b/database-versioning/new/libeufin-bank-drop.sql
new file mode 100644
index 00000000..123481a1
--- /dev/null
+++ b/database-versioning/new/libeufin-bank-drop.sql
@@ -0,0 +1,9 @@
+BEGIN;
+
+-- NOTE: The following unregistration would affect the
+-- legacy database schema too. That's acceptable as the
+-- legacy schema is being removed.
+SELECT _v.unregister_patch('libeufin-bank-0001');
+DROP SCHEMA libeufin_bank CASCADE;
+
+COMMIT;
diff --git a/database-versioning/new/procedures.sql b/database-versioning/new/procedures.sql
new file mode 100644
index 00000000..5798c5fd
--- /dev/null
+++ b/database-versioning/new/procedures.sql
@@ -0,0 +1,87 @@
+BEGIN;
+SET search_path TO libeufin_bank;
+
+CREATE OR REPLACE FUNCTION amount_normalize(
+ IN amount taler_amount
+ ,OUT normalized taler_amount
+)
+LANGUAGE plpgsql
+AS $$
+BEGIN
+ normalized.val = amount.val + amount.frac / 100000000;
+ normalized.frac = amount.frac % 100000000;
+END $$;
+COMMENT ON FUNCTION amount_normalize
+ IS 'Returns the normalized amount by adding to the .val the value of (.frac / 100000000) and removing the modulus 100000000 from .frac.';
+
+CREATE OR REPLACE FUNCTION amount_add(
+ IN a taler_amount
+ ,IN b taler_amount
+ ,OUT sum taler_amount
+)
+LANGUAGE plpgsql
+AS $$
+BEGIN
+ sum = (a.val + b.val, a.frac + b.frac);
+ CALL amount_normalize(sum ,sum);
+ IF (sum.val > (1<<52))
+ THEN
+ RAISE EXCEPTION 'addition overflow';
+ END IF;
+END $$;
+COMMENT ON FUNCTION amount_add
+ IS 'Returns the normalized sum of two amounts. It raises an exception when the resulting .val is larger than 2^52';
+
+CREATE OR REPLACE FUNCTION amount_left_minus_right(
+ IN l taler_amount
+ ,IN r taler_amount
+ ,OUT diff taler_amount
+ ,OUT ok BOOLEAN
+)
+LANGUAGE plpgsql
+AS $$
+BEGIN
+IF (l.val > r.val)
+THEN
+ ok = TRUE;
+ IF (l.frac >= r.frac)
+ THEN
+ diff.val = l.val - r.val;
+ diff.frac = l.frac - r.frac;
+ ELSE
+ diff.val = l.val - r.val - 1;
+ diff.frac = l.frac + 100000000 - r.frac;
+ END IF;
+ELSE
+ IF (l.val = r.val) AND (l.frac >= r.frac)
+ THEN
+ diff.val = 0;
+ diff.frac = l.frac - r.frac;
+ ok = TRUE;
+ ELSE
+ diff = (-1, -1);
+ ok = FALSE;
+ END IF;
+END IF;
+RETURN;
+END $$;
+COMMENT ON FUNCTION amount_left_minus_right
+ IS 'Subtracts the right amount from the left and returns the difference and TRUE, if the left amount is larger than the right, or an invalid amount and FALSE otherwise.';
+
+CREATE OR REPLACE PROCEDURE bank_set_config(
+ IN in_key TEXT,
+ IN in_value TEXT
+)
+LANGUAGE plpgsql
+AS $$
+BEGIN
+UPDATE configuration SET config_value=in_value WHERE config_key=in_key;
+IF NOT FOUND
+THEN
+ INSERT INTO configuration (config_key, config_value) VALUES (in_key, in_value);
+END IF;
+
+END $$;
+COMMENT ON PROCEDURE bank_set_config(TEXT, TEXT)
+ IS 'Update or insert configuration values';
+COMMIT; \ No newline at end of file
diff --git a/database-versioning/new/versioning.sql b/database-versioning/new/versioning.sql
new file mode 100644
index 00000000..c7fa8121
--- /dev/null
+++ b/database-versioning/new/versioning.sql
@@ -0,0 +1,298 @@
+-- LICENSE AND COPYRIGHT
+--
+-- Copyright (C) 2010 Hubert depesz Lubaczewski
+--
+-- This program is distributed under the (Revised) BSD License:
+-- L<http://www.opensource.org/licenses/bsd-license.php>
+--
+-- Redistribution and use in source and binary forms, with or without
+-- modification, are permitted provided that the following conditions
+-- are met:
+--
+-- * Redistributions of source code must retain the above copyright
+-- notice, this list of conditions and the following disclaimer.
+--
+-- * Redistributions in binary form must reproduce the above copyright
+-- notice, this list of conditions and the following disclaimer in the
+-- documentation and/or other materials provided with the distribution.
+--
+-- * Neither the name of Hubert depesz Lubaczewski's Organization
+-- nor the names of its contributors may be used to endorse or
+-- promote products derived from this software without specific
+-- prior written permission.
+--
+-- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+-- AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+-- IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+-- DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+-- FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+-- DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+-- SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+-- CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+-- OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+-- OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+--
+-- Code origin: https://gitlab.com/depesz/Versioning/blob/master/install.versioning.sql
+--
+--
+-- # NAME
+--
+-- **Versioning** - simplistic take on tracking and applying changes to databases.
+--
+-- # DESCRIPTION
+--
+-- This project strives to provide simple way to manage changes to
+-- database.
+--
+-- Instead of making changes on development server, then finding
+-- differences between production and development, deciding which ones
+-- should be installed on production, and finding a way to install them -
+-- you start with writing diffs themselves!
+--
+-- # INSTALLATION
+--
+-- To install versioning simply run install.versioning.sql in your database
+-- (all of them: production, stage, test, devel, ...).
+--
+-- # USAGE
+--
+-- In your files with patches to database, put whole logic in single
+-- transaction, and use \_v.\* functions - usually \_v.register_patch() at
+-- least to make sure everything is OK.
+--
+-- For example. Let's assume you have patch files:
+--
+-- ## 0001.sql:
+--
+-- ```
+-- create table users (id serial primary key, username text);
+-- ```
+--
+-- ## 0002.sql:
+--
+-- ```
+-- insert into users (username) values ('depesz');
+-- ```
+-- To change it to use versioning you would change the files, to this
+-- state:
+--
+-- 0000.sql:
+--
+-- ```
+-- BEGIN;
+-- select _v.register_patch('000-base', NULL, NULL);
+-- create table users (id serial primary key, username text);
+-- COMMIT;
+-- ```
+--
+-- ## 0002.sql:
+--
+-- ```
+-- BEGIN;
+-- select _v.register_patch('001-users', ARRAY['000-base'], NULL);
+-- insert into users (username) values ('depesz');
+-- COMMIT;
+-- ```
+--
+-- This will make sure that patch 001-users can only be applied after
+-- 000-base.
+--
+-- # AVAILABLE FUNCTIONS
+--
+-- ## \_v.register_patch( TEXT )
+--
+-- Registers named patch, or dies if it is already registered.
+--
+-- Returns integer which is id of patch in \_v.patches table - only if it
+-- succeeded.
+--
+-- ## \_v.register_patch( TEXT, TEXT[] )
+--
+-- Same as \_v.register_patch( TEXT ), but checks is all given patches (given as
+-- array in second argument) are already registered.
+--
+-- ## \_v.register_patch( TEXT, TEXT[], TEXT[] )
+--
+-- Same as \_v.register_patch( TEXT, TEXT[] ), but also checks if there are no conflicts with preexisting patches.
+--
+-- Third argument is array of names of patches that conflict with current one. So
+-- if any of them is installed - register_patch will error out.
+--
+-- ## \_v.unregister_patch( TEXT )
+--
+-- Removes information about given patch from the versioning data.
+--
+-- It doesn't remove objects that were created by this patch - just removes
+-- metainformation.
+--
+-- ## \_v.assert_user_is_superuser()
+--
+-- Make sure that current patch is being loaded by superuser.
+--
+-- If it's not - it will raise exception, and break transaction.
+--
+-- ## \_v.assert_user_is_not_superuser()
+--
+-- Make sure that current patch is not being loaded by superuser.
+--
+-- If it is - it will raise exception, and break transaction.
+--
+-- ## \_v.assert_user_is_one_of(TEXT, TEXT, ... )
+--
+-- Make sure that current patch is being loaded by one of listed users.
+--
+-- If ```current_user``` is not listed as one of arguments - function will raise
+-- exception and break the transaction.
+
+BEGIN;
+
+-- Added by Christian Grothoff to support concurrency, see
+-- https://stackoverflow.com/questions/29900845/create-schema-if-not-exists-raises-duplicate-key-error?rq=4
+LOCK TABLE pg_catalog.pg_namespace;
+
+
+-- This file adds versioning support to database it will be loaded to.
+-- It requires that PL/pgSQL is already loaded - will raise exception otherwise.
+-- All versioning "stuff" (tables, functions) is in "_v" schema.
+
+-- All functions are defined as 'RETURNS SETOF INT4' to be able to make them to RETURN literally nothing (0 rows).
+-- >> RETURNS VOID<< IS similar, but it still outputs "empty line" in psql when calling
+CREATE SCHEMA IF NOT EXISTS _v;
+COMMENT ON SCHEMA _v IS 'Schema for versioning data and functionality.';
+
+CREATE TABLE IF NOT EXISTS _v.patches (
+ patch_name TEXT PRIMARY KEY,
+ applied_tsz TIMESTAMPTZ NOT NULL DEFAULT now(),
+ applied_by TEXT NOT NULL,
+ requires TEXT[],
+ conflicts TEXT[]
+);
+COMMENT ON TABLE _v.patches IS 'Contains information about what patches are currently applied on database.';
+COMMENT ON COLUMN _v.patches.patch_name IS 'Name of patch, has to be unique for every patch.';
+COMMENT ON COLUMN _v.patches.applied_tsz IS 'When the patch was applied.';
+COMMENT ON COLUMN _v.patches.applied_by IS 'Who applied this patch (PostgreSQL username)';
+COMMENT ON COLUMN _v.patches.requires IS 'List of patches that are required for given patch.';
+COMMENT ON COLUMN _v.patches.conflicts IS 'List of patches that conflict with given patch.';
+
+CREATE OR REPLACE FUNCTION _v.register_patch( IN in_patch_name TEXT, IN in_requirements TEXT[], in_conflicts TEXT[], OUT versioning INT4 ) RETURNS setof INT4 AS $$
+DECLARE
+ t_text TEXT;
+ t_text_a TEXT[];
+ i INT4;
+BEGIN
+ -- Thanks to this we know only one patch will be applied at a time
+ LOCK TABLE _v.patches IN EXCLUSIVE MODE;
+
+ SELECT patch_name INTO t_text FROM _v.patches WHERE patch_name = in_patch_name;
+ IF FOUND THEN
+ RAISE EXCEPTION 'Patch % is already applied!', in_patch_name;
+ END IF;
+
+ t_text_a := ARRAY( SELECT patch_name FROM _v.patches WHERE patch_name = any( in_conflicts ) );
+ IF array_upper( t_text_a, 1 ) IS NOT NULL THEN
+ RAISE EXCEPTION 'Versioning patches conflict. Conflicting patche(s) installed: %.', array_to_string( t_text_a, ', ' );
+ END IF;
+
+ IF array_upper( in_requirements, 1 ) IS NOT NULL THEN
+ t_text_a := '{}';
+ FOR i IN array_lower( in_requirements, 1 ) .. array_upper( in_requirements, 1 ) LOOP
+ SELECT patch_name INTO t_text FROM _v.patches WHERE patch_name = in_requirements[i];
+ IF NOT FOUND THEN
+ t_text_a := t_text_a || in_requirements[i];
+ END IF;
+ END LOOP;
+ IF array_upper( t_text_a, 1 ) IS NOT NULL THEN
+ RAISE EXCEPTION 'Missing prerequisite(s): %.', array_to_string( t_text_a, ', ' );
+ END IF;
+ END IF;
+
+ INSERT INTO _v.patches (patch_name, applied_tsz, applied_by, requires, conflicts ) VALUES ( in_patch_name, now(), current_user, coalesce( in_requirements, '{}' ), coalesce( in_conflicts, '{}' ) );
+ RETURN;
+END;
+$$ language plpgsql;
+COMMENT ON FUNCTION _v.register_patch( TEXT, TEXT[], TEXT[] ) IS 'Function to register patches in database. Raises exception if there are conflicts, prerequisites are not installed or the migration has already been installed.';
+
+CREATE OR REPLACE FUNCTION _v.register_patch( TEXT, TEXT[] ) RETURNS setof INT4 AS $$
+ SELECT _v.register_patch( $1, $2, NULL );
+$$ language sql;
+COMMENT ON FUNCTION _v.register_patch( TEXT, TEXT[] ) IS 'Wrapper to allow registration of patches without conflicts.';
+CREATE OR REPLACE FUNCTION _v.register_patch( TEXT ) RETURNS setof INT4 AS $$
+ SELECT _v.register_patch( $1, NULL, NULL );
+$$ language sql;
+COMMENT ON FUNCTION _v.register_patch( TEXT ) IS 'Wrapper to allow registration of patches without requirements and conflicts.';
+
+CREATE OR REPLACE FUNCTION _v.unregister_patch( IN in_patch_name TEXT, OUT versioning INT4 ) RETURNS setof INT4 AS $$
+DECLARE
+ i INT4;
+ t_text_a TEXT[];
+BEGIN
+ -- Thanks to this we know only one patch will be applied at a time
+ LOCK TABLE _v.patches IN EXCLUSIVE MODE;
+
+ t_text_a := ARRAY( SELECT patch_name FROM _v.patches WHERE in_patch_name = ANY( requires ) );
+ IF array_upper( t_text_a, 1 ) IS NOT NULL THEN
+ RAISE EXCEPTION 'Cannot uninstall %, as it is required by: %.', in_patch_name, array_to_string( t_text_a, ', ' );
+ END IF;
+
+ DELETE FROM _v.patches WHERE patch_name = in_patch_name;
+ GET DIAGNOSTICS i = ROW_COUNT;
+ IF i < 1 THEN
+ RAISE EXCEPTION 'Patch % is not installed, so it can''t be uninstalled!', in_patch_name;
+ END IF;
+
+ RETURN;
+END;
+$$ language plpgsql;
+COMMENT ON FUNCTION _v.unregister_patch( TEXT ) IS 'Function to unregister patches in database. Dies if the patch is not registered, or if unregistering it would break dependencies.';
+
+CREATE OR REPLACE FUNCTION _v.assert_patch_is_applied( IN in_patch_name TEXT ) RETURNS TEXT as $$
+DECLARE
+ t_text TEXT;
+BEGIN
+ SELECT patch_name INTO t_text FROM _v.patches WHERE patch_name = in_patch_name;
+ IF NOT FOUND THEN
+ RAISE EXCEPTION 'Patch % is not applied!', in_patch_name;
+ END IF;
+ RETURN format('Patch %s is applied.', in_patch_name);
+END;
+$$ language plpgsql;
+COMMENT ON FUNCTION _v.assert_patch_is_applied( TEXT ) IS 'Function that can be used to make sure that patch has been applied.';
+
+CREATE OR REPLACE FUNCTION _v.assert_user_is_superuser() RETURNS TEXT as $$
+DECLARE
+ v_super bool;
+BEGIN
+ SELECT usesuper INTO v_super FROM pg_user WHERE usename = current_user;
+ IF v_super THEN
+ RETURN 'assert_user_is_superuser: OK';
+ END IF;
+ RAISE EXCEPTION 'Current user is not superuser - cannot continue.';
+END;
+$$ language plpgsql;
+COMMENT ON FUNCTION _v.assert_user_is_superuser() IS 'Function that can be used to make sure that patch is being applied using superuser account.';
+
+CREATE OR REPLACE FUNCTION _v.assert_user_is_not_superuser() RETURNS TEXT as $$
+DECLARE
+ v_super bool;
+BEGIN
+ SELECT usesuper INTO v_super FROM pg_user WHERE usename = current_user;
+ IF v_super THEN
+ RAISE EXCEPTION 'Current user is superuser - cannot continue.';
+ END IF;
+ RETURN 'assert_user_is_not_superuser: OK';
+END;
+$$ language plpgsql;
+COMMENT ON FUNCTION _v.assert_user_is_not_superuser() IS 'Function that can be used to make sure that patch is being applied using normal (not superuser) account.';
+
+CREATE OR REPLACE FUNCTION _v.assert_user_is_one_of(VARIADIC p_acceptable_users TEXT[] ) RETURNS TEXT as $$
+DECLARE
+BEGIN
+ IF current_user = any( p_acceptable_users ) THEN
+ RETURN 'assert_user_is_one_of: OK';
+ END IF;
+ RAISE EXCEPTION 'User is not one of: % - cannot continue.', p_acceptable_users;
+END;
+$$ language plpgsql;
+COMMENT ON FUNCTION _v.assert_user_is_one_of(TEXT[]) IS 'Function that can be used to make sure that patch is being applied by one of defined users.';
+
+COMMIT;
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/DB_helpers.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/DB_helpers.kt
new file mode 100644
index 00000000..b39a05f2
--- /dev/null
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/DB_helpers.kt
@@ -0,0 +1,123 @@
+package tech.libeufin.nexus
+
+import com.fasterxml.jackson.databind.JsonNode
+import com.fasterxml.jackson.databind.node.ObjectNode
+import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
+import io.ktor.http.*
+import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
+import org.jetbrains.exposed.sql.and
+import org.jetbrains.exposed.sql.transactions.transaction
+import tech.libeufin.nexus.server.GetTransactionsParams
+import tech.libeufin.nexus.server.Pain001Data
+import tech.libeufin.util.notFound
+import java.time.Instant
+
+fun getBankAccount(label: String): NexusBankAccountEntity {
+ val maybeBankAccount = transaction {
+ NexusBankAccountEntity.findByName(label)
+ }
+ return maybeBankAccount ?:
+ throw NexusError(
+ HttpStatusCode.NotFound,
+ "Account $label not found"
+ )
+}
+
+/**
+ * Queries the database according to the GET /transactions
+ * parameters.
+ */
+fun getIngestedTransactions(params: GetTransactionsParams): List<JsonNode> =
+ transaction {
+ val bankAccount = getBankAccount(params.bankAccountId)
+ val maybeResult = NexusBankTransactionEntity.find {
+ NexusBankTransactionsTable.bankAccount eq bankAccount.id.value and (
+ NexusBankTransactionsTable.id greaterEq params.startIndex
+ )
+ }.sortedBy { it.id.value }.take(params.resultSize.toInt()) // Smallest index (= earliest transaction) first
+ // Converting the result to the HTTP response type.
+ maybeResult.map {
+ val element: ObjectNode = jacksonObjectMapper().createObjectNode()
+ element.put("index", it.id.value.toString())
+ val txObj: JsonNode = jacksonObjectMapper().readTree(it.transactionJson)
+ element.set<JsonNode>("camtData", txObj)
+ return@map element
+ }
+ }
+
+// Gets connection or throws.
+fun getBankConnection(connId: String): NexusBankConnectionEntity {
+ val maybeConn = transaction {
+ NexusBankConnectionEntity.find {
+ NexusBankConnectionsTable.connectionId eq connId
+ }.firstOrNull()
+ }
+ if (maybeConn == null) throw notFound("Bank connection $connId not found")
+ return maybeConn
+}
+
+/**
+ * Retrieve payment initiation from database, raising exception if not found.
+ */
+fun getPaymentInitiation(uuid: Long): PaymentInitiationEntity {
+ return transaction {
+ PaymentInitiationEntity.findById(uuid)
+ } ?: throw NexusError(
+ HttpStatusCode.NotFound,
+ "Payment '$uuid' not found"
+ )
+}
+
+/**
+ * Gets a prepared payment starting from its 'payment information id'.
+ * Note: although the terminology comes from CaMt, a 'payment information id'
+ * is indeed any UID that identifies the payment. For this reason, also
+ * the x-libeufin-bank logic uses this helper.
+ *
+ * Returns the prepared payment, or null if that's not found. Not throwing
+ * any exception because the null case is common: not every transaction being
+ * processed by Neuxs was prepared/initiated here; incoming transactions are
+ * one example.
+ */
+fun getPaymentInitiation(pmtInfId: String): PaymentInitiationEntity? =
+ transaction {
+ PaymentInitiationEntity.find(
+ PaymentInitiationsTable.paymentInformationId.eq(pmtInfId)
+ ).firstOrNull()
+ }
+
+/**
+ * Insert one row in the database, and leaves it marked as non-submitted.
+ * @param debtorAccount the mnemonic id assigned by the bank to one bank
+ * account of the subscriber that is creating the pain entity. In this case,
+ * it will be the account whose money will pay the wire transfer being defined
+ * by this pain document.
+ */
+fun addPaymentInitiation(
+ paymentData: Pain001Data,
+ debtorAccount: NexusBankAccountEntity
+): PaymentInitiationEntity {
+ return transaction {
+
+ val now = Instant.now().toEpochMilli()
+ val nowHex = now.toString(16)
+ val painCounter = debtorAccount.pain001Counter++
+ val painHex = painCounter.toString(16)
+ val acctHex = debtorAccount.id.value.toString(16)
+
+ PaymentInitiationEntity.new {
+ currency = paymentData.currency
+ bankAccount = debtorAccount
+ subject = paymentData.subject
+ sum = paymentData.sum
+ creditorName = paymentData.creditorName
+ creditorBic = paymentData.creditorBic
+ creditorIban = paymentData.creditorIban
+ preparationDate = now
+ endToEndId = paymentData.endToEndId ?: "leuf-e-$nowHex-$painHex-$acctHex"
+ messageId = "leuf-mp1-$nowHex-$painHex-$acctHex"
+ paymentInformationId = "leuf-p-$nowHex-$painHex-$acctHex"
+ instructionId = "leuf-i-$nowHex-$painHex-$acctHex"
+ }
+ }
+}
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt
index 26453fac..5e398d7e 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt
@@ -38,23 +38,6 @@ import java.time.ZonedDateTime
private val keepBankMessages: String? = System.getenv("LIBEUFIN_NEXUS_KEEP_BANK_MESSAGES")
-/**
- * Gets a prepared payment starting from its 'payment information id'.
- * Note: although the terminology comes from CaMt, a 'payment information id'
- * is indeed any UID that identifies the payment. For this reason, also
- * the x-libeufin-bank logic uses this helper.
- *
- * Returns the prepared payment, or null if that's not found. Not throwing
- * any exception because the null case is common: not every transaction being
- * processed by Neuxs was prepared/initiated here; incoming transactions are
- * one example.
- */
-fun getPaymentInitiation(pmtInfId: String): PaymentInitiationEntity? =
- transaction {
- PaymentInitiationEntity.find(
- PaymentInitiationsTable.paymentInformationId.eq(pmtInfId)
- ).firstOrNull()
- }
fun requireBankAccount(call: ApplicationCall, parameterKey: String): NexusBankAccountEntity {
val name = call.parameters[parameterKey]
if (name == null)
@@ -283,18 +266,6 @@ fun ingestBankMessagesIntoAccount(
)
}
-/**
- * Retrieve payment initiation from database, raising exception if not found.
- */
-fun getPaymentInitiation(uuid: Long): PaymentInitiationEntity {
- return transaction {
- PaymentInitiationEntity.findById(uuid)
- } ?: throw NexusError(
- HttpStatusCode.NotFound,
- "Payment '$uuid' not found"
- )
-}
-
data class LastMessagesTimes(
val lastStatement: ZonedDateTime?,
val lastReport: ZonedDateTime?,
@@ -323,57 +294,12 @@ fun getLastMessagesTimes(acct: NexusBankAccountEntity): LastMessagesTimes {
}
)
}
-fun getBankAccount(label: String): NexusBankAccountEntity {
- val maybeBankAccount = transaction {
- NexusBankAccountEntity.findByName(label)
- }
- return maybeBankAccount ?:
- throw NexusError(
- HttpStatusCode.NotFound,
- "Account $label not found"
- )
-}
+
fun addPaymentInitiation(paymentData: Pain001Data, debtorAccount: String): PaymentInitiationEntity {
val bankAccount = getBankAccount(debtorAccount)
return addPaymentInitiation(paymentData, bankAccount)
}
-/**
- * Insert one row in the database, and leaves it marked as non-submitted.
- * @param debtorAccount the mnemonic id assigned by the bank to one bank
- * account of the subscriber that is creating the pain entity. In this case,
- * it will be the account whose money will pay the wire transfer being defined
- * by this pain document.
- */
-fun addPaymentInitiation(
- paymentData: Pain001Data,
- debtorAccount: NexusBankAccountEntity
-): PaymentInitiationEntity {
- return transaction {
-
- val now = Instant.now().toEpochMilli()
- val nowHex = now.toString(16)
- val painCounter = debtorAccount.pain001Counter++
- val painHex = painCounter.toString(16)
- val acctHex = debtorAccount.id.value.toString(16)
-
- PaymentInitiationEntity.new {
- currency = paymentData.currency
- bankAccount = debtorAccount
- subject = paymentData.subject
- sum = paymentData.sum
- creditorName = paymentData.creditorName
- creditorBic = paymentData.creditorBic
- creditorIban = paymentData.creditorIban
- preparationDate = now
- endToEndId = paymentData.endToEndId ?: "leuf-e-$nowHex-$painHex-$acctHex"
- messageId = "leuf-mp1-$nowHex-$painHex-$acctHex"
- paymentInformationId = "leuf-p-$nowHex-$painHex-$acctHex"
- instructionId = "leuf-i-$nowHex-$painHex-$acctHex"
- }
- }
-}
-
suspend fun fetchBankAccountTransactions(
client: HttpClient,
fetchSpec: FetchSpecJson,
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt
index 18e1bb08..7af1db69 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt
@@ -47,7 +47,6 @@ import org.jetbrains.exposed.sql.statements.api.ExposedBlob
import org.jetbrains.exposed.sql.transactions.transaction
import tech.libeufin.nexus.*
import tech.libeufin.nexus.bankaccount.getLastMessagesTimes
-import tech.libeufin.nexus.bankaccount.getPaymentInitiation
import tech.libeufin.nexus.iso20022.NexusPaymentInitiationData
import tech.libeufin.nexus.iso20022.createPain001document
import tech.libeufin.nexus.logger
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/server/Helpers.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/server/Helpers.kt
index a55cbbd2..175509cd 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/server/Helpers.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/server/Helpers.kt
@@ -1,15 +1,8 @@
package tech.libeufin.nexus.server
-import com.fasterxml.jackson.databind.JsonNode
-import com.fasterxml.jackson.databind.node.ObjectNode
-import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import io.ktor.http.*
-import org.jetbrains.exposed.sql.and
-import org.jetbrains.exposed.sql.transactions.transaction
import tech.libeufin.nexus.*
-import tech.libeufin.nexus.bankaccount.getBankAccount
import tech.libeufin.util.internalServerError
-import tech.libeufin.util.notFound
// Type holding parameters of GET /transactions.
data class GetTransactionsParams(
@@ -18,28 +11,6 @@ data class GetTransactionsParams(
val resultSize: Long
)
-/**
- * Queries the database according to the GET /transactions
- * parameters.
- */
-fun getIngestedTransactions(params: GetTransactionsParams): List<JsonNode> =
- transaction {
- val bankAccount = getBankAccount(params.bankAccountId)
- val maybeResult = NexusBankTransactionEntity.find {
- NexusBankTransactionsTable.bankAccount eq bankAccount.id.value and (
- NexusBankTransactionsTable.id greaterEq params.startIndex
- )
- }.sortedBy { it.id.value }.take(params.resultSize.toInt()) // Smallest index (= earliest transaction) first
- // Converting the result to the HTTP response type.
- maybeResult.map {
- val element: ObjectNode = jacksonObjectMapper().createObjectNode()
- element.put("index", it.id.value.toString())
- val txObj: JsonNode = jacksonObjectMapper().readTree(it.transactionJson)
- element.set<JsonNode>("camtData", txObj)
- return@map element
- }
- }
-
fun unknownBankAccount(bankAccountLabel: String): NexusError {
return NexusError(
HttpStatusCode.NotFound,
@@ -53,7 +24,6 @@ fun unknownBankAccount(bankAccountLabel: String): NexusError {
* be employed wherever now type names are passed as plain
* strings.
*/
-
enum class EbicsDialects(val dialectName: String) {
POSTFINANCE("pf")
}
@@ -106,46 +76,4 @@ enum class BankConnectionType(val typeName: String) {
enum class NexusFacadeType(val facadeType: String) {
TALER("taler-wire-gateway"),
ANASTASIS("anastasis")
-}
-
-/**
- * These types point at the _content_ brought by bank connections.
- * The following stack depicts the layering of banking communication
- * as modeled here in Nexus. On top the most inner layer.
- *
- * --------------------
- * Banking data type
- * --------------------
- * Bank connection type
- * --------------------
- * HTTP
- * --------------------
- *
- * Once the banking data type arrives to the local database, facades
- * types MAY apply further processing to it.
- *
- * For example, a Taler facade WILL look for Taler-meaningful wire
- * subjects and act accordingly. Even without a facade, the Nexus
- * native HTTP API picks instances of banking data and extracts its
- * details to serve to the client.
- *
- * NOTE: this type MAY help but is NOT essential, as each connection
- * is USUALLY tied with the same banking data type. For example, EBICS
- * brings CaMt, and x-libeufin-bank bring its own (same-named x-libeufin-bank)
- * banking data type.
- */
-enum class BankingDataType {
- X_LIBEUFIN_BANK,
- CAMT
-}
-
-// Gets connection or throws.
-fun getBankConnection(connId: String): NexusBankConnectionEntity {
- val maybeConn = transaction {
- NexusBankConnectionEntity.find {
- NexusBankConnectionsTable.connectionId eq connId
- }.firstOrNull()
- }
- if (maybeConn == null) throw notFound("Bank connection $connId not found")
- return maybeConn
} \ No newline at end of file
diff --git a/nexus/src/test/kotlin/Iso20022Test.kt b/nexus/src/test/kotlin/Iso20022Test.kt
index 848e162a..06d14ce1 100644
--- a/nexus/src/test/kotlin/Iso20022Test.kt
+++ b/nexus/src/test/kotlin/Iso20022Test.kt
@@ -12,12 +12,9 @@ import org.w3c.dom.Document
import poFiCamt054_2019_incoming
import poFiCamt054_2019_outgoing
import prepNexusDb
-import tech.libeufin.nexus.bankaccount.getBankAccount
import tech.libeufin.nexus.iso20022.*
import tech.libeufin.nexus.server.EbicsDialects
import tech.libeufin.nexus.server.FetchLevel
-import tech.libeufin.nexus.server.getBankConnection
-import tech.libeufin.nexus.server.nexusApp
import tech.libeufin.util.DestructionError
import tech.libeufin.util.XMLUtil
import tech.libeufin.util.destructXml
diff --git a/nexus/src/test/kotlin/PostFinance.kt b/nexus/src/test/kotlin/PostFinance.kt
index 1dce6d62..8c392487 100644
--- a/nexus/src/test/kotlin/PostFinance.kt
+++ b/nexus/src/test/kotlin/PostFinance.kt
@@ -7,10 +7,11 @@ import kotlinx.coroutines.runBlocking
import org.jetbrains.exposed.sql.transactions.transaction
import tech.libeufin.nexus.bankaccount.addPaymentInitiation
import tech.libeufin.nexus.bankaccount.fetchBankAccountTransactions
-import tech.libeufin.nexus.bankaccount.getBankAccount
import tech.libeufin.nexus.ebics.EbicsUploadSpec
import tech.libeufin.nexus.ebics.doEbicsUploadTransaction
import tech.libeufin.nexus.ebics.getEbicsSubscriberDetails
+import tech.libeufin.nexus.getBankAccount
+import tech.libeufin.nexus.getBankConnection
import tech.libeufin.nexus.getConnectionPlugin
import tech.libeufin.nexus.getNexusUser
import tech.libeufin.nexus.server.*
@@ -92,7 +93,7 @@ private fun uploadPain001Payment(
subject = subject,
endToEndId = "Zufall"
),
- getBankAccount("foo")
+ getBankAccount("foo").bankAccountName
)
}
val ebicsConn = getConnectionPlugin("ebics")
diff --git a/nexus/src/test/kotlin/SandboxAccessApiTest.kt b/nexus/src/test/kotlin/SandboxAccessApiTest.kt
index ac64d327..4236e0ce 100644
--- a/nexus/src/test/kotlin/SandboxAccessApiTest.kt
+++ b/nexus/src/test/kotlin/SandboxAccessApiTest.kt
@@ -13,7 +13,6 @@ import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.transactions.transaction
import org.junit.Ignore
import org.junit.Test
-import tech.libeufin.nexus.bankaccount.getBankAccount
import tech.libeufin.sandbox.*
import tech.libeufin.util.getDatabaseName
import java.util.*
diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Database.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Database.kt
index 5a978180..9842c7ce 100644
--- a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Database.kt
+++ b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Database.kt
@@ -6,6 +6,7 @@ import tech.libeufin.util.internalServerError
import java.sql.DriverManager
import java.sql.PreparedStatement
import java.sql.ResultSet
+import java.util.*
private const val DB_CTR_LIMIT = 1000000
@@ -56,6 +57,17 @@ data class BankAccountTransaction(
val bankAccountId: Long,
)
+data class TalerWithdrawalOperation(
+ val withdrawalId: UUID,
+ val amount: TalerAmount,
+ val selectionDone: Boolean = false,
+ val aborted: Boolean = false,
+ val confirmationDone: Boolean = false,
+ val reservePub: ByteArray?,
+ val selectedExchangePayto: String?,
+ val walletBankAccount: Long
+)
+
class Database(private val dbConfig: String) {
private var dbConn: PgConnection? = null
private var dbCtr: Int = 0
@@ -73,6 +85,7 @@ class Database(private val dbConfig: String) {
preparedStatements.clear()
dbConn = DriverManager.getConnection(dbConfig).unwrap(PgConnection::class.java)
dbCtr = 0
+ dbConn?.execSQLUpdate("SET search_path TO libeufin_bank;")
}
private fun prepare(sql: String): PreparedStatement {
@@ -88,22 +101,19 @@ class Database(private val dbConfig: String) {
// CONFIG
fun configGet(configKey: String): String? {
reconnect()
- val stmt = prepare("""
- SELECT value FROM configuration WHERE key=?;
- """.trimIndent())
+ val stmt = prepare("SELECT config_value FROM configuration WHERE config_key=?;")
stmt.setString(1, configKey)
- if (!stmt.execute()) return null
- stmt.use {
- return stmt.resultSet.getString("value")
+ val rs = stmt.executeQuery()
+ rs.use {
+ if(!it.next()) return null
+ return it.getString("config_value")
}
}
fun configSet(configKey: String, configValue: String) {
reconnect()
- val stmt = prepare("""
- UPDATE configuration SET value=? WHERE key=?
- """.trimIndent())
- stmt.setString(1, configValue)
- stmt.setString(2, configKey)
+ val stmt = prepare("CALL bank_set_config(TEXT(?), TEXT(?))")
+ stmt.setString(1, configKey)
+ stmt.setString(2, configValue)
stmt.execute()
}
@@ -111,8 +121,17 @@ class Database(private val dbConfig: String) {
fun customerCreate(customer: Customer) {
reconnect()
val stmt = prepare("""
- INSERT INTO customers VALUES (?, ?, ?, ?, ?, ?, ?)
- """.trimIndent()
+ INSERT INTO customers (
+ login
+ ,password_hash
+ ,name
+ ,email
+ ,phone
+ ,cashout_payto
+ ,cashout_currency
+ )
+ VALUES (?, ?, ?, ?, ?, ?, ?)
+ """
)
stmt.setString(1, customer.login)
stmt.setString(2, customer.passwordHash)
@@ -123,40 +142,61 @@ class Database(private val dbConfig: String) {
stmt.setString(7, customer.cashoutCurrency)
stmt.execute()
}
- fun customerGetFromLogin(login: String, cb: (ResultSet)->Unit) {
+ fun customerGetFromLogin(login: String): Customer? {
reconnect()
val stmt = prepare("""
- SELECT * FROM customers WHERE login=?
- """.trimIndent())
+ SELECT
+ password_hash,
+ name,
+ email,
+ phone,
+ cashout_payto,
+ cashout_currency
+ FROM customers
+ WHERE login=?
+ """)
stmt.setString(1, login)
- if (!stmt.execute()) return
- stmt.use { // why .use{} and not directly access .resultSet?
- cb(stmt.resultSet)
+ val rs = stmt.executeQuery()
+ rs.use {
+ if (!rs.next()) return null
+ return Customer(
+ login = login,
+ passwordHash = it.getString("password_hash"),
+ name = it.getString("name"),
+ phone = it.getString("phone"),
+ email = it.getString("email"),
+ cashoutCurrency = it.getString("cashout_currency"),
+ cashoutPayto = it.getString("cashout_payto")
+ )
}
}
// Possibly more "customerGetFrom*()" to come.
// BANK ACCOUNTS
- fun bankAccountCreate(bankAccount: BankAccount) {
+
+ /*
+ // Returns false on conflicts.
+ fun bankAccountCreate(bankAccount: BankAccount): Boolean {
reconnect()
val stmt = prepare("""
- INSERT INTO bank_accounts VALUES (?, ?, ?, ?, ?, ?, ?)
- """.trimIndent())
+ INSERT INTO bank_accounts (col, col, ..) VALUES (?, ?, ?, ?, ?, ?, ?)
+ """)
stmt.setString(1, bankAccount.iban)
- stmt.setString(1, bankAccount.bic)
- stmt.setString(1, bankAccount.bankAccountLabel)
- stmt.setLong(1, bankAccount.owningCustomerId)
- stmt.setLong(1, bankAccount.lastNexusFetchRowId)
- // Following might fail and need a "?::taler_amount" on the last parameter.
- // See: https://stackoverflow.com/questions/10571821/inserting-into-custom-sql-types-with-prepared-statements-in-java
- stmt.setObject(1, bankAccount.balance)
- stmt.execute()
+ stmt.setString(2, bankAccount.bic)
+ stmt.setString(3, bankAccount.bankAccountLabel)
+ stmt.setLong(4, bankAccount.owningCustomerId)
+ stmt.setLong(5, bankAccount.lastNexusFetchRowId)
+ // using the default zero value for the balance.
+ val ret = stmt.execute()
+ // FIXME: investigate the failure cause: DBMS vs Unique constraint violation.
+ // FIXME: need test case to trigger such violation.
}
- fun bankAccountGetFromLabel(bankAccountLabel: String, cb: (ResultSet) -> Unit) {
+
+ fun bankAccountGetFromLabel(bankAccountLabel: String): BankAccount {
reconnect()
val stmt = prepare("""
SELECT * FROM bank_accounts WHERE bank_account_label=?
- """.trimIndent())
+ """)
stmt.setString(1, bankAccountLabel)
if (!stmt.execute()) return
stmt.use { // why .use{} and not directly access .resultSet?
@@ -166,26 +206,34 @@ class Database(private val dbConfig: String) {
// More bankAccountGetFrom*() to come, on a needed basis.
// BANK ACCOUNT TRANSACTIONS
- fun bankTransactionCreate(tx: BankAccountTransaction) {
+ enum class BankTransactionResult {
+ NO_CREDITOR,
+ NO_DEBTOR,
+ SUCCESS,
+ CONFLICT
+ }
+ fun bankTransactionCreate(
+ // tx: BankInternalTransaction
+ creditTx: BankAccountTransaction,
+ debitTx: BankAccountTransaction
+ ): BankTransactionResult {
reconnect()
val stmt = prepare("""
- INSERT INTO bank_account_transactions VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
- """.trimIndent())
- stmt.setString(1, tx.creditorIban)
- stmt.setString(2, tx.creditorBic)
- stmt.setString(3, tx.creditorName)
- stmt.setString(4, tx.debtorIban)
- stmt.setString(5, tx.debtorBic)
- stmt.setString(6, tx.debtorName)
+ SELECT out_nx_creditor, out_nx_debitor, out_balance_insufficient
+ FROM bank_wire_transfer(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
+ """ // FIXME: adjust balances.
+ )
+ // FIXME: implement this operation with a stored procedure.
+ // Credit side
+ stmt.setString(1, tx.creditorAccountId)
+ stmt.setString(1, tx.debitorAccountId)
stmt.setString(7, tx.subject)
stmt.setObject(8, tx.amount)
stmt.setLong(9, tx.transactionDate)
stmt.setString(10, tx.accountServicerReference)
stmt.setString(11, tx.paymentInformationId)
stmt.setString(12, tx.endToEndId)
- stmt.setBoolean(13, tx.isPending)
- stmt.setObject(14, tx.direction)
- stmt.setLong(15, tx.bankAccountId)
+
stmt.execute()
}
@@ -202,7 +250,7 @@ class Database(private val dbConfig: String) {
bankAccountTransactionId < ?
AND bank_account_id=?
AND transaction_date BETWEEN ? AND ?
- """.trimIndent())
+ """)
stmt.setLong(1, upperBound)
stmt.setLong(2, bankAccountId)
stmt.setLong(3, fromMs)
@@ -212,7 +260,52 @@ class Database(private val dbConfig: String) {
cb(stmt.resultSet)
}
}
- // NOTE: to run BFH, EBICS and cash-out tables can be postponed.
+ // WITHDRAWALS
+ fun talerWithdrawalCreate(opUUID: UUID, walletBankAccount: Long) {
+ reconnect()
+ val stmt = prepare("""
+ INSERT INTO taler_withdrawals_operations (withdrawal_id, wallet_bank_account)
+ VALUES (?,?)
+ """) // Take all defaults from the SQL.
+ stmt.setObject(1, opUUID)
+ stmt.setObject(2, walletBankAccount)
+ stmt.execute()
+ }
+
+ // Values coming from the wallet.
+ fun talerWithdrawalSetDetails(
+ opUUID: UUID,
+ exchangePayto: String,
+ reservePub: ByteArray
+ ) {
+ reconnect()
+ val stmt = prepare("""
+ UPDATE taler_withdrawal_operations
+ SET selected_exchange_payto = ?, reserve_pub = ?, selection_done = true
+ WHERE withdrawal_id=?
+ """
+ )
+ stmt.setString(1, exchangePayto)
+ stmt.setBytes(2, reservePub)
+ stmt.setObject(3, opUUID)
+ stmt.execute()
+ }
+
+ fun talerWithdrawalConfirm(opUUID: UUID) {
+ reconnect()
+ val stmt = prepare("""
+ UPDATE taler_withdrawal_operations
+ SET confirmation_done = true
+ WHERE withdrawal_id=?
+ """
+ )
+ stmt.setObject(1, opUUID)
+ stmt.execute()
+ }
+
+
+ // NOTE: to run BFH, EBICS and cash-out tables can be postponed.
+*/
}
diff --git a/sandbox/src/test/kotlin/DatabaseTest.kt b/sandbox/src/test/kotlin/DatabaseTest.kt
index 5223cdf6..77a85312 100644
--- a/sandbox/src/test/kotlin/DatabaseTest.kt
+++ b/sandbox/src/test/kotlin/DatabaseTest.kt
@@ -1,2 +1,26 @@
+import org.junit.Test
+import tech.libeufin.sandbox.Database
+import tech.libeufin.util.execCommand
+
class DatabaseTest {
+ fun initDb() {
+ execCommand(
+ listOf(
+ "libeufin-bank-dbinit",
+ "-d",
+ "libeufincheck",
+ "-r"
+ ),
+ throwIfFails = true
+ )
+ }
+ @Test
+ fun configTest() {
+ initDb()
+ val db = Database("jdbc:postgresql:///libeufincheck")
+ assert(db.configGet("bar") == null)
+ assert(db.configGet("bar") == null)
+ db.configSet("foo", "bar")
+ assert(db.configGet("foo") == "bar")
+ }
} \ No newline at end of file