commit b880b232b68fc8c6aa992ad94b5a672ff8354e3c
parent 1e7931392a5abd759afee2a18065ccbcdbf6176f
Author: Florian Dold <dold@taler.net>
Date: Tue, 1 Sep 2026 14:48:41 +0200
KYC logic: trigger rules only above their thresholds
Treat configured thresholds as inclusive maximum amounts and add focused boundary coverage.\n\nFix #11416.
Diffstat:
4 files changed, 221 insertions(+), 5 deletions(-)
diff --git a/src/include/taler/taler_kyclogic_lib.h b/src/include/taler/taler_kyclogic_lib.h
@@ -274,7 +274,9 @@ TALER_KYCLOGIC_rules_free (struct TALER_KYCLOGIC_LegitimizationRuleSet *lrs);
/**
* Check if KYC is provided for a particular operation. Returns the set of
- * checks that still need to be satisfied.
+ * checks that still need to be satisfied. Rule thresholds are inclusive
+ * limits: a total less than or equal to the threshold is permitted, and a
+ * rule is triggered only when the total exceeds its threshold.
*
* Called within a database transaction, so must
* not start a new one.
diff --git a/src/kyclogic/kyclogic_api.c b/src/kyclogic/kyclogic_api.c
@@ -3944,14 +3944,21 @@ check_amount (
i);
continue; /* out of time range for rule */
}
- if (-1 == TALER_amount_cmp (&ktc->sum,
- &rule->threshold))
+ /*
+ * A KYC threshold is the inclusive maximum permitted by the rule:
+ * clients may transact up to and including this amount. The rule is
+ * triggered only once the accumulated total exceeds the threshold.
+ * Keeping equality on the permitted side is also important because
+ * exposed AccountLimit values are advertised as maximum contributions.
+ */
+ if (0 >= TALER_amount_cmp (&ktc->sum,
+ &rule->threshold))
{
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Below threshold of %s for rule %u\n",
+ "At or below threshold of %s for rule %u\n",
TALER_amount2s (&rule->threshold),
i);
- continue; /* sum < threshold */
+ continue; /* sum <= threshold */
}
if ( (NULL != ktc->triggered_rule) &&
(1 == TALER_amount_cmp (&ktc->triggered_rule->threshold,
diff --git a/src/kyclogic/meson.build b/src/kyclogic/meson.build
@@ -74,6 +74,25 @@ pkg.generate(
)
+test_kyclogic = executable(
+ 'test_kyclogic',
+ ['test_kyclogic.c'],
+ dependencies: [
+ libtalerkyclogic_dep,
+ libtalerutil_dep,
+ gnunetutil_dep,
+ ],
+ include_directories: [incdir, configuration_inc],
+ build_by_default: false,
+ install: false,
+)
+test(
+ 'test_kyclogic',
+ test_kyclogic,
+ suite: ['kyclogic'],
+)
+
+
executable(
'taler-exchange-helper-sanctions-dummy',
diff --git a/src/kyclogic/test_kyclogic.c b/src/kyclogic/test_kyclogic.c
@@ -0,0 +1,188 @@
+/*
+ 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 kyclogic/test_kyclogic.c
+ * @brief Tests for KYC rule evaluation
+ * @author Florian Dold
+ */
+#include "platform.h"
+#include "taler/taler_kyclogic_lib.h"
+#include "taler/taler_util.h"
+
+
+/**
+ * Amounts to return from the test iterator.
+ */
+struct TestAmounts
+{
+ /**
+ * Amounts, in reverse chronological order.
+ */
+ const char *const *amounts;
+
+ /**
+ * Number of entries in @e amounts.
+ */
+ unsigned int num_amounts;
+};
+
+
+/**
+ * Return the amounts for one boundary test.
+ *
+ * @param cls a `struct TestAmounts`
+ * @param limit earliest relevant transaction time
+ * @param cb callback to invoke for each amount
+ * @param cb_cls closure for @a cb
+ * @return transaction status
+ */
+static enum GNUNET_DB_QueryStatus
+amount_iterator (void *cls,
+ struct GNUNET_TIME_Absolute limit,
+ TALER_KYCLOGIC_KycAmountCallback cb,
+ void *cb_cls)
+{
+ const struct TestAmounts *ta = cls;
+ struct GNUNET_TIME_Absolute now = GNUNET_TIME_absolute_get ();
+
+ (void) limit;
+ for (unsigned int i=0; i<ta->num_amounts; i++)
+ {
+ struct TALER_Amount amount;
+
+ GNUNET_assert (GNUNET_OK ==
+ TALER_string_to_amount (ta->amounts[i],
+ &amount));
+ GNUNET_assert (GNUNET_OK ==
+ cb (cb_cls,
+ &amount,
+ now));
+ }
+ return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT;
+}
+
+
+/**
+ * Check whether the configured EUR:2 rule triggers for @a amounts.
+ *
+ * @param amounts amounts to add up
+ * @param num_amounts length of @a amounts
+ * @param expect_trigger true if the rule should trigger
+ */
+static void
+check_threshold (const char *const amounts[],
+ unsigned int num_amounts,
+ bool expect_trigger)
+{
+ struct TestAmounts ta = {
+ .amounts = amounts,
+ .num_amounts = num_amounts
+ };
+ const struct TALER_KYCLOGIC_KycRule *triggered_rule;
+ struct TALER_Amount next_threshold;
+ struct TALER_Amount expected_threshold;
+
+ GNUNET_assert (
+ GNUNET_DB_STATUS_SUCCESS_ONE_RESULT ==
+ TALER_KYCLOGIC_kyc_test_required (
+ TALER_KYCLOGIC_KYC_TRIGGER_DEPOSIT,
+ NULL,
+ &amount_iterator,
+ &ta,
+ &triggered_rule,
+ &next_threshold));
+ GNUNET_assert (expect_trigger == (NULL != triggered_rule));
+ GNUNET_assert (GNUNET_OK ==
+ TALER_string_to_amount ("EUR:2",
+ &expected_threshold));
+ GNUNET_assert (0 ==
+ TALER_amount_cmp (&expected_threshold,
+ &next_threshold));
+}
+
+
+int
+main (int argc,
+ const char *const argv[])
+{
+ struct GNUNET_CONFIGURATION_Handle *cfg;
+ const char *const below[] = { "EUR:1.99999999" };
+ const char *const equal[] = { "EUR:2" };
+ const char *const cumulative_equal[] = { "EUR:1", "EUR:1" };
+ const char *const cumulative_above[] = { "EUR:1", "EUR:1.00000001" };
+
+ (void) argc;
+ (void) argv;
+ GNUNET_log_setup ("test-kyclogic",
+ "WARNING",
+ NULL);
+ cfg = GNUNET_CONFIGURATION_create (TALER_EXCHANGE_project_data ());
+ GNUNET_CONFIGURATION_set_value_string (cfg,
+ "exchange",
+ "CURRENCY",
+ "EUR");
+ GNUNET_CONFIGURATION_set_value_string (cfg,
+ "kyc-rule-deposit-boundary",
+ "ENABLED",
+ "YES");
+ GNUNET_CONFIGURATION_set_value_string (cfg,
+ "kyc-rule-deposit-boundary",
+ "EXPOSED",
+ "YES");
+ GNUNET_CONFIGURATION_set_value_string (cfg,
+ "kyc-rule-deposit-boundary",
+ "IS_AND_COMBINATOR",
+ "YES");
+ GNUNET_CONFIGURATION_set_value_string (cfg,
+ "kyc-rule-deposit-boundary",
+ "OPERATION_TYPE",
+ "DEPOSIT");
+ GNUNET_CONFIGURATION_set_value_string (cfg,
+ "kyc-rule-deposit-boundary",
+ "THRESHOLD",
+ "EUR:2");
+ GNUNET_CONFIGURATION_set_value_string (cfg,
+ "kyc-rule-deposit-boundary",
+ "TIMEFRAME",
+ "1 day");
+ GNUNET_CONFIGURATION_set_value_string (cfg,
+ "kyc-rule-deposit-boundary",
+ "NEXT_MEASURES",
+ "verboten");
+ GNUNET_assert (GNUNET_OK ==
+ TALER_KYCLOGIC_kyc_init (cfg,
+ NULL));
+ GNUNET_CONFIGURATION_destroy (cfg);
+
+ /* The configured threshold is an inclusive maximum. */
+ check_threshold (below,
+ sizeof (below) / sizeof (below[0]),
+ false);
+ check_threshold (equal,
+ sizeof (equal) / sizeof (equal[0]),
+ false);
+ check_threshold (cumulative_equal,
+ sizeof (cumulative_equal) /
+ sizeof (cumulative_equal[0]),
+ false);
+ check_threshold (cumulative_above,
+ sizeof (cumulative_above) /
+ sizeof (cumulative_above[0]),
+ true);
+
+ TALER_KYCLOGIC_kyc_done ();
+ return 0;
+}