commit 58f315885efa6f7835074ef1ab154ed39e07ea82 parent 2d72b425d385eb0ee71a10716fba2a42ef4789d9 Author: Christian Grothoff <grothoff@gnunet.org> Date: Sun, 25 Jan 2026 20:14:25 +0900 add logic to force re-checking the KYC status if/when applications inquire about the KYC status Diffstat:
19 files changed, 657 insertions(+), 114 deletions(-)
diff --git a/src/backend/taler-merchant-kyccheck.c b/src/backend/taler-merchant-kyccheck.c @@ -342,6 +342,13 @@ static struct GNUNET_DB_EventHandler *eh_keys; static struct GNUNET_DB_EventHandler *eh_rule; /** + * Event handler to learn that higher-frequency KYC + * checks were forced by an application actively inspecting + * some KYC status values. + */ +static struct GNUNET_DB_EventHandler *eh_update_forced; + +/** * Event handler to learn that we got new /keys * from an exchange and should reconsider eligibility. */ @@ -646,6 +653,8 @@ exchange_check_cb ( &i->a->h_wire, i->e->keys->exchange_url, i->last_kyc_check, + i->due, + i->backoff, i->last_http_status, i->last_ec, i->rule_gen, @@ -663,7 +672,7 @@ exchange_check_cb ( return; } GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "account_set_kyc_status (%s, %u, %s, %s) returned %d\n", + "account_kyc_set_status (%s, %u, %s, %s) returned %d\n", i->e->keys->exchange_url, i->last_http_status, i->auth_ok ? "auth OK" : "auth needed", @@ -820,6 +829,8 @@ start_inquiry (struct Exchange *e, &i->last_ec, &i->rule_gen, &i->last_kyc_check, + &i->due, + &i->backoff, &i->aml_review, &i->jlimits); if (qs < 0) @@ -831,54 +842,8 @@ start_inquiry (struct Exchange *e, } if (qs > 0) i->not_first_time = true; - switch (i->last_http_status) - { - case MHD_HTTP_OK: - /* KYC is OK, but we could have missed some triggers, - so let's check, but slowly within the next minute - so that we do not overdo it if this process happens - to be restarted a lot. */ - if (GNUNET_YES != test_mode) - { - i->due = GNUNET_TIME_relative_to_absolute ( - GNUNET_TIME_randomize (GNUNET_TIME_UNIT_MINUTES)); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Previous KYC status is OK, randomizing inquiry to start at %s\n", - GNUNET_TIME_absolute2s (i->due)); - } - break; - case MHD_HTTP_ACCEPTED: - /* KYC required, due immediately */ - break; - case MHD_HTTP_NO_CONTENT: - /* KYC is OFF, only check again if triggered */ - if (GNUNET_YES != test_mode) - { - i->due = GNUNET_TIME_relative_to_absolute ( - GNUNET_TIME_randomize (aml_low_freq)); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "KYC was disabled, randomizing inquiry to start at %s\n", - GNUNET_TIME_absolute2s (i->due)); - } - break; - case MHD_HTTP_FORBIDDEN: /* bad signature */ - case MHD_HTTP_NOT_FOUND: /* account unknown */ - case MHD_HTTP_CONFLICT: /* no account_pub known */ - /* go immediately into long-polling */ - break; - default: - /* start with decent back-off after hard failure */ - if (GNUNET_YES != test_mode) - { - i->backoff - = GNUNET_TIME_randomize (GNUNET_TIME_UNIT_MINUTES); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Last KYC check failed, starting with backoff %s\n", - GNUNET_TIME_relative2s (i->backoff, - true)); - } - break; - } + if (GNUNET_YES == test_mode) + i->due = GNUNET_TIME_UNIT_ZERO_ABS; /* immediately */ inquiry_work (i); } @@ -941,6 +906,54 @@ stop_inquiry_at (struct Exchange *e, /** + * Set the account @a h_wire of @a instance_id to be ineligible + * for the exchange at @a exchange_url and thus no need to do KYC checks. + * + * @param instance_id instance that has the account + * @param exchange_url base URL of the exchange + * @param h_wire hash of the merchant bank account that is ineligible + */ +static void +flag_ineligible (const char *instance_id, + const char *exchange_url, + const struct TALER_MerchantWireHashP *h_wire) +{ + enum GNUNET_DB_QueryStatus qs; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Account %s not eligible at exchange %s\n", + TALER_B2S (h_wire), + exchange_url); + qs = db_plugin->account_kyc_set_status ( + db_plugin->cls, + instance_id, + h_wire, + exchange_url, + GNUNET_TIME_timestamp_get (), + GNUNET_TIME_UNIT_FOREVER_ABS, + GNUNET_TIME_UNIT_FOREVER_REL, + 0, + TALER_EC_MERCHANT_PRIVATE_ACCOUNT_NOT_ELIGIBLE_FOR_EXCHANGE, + 0, + NULL, + NULL, + false, + false); + if (qs < 0) + { + GNUNET_break (0); + global_ret = EXIT_FAILURE; + GNUNET_SCHEDULER_shutdown (); + return; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "account_kyc_set_status (%s) returned %d\n", + exchange_url, + (int) qs); +} + + +/** * Start inquries for all exchanges on account @a a. * * @param a an account @@ -960,36 +973,9 @@ start_inquiries (struct Account *a) } else { - enum GNUNET_DB_QueryStatus qs; - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Account %s not eligible at exchange %s\n", - a->merchant_account_uri.full_payto, - e->keys->exchange_url); - qs = db_plugin->account_kyc_set_status ( - db_plugin->cls, - a->instance_id, - &a->h_wire, - e->keys->exchange_url, - GNUNET_TIME_timestamp_get (), - 0, - TALER_EC_MERCHANT_PRIVATE_ACCOUNT_NOT_ELIGIBLE_FOR_EXCHANGE, - 0, - NULL, - NULL, - false, - false); - if (qs < 0) - { - GNUNET_break (0); - global_ret = EXIT_FAILURE; - GNUNET_SCHEDULER_shutdown (); - return; - } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "account_set_kyc_status (%s) returned %d\n", - e->keys->exchange_url, - (int) qs); + flag_ineligible (a->instance_id, + e->keys->exchange_url, + &a->h_wire); } } } @@ -1465,6 +1451,11 @@ shutdown_task (void *cls) db_plugin->event_listen_cancel (eh_rule); eh_rule = NULL; } + if (NULL != eh_update_forced) + { + db_plugin->event_listen_cancel (eh_update_forced); + eh_update_forced = NULL; + } if (NULL != keys_rule) { db_plugin->event_listen_cancel (keys_rule); @@ -1487,6 +1478,100 @@ shutdown_task (void *cls) /** + * Function called when we urgently need to re-check the KYC status + * of some account. Finds the respective inquiry and re-launches + * the check, unless we are already doing it. + * + * @param cls NULL + * @param instance_id instance for which to force the check + * @param exchange_url base URL of the exchange to check + * @param h_wire hash of the wire account to check KYC status for + */ +static void +force_check_now (void *cls, + const char *instance_id, + const char *exchange_url, + const struct TALER_MerchantWireHashP *h_wire) +{ + for (struct Account *a = a_head; + NULL != a; + a = a->next) + { + if (0 != + strcmp (instance_id, + a->instance_id)) + continue; + if (0 != + GNUNET_memcmp (h_wire, + &a->h_wire)) + continue; + for (struct Inquiry *i = a->i_head; + NULL != i; + i = i->next) + { + if (0 != + strcmp (i->e->keys->exchange_url, + exchange_url)) + continue; + /* If we are not actively checking with the exchange, do start + to check immediately */ + if (NULL != i->kyc) + { + i->due = GNUNET_TIME_absolute_get (); /* now! */ + if (NULL != i->task) + GNUNET_SCHEDULER_cancel (i->task); + i->task = GNUNET_SCHEDULER_add_at (i->due, + &inquiry_work, + i); + } + return; + } + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "No inquiry at `%s' for exchange `%s' and h_wire `%s'. Likely the account is not eligible.\n", + instance_id, + exchange_url, + TALER_B2S (h_wire)); + /* In this case, set the due date back to FOREVER */ + flag_ineligible (instance_id, + exchange_url, + h_wire); +} + + +/** + * Function called when a KYC status update was forced by an + * application checking the KYC status of an account. + * + * @param cls closure (NULL) + * @param extra additional event data provided + * @param extra_size number of bytes in @a extra + */ +static void +update_forced (void *cls, + const void *extra, + size_t extra_size) +{ + enum GNUNET_DB_QueryStatus qs; + + (void) cls; + (void) extra; + (void) extra_size; + qs = db_plugin->account_kyc_get_outdated ( + db_plugin->cls, + &force_check_now, + NULL); + if (qs < 0) + { + GNUNET_break (0); + global_ret = EXIT_FAILURE; + GNUNET_SCHEDULER_shutdown (); + return; + } +} + + +/** * First task. * * @param cls closure, NULL @@ -1585,6 +1670,19 @@ run (void *cls, { struct GNUNET_DB_EventHeaderP es = { .size = htons (sizeof (es)), + .type = htons (TALER_DBEVENT_MERCHANT_EXCHANGE_KYC_UPDATE_FORCED) + }; + + eh_update_forced + = db_plugin->event_listen (db_plugin->cls, + &es, + GNUNET_TIME_UNIT_FOREVER_REL, + &update_forced, + NULL); + } + { + struct GNUNET_DB_EventHeaderP es = { + .size = htons (sizeof (es)), .type = htons (TALER_DBEVENT_MERCHANT_EXCHANGE_KYC_RULE_TRIGGERED) }; @@ -1598,6 +1696,7 @@ run (void *cls, GNUNET_CONFIGURATION_iterate_sections (cfg, &accept_exchanges, NULL); + fprintf (stderr, "NOW\n"); { struct GNUNET_DB_EventHeaderP es = { .size = htons (sizeof (es)), diff --git a/src/backenddb/Makefile.am b/src/backenddb/Makefile.am @@ -45,6 +45,7 @@ sql_DATA = \ merchant-0026.sql \ merchant-0027.sql \ merchant-0028.sql \ + merchant-0029.sql \ drop.sql BUILT_SOURCES = \ @@ -89,6 +90,7 @@ libtalermerchantdb_la_LDFLAGS = \ -no-undefined libtaler_plugin_merchantdb_postgres_la_SOURCES = \ + pg_account_kyc_get_outdated.h pg_account_kyc_get_outdated.c \ pg_account_kyc_get_status.h pg_account_kyc_get_status.c \ pg_account_kyc_set_failed.h pg_account_kyc_set_failed.c \ pg_account_kyc_set_status.h pg_account_kyc_set_status.c \ diff --git a/src/backenddb/merchant-0029.sql b/src/backenddb/merchant-0029.sql @@ -0,0 +1,39 @@ +-- +-- 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 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 General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + +-- @file merchant-0029.sql +-- @brief Add field for long-poll signalling to taler-merchant-kyccheck +-- @author Christian Grothoff + +BEGIN; + +-- Check patch versioning is in place. +SELECT _v.register_patch('merchant-0029', NULL, NULL); + +SET search_path TO merchant; + +ALTER TABLE merchant_kyc + ADD COLUMN next_kyc_poll INT8 NOT NULL DEFAULT 0, + ADD COLUMN kyc_backoff INT8 NOT NULL DEFAULT 0; +COMMENT ON COLUMN merchant_kyc.next_kyc_poll + IS 'When should we next do a KYC poll on this exchange and bank account'; +COMMENT ON COLUMN merchant_kyc.kyc_backoff + IS 'What is the current backoff value between KYC polls'; + +CREATE INDEX merchant_kyc_by_next_kyc_poll + ON merchant_kyc + (next_kyc_poll ASC); + +COMMIT; diff --git a/src/backenddb/pg_account_kyc_get_outdated.c b/src/backenddb/pg_account_kyc_get_outdated.c @@ -0,0 +1,153 @@ +/* + 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 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 General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +/** + * @file backenddb/pg_account_kyc_get_outdated.c + * @brief Implementation of the account_kyc_get_outdated function for Postgres + * @author Christian Grothoff + */ +#include "platform.h" +#include <taler/taler_error_codes.h> +#include <taler/taler_dbevents.h> +#include <taler/taler_pq_lib.h> +#include "pg_account_kyc_get_outdated.h" +#include "pg_helper.h" + +/** + * Closure for kyc_status_cb(). + */ +struct KycStatusContext +{ + /** + * Function to call with results. + */ + TALER_MERCHANTDB_KycOutdatedCallback kyc_cb; + + /** + * Closure for @e kyc_cb. + */ + void *kyc_cb_cls; + + /** + * Number of results found. + */ + unsigned int count; + + /** + * Set to true on failure(s). + */ + bool failure; +}; + + +/** + * Function to be called with the results of a SELECT statement + * that has returned @a num_results results about accounts. + * + * @param[in,out] cls of type `struct KycStatusContext *` + * @param result the postgres result + * @param num_results the number of results in @a result + */ +static void +kyc_status_cb (void *cls, + PGresult *result, + unsigned int num_results) +{ + struct KycStatusContext *ksc = cls; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Got %u outdated KYC records\n", + num_results); + for (unsigned int i = 0; i < num_results; i++) + { + struct TALER_MerchantWireHashP h_wire; + char *exchange_url; + char *instance_id; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_auto_from_type ("h_wire", + &h_wire), + GNUNET_PQ_result_spec_string ("exchange_url", + &exchange_url), + GNUNET_PQ_result_spec_string ("instance_id", + &instance_id), + GNUNET_PQ_result_spec_end + }; + + if (GNUNET_OK != + GNUNET_PQ_extract_result (result, + rs, + i)) + { + GNUNET_break (0); + ksc->failure = true; + return; + } + ksc->kyc_cb (ksc->kyc_cb_cls, + instance_id, + exchange_url, + &h_wire); + GNUNET_PQ_cleanup_result (rs); + } +} + + +enum GNUNET_DB_QueryStatus +TMH_PG_account_kyc_get_outdated ( + void *cls, + TALER_MERCHANTDB_KycOutdatedCallback kyc_cb, + void *kyc_cb_cls) +{ + struct PostgresClosure *pg = cls; + struct KycStatusContext ksc = { + .kyc_cb = kyc_cb, + .kyc_cb_cls = kyc_cb_cls + }; + struct GNUNET_TIME_Absolute now + = GNUNET_TIME_absolute_get (); + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_absolute_time (&now), + GNUNET_PQ_query_param_end + }; + enum GNUNET_DB_QueryStatus qs; + + check_connection (pg); + PREPARE (pg, + "account_kyc_get_outdated", + "SELECT " + " mi.merchant_id" + " ,ma.h_wire" + " ,kyc.exchange_url" + " FROM merchant_kyc kyc" + " JOIN merchant_accounts ma" + " USING (account_serial)" + " JOIN merchant_instances mi" + " USING (merchant_serial)" + " WHERE kyc.next_kyc_poll < $1" + " ORDER BY kyc.next_kyc_poll ASC;"); + qs = GNUNET_PQ_eval_prepared_multi_select ( + pg->conn, + "account_kyc_get_outdated", + params, + &kyc_status_cb, + &ksc); + if (ksc.failure) + { + GNUNET_break (0); + return GNUNET_DB_STATUS_HARD_ERROR; + } + if (0 > qs) + return qs; + return ksc.count; +} diff --git a/src/backenddb/pg_account_kyc_get_outdated.h b/src/backenddb/pg_account_kyc_get_outdated.h @@ -0,0 +1,42 @@ +/* + This file is part of TALER + Copyright (C) 2025 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU 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 General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +/** + * @file backenddb/pg_account_kyc_get_outdated.h + * @brief implementation of the account_kyc_get_outdated function for Postgres + * @author Christian Grothoff + */ +#ifndef PG_ACCOUNT_KYC_GET_OUTDATED_H +#define PG_ACCOUNT_KYC_GET_OUTDATED_H + +#include <taler/taler_util.h> +#include <taler/taler_json_lib.h> +#include "taler_merchantdb_plugin.h" + +/** + * Find accounts requiring KYC checks. + * + * @param cls closure + * @param kyc_cb status callback to invoke + * @param kyc_cb_cls closure for @a kyc_cb + * @return database result code + */ +enum GNUNET_DB_QueryStatus +TMH_PG_account_kyc_get_outdated ( + void *cls, + TALER_MERCHANTDB_KycOutdatedCallback kyc_cb, + void *kyc_cb_cls); + +#endif diff --git a/src/backenddb/pg_account_kyc_get_status.c b/src/backenddb/pg_account_kyc_get_status.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2022 Taler Systems SA + Copyright (C) 2022, 2026 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -170,8 +170,11 @@ TMH_PG_account_kyc_get_status ( .kyc_cb = kyc_cb, .kyc_cb_cls = kyc_cb_cls }; + struct GNUNET_TIME_Absolute now + = GNUNET_TIME_absolute_get (); struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_string (merchant_id), + GNUNET_PQ_query_param_absolute_time (&now), NULL == exchange_url ? GNUNET_PQ_query_param_null () : GNUNET_PQ_query_param_string (exchange_url), @@ -185,28 +188,18 @@ TMH_PG_account_kyc_get_status ( check_connection (pg); PREPARE (pg, "lookup_kyc_status", - "SELECT" - " ma.h_wire" - ",ma.payto_uri" - ",mk.exchange_url" - ",mk.kyc_timestamp" - ",mk.kyc_ok" - ",mk.access_token" - ",mk.exchange_http_status" - ",mk.exchange_ec_code" - ",mk.aml_review" - ",mk.jaccount_limits::TEXT" - " FROM merchant_instances mi" - " JOIN merchant_accounts ma" - " USING (merchant_serial)" - " LEFT JOIN merchant_kyc mk" - " USING (account_serial)" - " WHERE (mi.merchant_id=$1)" - " AND ma.active" - " AND ( ($2::TEXT IS NULL)" - " OR (mk.exchange_url=$2) )" - " AND ( ($3::BYTEA IS NULL)" - " OR (ma.h_wire=$3) );"); + "SELECT " + " out_h_wire AS h_wire" + " ,out_payto_uri AS payto_uri" + " ,out_exchange_url AS exchange_url" + " ,out_kyc_timestamp AS kyc_timestamp" + " ,out_kyc_ok AS kyc_ok" + " ,out_access_token AS access_token" + " ,out_exchange_http_status AS exchange_http_status" + " ,out_exchange_ec_code AS exchange_ec_code" + " ,out_aml_review AS aml_review" + " ,out_jaccount_limits::TEXT AS jaccount_limits" + " FROM merchant_do_account_kyc_get_status($1, $2, $3, $4);"); qs = GNUNET_PQ_eval_prepared_multi_select ( pg->conn, "lookup_kyc_status", diff --git a/src/backenddb/pg_account_kyc_get_status.h b/src/backenddb/pg_account_kyc_get_status.h @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2022 Taler Systems SA + Copyright (C) 2022, 2026 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -26,7 +26,9 @@ #include "taler_merchantdb_plugin.h" /** - * Check an instance's account's KYC status. + * Check an instance's account's KYC status. Triggers + * a request to taler-merchant-kyccheck to get a + * KYC status update as a side-effect! * * @param cls closure * @param merchant_id merchant backend instance ID diff --git a/src/backenddb/pg_account_kyc_get_status.sql b/src/backenddb/pg_account_kyc_get_status.sql @@ -0,0 +1,122 @@ +-- +-- 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 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 General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +-- + + +DROP FUNCTION IF EXISTS merchant_do_account_kyc_get_status; +CREATE FUNCTION merchant_do_account_kyc_get_status ( + IN in_merchant_id TEXT, + IN in_now INT8, + IN in_exchange_url TEXT, -- can be NULL + IN in_h_wire BYTEA -- can be NULL +) RETURNS TABLE ( + out_h_wire BYTEA, -- never NULL + out_payto_uri TEXT, -- never NULL + out_exchange_url TEXT, + out_kyc_timestamp INT8, + out_kyc_ok BOOLEAN, + out_access_token BYTEA, + out_exchange_http_status INT4, + out_exchange_ec_code INT4, + out_aml_review BOOLEAN, + out_jaccount_limits TEXT +) +LANGUAGE plpgsql +AS $$ +DECLARE + my_merchant_serial INT8; + my_account_serial INT8; + my_h_wire BYTEA; + my_payto_uri TEXT; + my_kyc_record RECORD; + +BEGIN + -- Get the merchant_serial from merchant_instances + SELECT merchant_serial + INTO my_merchant_serial + FROM merchant_instances + WHERE merchant_id = in_merchant_id; + IF NOT FOUND + THEN + RETURN; + END IF; + + -- Iterate over merchant_accounts for this merchant + FOR my_account_serial, my_h_wire, my_payto_uri + IN SELECT account_serial, h_wire, payto_uri + FROM merchant_accounts + WHERE merchant_serial = my_merchant_serial + AND active + AND (in_h_wire IS NULL OR h_wire = in_h_wire) + LOOP + + -- Fetch KYC info for this account (can have multiple results) + FOR my_kyc_record IN + SELECT + mk.kyc_serial_id + ,mk.exchange_url + ,mk.kyc_timestamp + ,mk.kyc_ok + ,mk.access_token + ,mk.exchange_http_status + ,mk.exchange_ec_code + ,mk.aml_review + ,mk.jaccount_limits::TEXT + FROM merchant_kyc mk + WHERE mk.account_serial = my_account_serial + AND (in_exchange_url IS NULL OR mk.exchange_url = in_exchange_url) + LOOP + -- Ask taler-merchant-kyccheck to get us an update on the status ASAP + UPDATE merchant_kyc + SET next_kyc_poll=in_now + WHERE kyc_serial_id = my_kyc_record.kyc_serial_id; + NOTIFY XDQM4Z4N0D3GX0H9JEXH70EBC2T3KY7HC0TJB0Z60D2H781RXR6AG; -- MERCHANT_EXCHANGE_KYC_UPDATE_FORCED + RETURN QUERY + SELECT + my_h_wire, + my_payto_uri, + my_kyc_record.exchange_url, + my_kyc_record.kyc_timestamp, + my_kyc_record.kyc_ok, + my_kyc_record.access_token, + my_kyc_record.exchange_http_status, + my_kyc_record.exchange_ec_code, + my_kyc_record.aml_review, + my_kyc_record.jaccount_limits::TEXT; + END LOOP; -- loop over exchanges with KYC status for the given account + + IF NOT FOUND + THEN + -- Still return to server that we do NOT know anything + -- for the given exchange yet (but that the bank account exists) + RETURN QUERY + SELECT + my_h_wire, + my_payto_uri, + NULL::TEXT, + NULL::INT8, + NULL::BOOLEAN, + NULL::BYTEA, + NULL::INT4, + NULL::INT4, + NULL::BOOLEAN, + NULL::TEXT; + END IF; + + END LOOP; -- loop over merchant_accounts + +END $$; +COMMENT ON FUNCTION merchant_do_account_kyc_get_status + IS 'Returns the KYC status of selected exchanges and accounts, but ALSO resets the next_kyc_check time for all returned data points to the current time (in_now argument)'; diff --git a/src/backenddb/pg_account_kyc_set_status.c b/src/backenddb/pg_account_kyc_set_status.c @@ -34,6 +34,8 @@ TMH_PG_account_kyc_set_status ( const struct TALER_MerchantWireHashP *h_wire, const char *exchange_url, struct GNUNET_TIME_Timestamp timestamp, + struct GNUNET_TIME_Absolute next_time, + struct GNUNET_TIME_Relative kyc_backoff, unsigned int exchange_http_status, enum TALER_ErrorCode exchange_ec_code, uint64_t rule_gen, @@ -76,6 +78,8 @@ TMH_PG_account_kyc_set_status ( GNUNET_PQ_query_param_string (notify_s), GNUNET_PQ_query_param_string (notify2_s), GNUNET_PQ_query_param_uint64 (&rule_gen), + GNUNET_PQ_query_param_absolute_time (&next_time), + GNUNET_PQ_query_param_relative_time (&kyc_backoff), GNUNET_PQ_query_param_end }; bool no_instance; @@ -97,7 +101,7 @@ TMH_PG_account_kyc_set_status ( " ,out_no_account AS no_account" " FROM merchant_do_account_kyc_set_status" "($1, $2, $3, $4, $5, $6, $7, $8::TEXT::JSONB" - ",$9, $10, $11, $12, $13);"); + ",$9, $10, $11, $12, $13, $14, $15);"); qs = GNUNET_PQ_eval_prepared_singleton_select ( pg->conn, "account_kyc_set_status", diff --git a/src/backenddb/pg_account_kyc_set_status.h b/src/backenddb/pg_account_kyc_set_status.h @@ -33,6 +33,8 @@ * @param h_wire hash of the wire account to check * @param exchange_url base URL of the exchange to check * @param timestamp timestamp to store + * @param next_time when should we next check the KYC status + * @param kyc_backoff what is the current backoff between KYC status checks * @param exchange_http_status HTTP status code returned last by the exchange * @param exchange_ec_code Taler error code returned last by the exchange * @param rule_gen generation of the rule in the exchange database @@ -50,6 +52,8 @@ TMH_PG_account_kyc_set_status ( const struct TALER_MerchantWireHashP *h_wire, const char *exchange_url, struct GNUNET_TIME_Timestamp timestamp, + struct GNUNET_TIME_Absolute next_time, + struct GNUNET_TIME_Relative kyc_backoff, unsigned int exchange_http_status, enum TALER_ErrorCode exchange_ec_code, uint64_t rule_gen, diff --git a/src/backenddb/pg_account_kyc_set_status.sql b/src/backenddb/pg_account_kyc_set_status.sql @@ -1,6 +1,6 @@ -- -- This file is part of TALER --- Copyright (C) 2024 Taler Systems SA +-- Copyright (C) 2024, 2026 Taler Systems SA -- -- TALER is free software; you can redistribute it and/or modify it under the -- terms of the GNU General Public License as published by the Free Software @@ -31,6 +31,8 @@ CREATE FUNCTION merchant_do_account_kyc_set_status ( IN in_notify_str TEXT, IN in_notify2_str TEXT, IN in_rule_gen INT8, + IN in_next_time INT8, + IN in_kyc_backoff INT8, OUT out_no_instance BOOL, OUT out_no_account BOOL) LANGUAGE plpgsql @@ -76,6 +78,8 @@ UPDATE merchant_kyc ,exchange_ec_code=in_exchange_ec_code ,access_token=in_access_token ,last_rule_gen=in_rule_gen + ,next_kyc_poll=in_next_time + ,kyc_backoff=in_kyc_backoff WHERE account_serial=my_account_serial AND exchange_url=in_exchange_url; @@ -92,7 +96,9 @@ THEN ,exchange_http_status ,exchange_ec_code ,access_token - ,last_rule_gen) + ,last_rule_gen + ,next_kyc_poll + ,kyc_backoff) VALUES (in_timestamp ,in_kyc_ok @@ -103,7 +109,9 @@ THEN ,in_exchange_http_status ,in_exchange_ec_code ,in_access_token - ,in_rule_gen); + ,in_rule_gen + ,in_next_time + ,in_kyc_backoff); END IF; EXECUTE FORMAT ( diff --git a/src/backenddb/pg_get_kyc_status.c b/src/backenddb/pg_get_kyc_status.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2024 Taler Systems SA + Copyright (C) 2024, 2026 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -39,6 +39,8 @@ TMH_PG_get_kyc_status ( enum TALER_ErrorCode *last_ec, uint64_t *rule_gen, struct GNUNET_TIME_Timestamp *last_kyc_check, + struct GNUNET_TIME_Absolute *next_kyc_poll, + struct GNUNET_TIME_Relative *kyc_backoff, bool *aml_review, json_t **jlimits) { @@ -67,6 +69,10 @@ TMH_PG_get_kyc_status ( kyc_ok), GNUNET_PQ_result_spec_timestamp ("kyc_timestamp", last_kyc_check), + GNUNET_PQ_result_spec_absolute_time ("next_kyc_poll", + next_kyc_poll), + GNUNET_PQ_result_spec_relative_time ("kyc_backoff", + kyc_backoff), GNUNET_PQ_result_spec_bool ("aml_review", aml_review), GNUNET_PQ_result_spec_allow_null ( @@ -87,6 +93,8 @@ TMH_PG_get_kyc_status ( ",mk.kyc_ok" ",mk.last_rule_gen" ",mk.kyc_timestamp" + ",mk.next_kyc_poll" + ",mk.kyc_backoff" ",mk.aml_review" ",mk.jaccount_limits::TEXT" " FROM merchant_kyc mk" diff --git a/src/backenddb/pg_get_kyc_status.h b/src/backenddb/pg_get_kyc_status.h @@ -42,6 +42,8 @@ * known decision of the exchange exists for this record; used * for long-polling for changes to exchange decisions * @param[out] last_kyc_check set to time of last KYC check + * @param[out] next_kyc_poll when should we check the KYC status next + * @param[out] kyc_backoff current back-off frequency for KYC checks * @param[out] aml_review set to true if the account is under AML review (if this exposed) * @param[out] jlimits set to JSON array with AccountLimits, NULL if unknown (and likely defaults apply or KYC auth is urgently needed, see @a auth_ok) * @return database result code @@ -59,6 +61,8 @@ TMH_PG_get_kyc_status ( enum TALER_ErrorCode *last_ec, uint64_t *rule_gen, struct GNUNET_TIME_Timestamp *last_kyc_check, + struct GNUNET_TIME_Absolute *next_kyc_poll, + struct GNUNET_TIME_Relative *kyc_backoff, bool *aml_review, json_t **jlimits); diff --git a/src/backenddb/pg_template.h b/src/backenddb/pg_template.h @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2025 Taler Systems SA + Copyright (C) 2026 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software diff --git a/src/backenddb/plugin_merchantdb_postgres.c b/src/backenddb/plugin_merchantdb_postgres.c @@ -33,6 +33,7 @@ #include "pg_helper.h" #include "pg_gc.h" #include "pg_insert_otp.h" +#include "pg_account_kyc_get_outdated.h" #include "pg_get_kyc_status.h" #include "pg_get_kyc_limits.h" #include "pg_delete_otp.h" @@ -723,6 +724,8 @@ libtaler_plugin_merchantdb_postgres_init (void *cls) = &TMH_PG_lookup_statistics_amount_by_bucket2; plugin->lookup_statistics_counter_by_bucket2 = &TMH_PG_lookup_statistics_counter_by_bucket2; + plugin->account_kyc_get_outdated + = &TMH_PG_account_kyc_get_outdated; plugin->select_report = &TMH_PG_select_report; plugin->insert_product_group diff --git a/src/backenddb/procedures.sql.in b/src/backenddb/procedures.sql.in @@ -33,6 +33,7 @@ SET search_path TO merchant; #include "pg_update_product_group.sql" #include "pg_update_money_pot.sql" #include "pg_increment_money_pots.sql" +#include "pg_account_kyc_get_status.sql" DROP PROCEDURE IF EXISTS merchant_do_gc; CREATE PROCEDURE merchant_do_gc(in_now INT8) diff --git a/src/backenddb/test_merchantdb.c b/src/backenddb/test_merchantdb.c @@ -5641,6 +5641,8 @@ test_kyc (void) &account.h_wire, "https://exchange.net/", now, + GNUNET_TIME_UNIT_FOREVER_ABS, + GNUNET_TIME_UNIT_HOURS, MHD_HTTP_OK, TALER_EC_NONE, 42, @@ -5654,6 +5656,8 @@ test_kyc (void) &account.h_wire, "https://exchange2.com/", now, + GNUNET_TIME_UNIT_FOREVER_ABS, + GNUNET_TIME_UNIT_HOURS, MHD_HTTP_OK, TALER_EC_NONE, 42, @@ -5667,6 +5671,8 @@ test_kyc (void) &account.h_wire, "https://exchange.net/", now, + GNUNET_TIME_UNIT_FOREVER_ABS, + GNUNET_TIME_UNIT_HOURS, MHD_HTTP_OK, TALER_EC_NONE, 42, @@ -7332,7 +7338,8 @@ run (void *cls) if (0 == result)*/ { /* Test dropping tables */ - if (GNUNET_OK != plugin->drop_tables (plugin->cls)) + if (GNUNET_OK != + plugin->drop_tables (plugin->cls)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Dropping tables failed\n"); diff --git a/src/include/taler_merchantdb_plugin.h b/src/include/taler_merchantdb_plugin.h @@ -1101,6 +1101,23 @@ typedef void /** + * Function called from ``account_kyc_get_outdated`` + * with information about outdated KYC status information. + * + * @param cls closure + * @param instance_id which instance is this about + * @param exchange_url base URL of the exchange for which the status is dated + * @param h_wire hash of the wire account for which the status is dated + */ +typedef void +(*TALER_MERCHANTDB_KycOutdatedCallback)( + void *cls, + const char *instance_id, + const char *exchange_url, + const struct TALER_MerchantWireHashP *h_wire); + + +/** * Results from trying to increase a refund. */ enum TALER_MERCHANTDB_RefundStatus @@ -2318,7 +2335,9 @@ struct TALER_MERCHANTDB_Plugin /** - * Check an instance's account's KYC status. + * Check an instance's account's KYC status. Triggers + * a request to taler-merchant-kyccheck to get a + * KYC status update as a side-effect! * * @param cls closure * @param merchant_id merchant backend instance ID @@ -2339,6 +2358,22 @@ struct TALER_MERCHANTDB_Plugin TALER_MERCHANTDB_KycCallback kyc_cb, void *kyc_cb_cls); + + /** + * Find accounts requiring KYC checks. + * + * @param cls closure + * @param kyc_cb status callback to invoke + * @param kyc_cb_cls closure for @a kyc_cb + * @return database result code + */ + enum GNUNET_DB_QueryStatus + (*account_kyc_get_outdated)( + void *cls, + TALER_MERCHANTDB_KycOutdatedCallback kyc_cb, + void *kyc_cb_cls); + + /** * Check an account's KYC status at an exchange. * @@ -2355,6 +2390,8 @@ struct TALER_MERCHANTDB_Plugin * known decision of the exchange exists for this record; used * for long-polling for changes to exchange decisions * @param[out] last_kyc_check set to time of last KYC check + * @param[out] next_kyc_poll when should we check the KYC status next + * @param[out] kyc_backoff current back-off frequency for KYC checks * @param[out] aml_review set to true if the account is under AML review (if this exposed) * @param[out] jlimits set to JSON array with AccountLimits, NULL if unknown (and likely defaults apply or KYC auth is urgently needed, see @a auth_ok) * @return database result code @@ -2372,6 +2409,8 @@ struct TALER_MERCHANTDB_Plugin enum TALER_ErrorCode *last_ec, uint64_t *rule_gen, struct GNUNET_TIME_Timestamp *last_kyc_check, + struct GNUNET_TIME_Absolute *next_kyc_poll, + struct GNUNET_TIME_Relative *kyc_backoff, bool *aml_review, json_t **jlimits); @@ -2407,6 +2446,8 @@ struct TALER_MERCHANTDB_Plugin * @param h_wire hash of the wire account to check * @param exchange_url base URL of the exchange to check * @param timestamp timestamp to store + * @param next_time when should we next check the KYC status + * @param kyc_backoff what is the current backoff between KYC status checks * @param exchange_http_status HTTP status code returned last by the exchange * @param exchange_ec_code Taler error code returned last by the exchange * @param rule_gen generation of the rule in the exchange database @@ -2424,6 +2465,8 @@ struct TALER_MERCHANTDB_Plugin const struct TALER_MerchantWireHashP *h_wire, const char *exchange_url, struct GNUNET_TIME_Timestamp timestamp, + struct GNUNET_TIME_Absolute next_time, + struct GNUNET_TIME_Relative kyc_backoff, unsigned int exchange_http_status, enum TALER_ErrorCode exchange_ec_code, uint64_t rule_gen, diff --git a/src/testing/test_merchant_api.c b/src/testing/test_merchant_api.c @@ -327,6 +327,15 @@ run (void *cls, merchant_url, "admin", MHD_HTTP_NO_CONTENT), + TALER_TESTING_cmd_merchant_kyc_get ( + "instance-create-kyc-no-accounts", + merchant_url, + NULL, + NULL, + EXCHANGE_URL, + TALER_EXCHANGE_KLPT_NONE, + MHD_HTTP_NO_CONTENT, + false), TALER_TESTING_cmd_merchant_post_account ( "instance-create-default-account", merchant_url, @@ -340,8 +349,8 @@ run (void *cls, NULL, EXCHANGE_URL, TALER_EXCHANGE_KLPT_NONE, - MHD_HTTP_NO_CONTENT, - false), + MHD_HTTP_OK, + true), TALER_TESTING_cmd_merchant_post_orders_no_claim ( "create-proposal-bad-currency", merchant_url,