commit e9837976d0c89c2fa7a50b61854d1cf6da5e8705
parent aa97d3af2443e20190dc163959816cc49d9c837b
Author: Christian Grothoff <christian@grothoff.org>
Date: Mon, 12 Oct 2026 22:52:44 +0200
fix overlooked PRIMARY KEY on kyc_alerts, only the existing UNIQUE should apply
Diffstat:
3 files changed, 117 insertions(+), 1 deletion(-)
diff --git a/src/exchangedb/sql-schema/exchange-0019.sql b/src/exchangedb/sql-schema/exchange-0019.sql
@@ -0,0 +1,44 @@
+--
+-- 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/>
+--
+
+BEGIN;
+
+SELECT _v.register_patch('exchange-0019', NULL, NULL);
+
+SET search_path TO exchange;
+
+
+-- Allow one pending alert per account *and* receiver.
+--
+-- kyc_alerts was keyed by h_payto alone, so an account could only have a
+-- single pending alert, whatever its trigger_type: the alert for a second
+-- receiver was dropped by the producers' ON CONFLICT DO NOTHING, and the
+-- first receiver to drain the row deleted it for everybody. Key the table
+-- by (trigger_type, h_payto) instead, which also makes the old
+-- UNIQUE (trigger_type, h_payto) redundant. Existing rows satisfy both
+-- keys, so they are kept as they are.
+
+ALTER TABLE kyc_alerts
+ DROP CONSTRAINT kyc_alerts_pkey,
+ DROP CONSTRAINT kyc_alerts_trigger_type_h_payto_key;
+
+ALTER TABLE kyc_alerts
+ ADD PRIMARY KEY (trigger_type, h_payto);
+
+COMMENT ON TABLE kyc_alerts
+ IS 'alerts about completed KYC events reliably notifying other components (even if they are not running); at most one pending alert per account and receiver';
+
+COMMIT;
diff --git a/src/exchangedb/sql-schema/meson.build b/src/exchangedb/sql-schema/meson.build
@@ -195,6 +195,7 @@ generated_sql = [
['exchange-0016.sql', ['exchange-0016.sql']],
['exchange-0017.sql', ['exchange-0017.sql']],
['exchange-0018.sql', ['exchange-0018.sql']],
+ ['exchange-0019.sql', ['exchange-0019.sql']],
['tops-0001.sql', ['tops-0001.sql']],
]
diff --git a/src/exchangedb/test_kyc_alerts.c b/src/exchangedb/test_kyc_alerts.c
@@ -19,7 +19,8 @@
* `kyc_alerts`
* @author Christian Grothoff
*
- * Covers #TALER_EXCHANGEDB_do_drain_kyc_alert().
+ * Covers #TALER_EXCHANGEDB_do_drain_kyc_alert(), and that the table keeps
+ * one alert per account and trigger type.
*
* `kyc_alerts` is a work queue: the stored procedures that change the KYC
* state of an account append a row, and taler-exchange-aggregator drains
@@ -39,6 +40,11 @@
*/
#define AGGREGATOR_TRIGGER 1
+/**
+ * Trigger type of a (hypothetical) second receiver of alerts.
+ */
+#define OTHER_TRIGGER 2
+
/**
* The two accounts we raise alerts for.
@@ -200,6 +206,69 @@ check_requeue (struct TALER_EXCHANGEDB_PostgresContext *pg)
/**
+ * Each receiver has its own queue: an account can be queued for two
+ * receivers at once, and draining it for one leaves it queued for the
+ * other.
+ *
+ * @param pg the database context
+ * @return 0 on success
+ */
+static int
+check_per_trigger (struct TALER_EXCHANGEDB_PostgresContext *pg)
+{
+ struct TALER_NormalizedPaytoHashP h_payto;
+ char *hex;
+
+ /* no producer writes another trigger type yet, so do it by hand */
+ hex = TDB_hex (&account[0].h_normalized,
+ sizeof (account[0].h_normalized));
+ FAILIF_C (GNUNET_OK !=
+ TDB_exec (pg,
+ "INSERT INTO kyc_alerts"
+ " (h_payto,trigger_type)"
+ " VALUES (decode('%s','hex'),%d);",
+ hex,
+ OTHER_TRIGGER),
+ GNUNET_free (hex));
+ GNUNET_free (hex);
+ FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
+ TALER_EXCHANGEDB_insert_aml_program_failure (
+ pg,
+ 0,
+ &account[0].h_normalized,
+ "for the aggregator",
+ TALER_EC_EXCHANGE_GENERIC_KYC_FAILED));
+ /* both alerts for the same account are queued */
+ FAILIF (2 != TDB_count (pg,
+ "FROM kyc_alerts"));
+ FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
+ TALER_EXCHANGEDB_do_drain_kyc_alert (pg,
+ AGGREGATOR_TRIGGER,
+ &h_payto));
+ FAILIF (0 != GNUNET_memcmp (&h_payto,
+ &account[0].h_normalized));
+ /* draining it for the aggregator leaves the other receiver's alert */
+ FAILIF (1 != TDB_count (pg,
+ "FROM kyc_alerts"
+ " WHERE trigger_type=%d",
+ OTHER_TRIGGER));
+ FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
+ TALER_EXCHANGEDB_do_drain_kyc_alert (pg,
+ AGGREGATOR_TRIGGER,
+ &h_payto));
+ FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
+ TALER_EXCHANGEDB_do_drain_kyc_alert (pg,
+ OTHER_TRIGGER,
+ &h_payto));
+ FAILIF (0 != GNUNET_memcmp (&h_payto,
+ &account[0].h_normalized));
+ FAILIF (0 != TDB_count (pg,
+ "FROM kyc_alerts"));
+ return 0;
+}
+
+
+/**
* The checks to run, in order.
*/
static const struct TDB_Test tests[] = {
@@ -211,6 +280,8 @@ static const struct TDB_Test tests[] = {
&check_drain },
{ "kyc-alerts-requeue",
&check_requeue },
+ { "kyc-alerts-per-trigger",
+ &check_per_trigger },
{ NULL, NULL }
};