commit f5eb43fc93770b12885433ffb5b1720e9f0a6d95
parent 8ebbed0a6d45c63a3fdb25cbcf089833edd7293a
Author: Florian Dold <dold@taler.net>
Date: Sun, 30 Aug 2026 01:54:04 +0200
exchange AML API: correct account lifecycle filtering
Diffstat:
4 files changed, 155 insertions(+), 20 deletions(-)
diff --git a/src/exchange/taler-exchange-httpd_get-aml-OFFICER_PUB-accounts.c b/src/exchange/taler-exchange-httpd_get-aml-OFFICER_PUB-accounts.c
@@ -153,11 +153,13 @@ record_cb (
bool to_investigate,
struct TALER_FullPayto payto)
{
- if ( (NULL == comments) &&
- (GNUNET_TIME_absolute_is_never (open_time.abs_time)) )
- comments = "transacted amounts below limits that trigger account opening";
- if (NULL == comments)
- comments = "";
+ const bool below_opening_threshold =
+ (NULL == comments) &&
+ (GNUNET_TIME_absolute_is_never (open_time.abs_time));
+ const char *system_note = below_opening_threshold
+ ? "BELOW_ACCOUNT_OPENING_THRESHOLD"
+ : NULL;
+
switch (rc->format)
{
case RCF_JSON:
@@ -175,6 +177,9 @@ record_cb (
GNUNET_JSON_pack_allow_null (
GNUNET_JSON_pack_string ("comments",
comments)),
+ GNUNET_JSON_pack_allow_null (
+ GNUNET_JSON_pack_string ("system_note",
+ system_note)),
GNUNET_JSON_pack_int64 ("rowid",
row_id),
GNUNET_JSON_pack_timestamp ("open_time",
@@ -193,13 +198,16 @@ record_cb (
char closetime_s[128];
const struct tm *tm;
time_t tt;
+ const char *export_comments = comments;
epayto = TALER_escape_xml (payto.full_payto);
- if ( (0 == strlen (comments)) &&
- (GNUNET_TIME_absolute_is_never (open_time.abs_time)) )
- comments =
- "transacted amounts below limits that trigger account opening";
- ecomments = TALER_escape_xml (comments);
+ if (below_opening_threshold)
+ export_comments =
+ "System: transactions remain below the threshold "
+ "that opens an AML file";
+ if (NULL == export_comments)
+ export_comments = "";
+ ecomments = TALER_escape_xml (export_comments);
tt = (time_t) GNUNET_TIME_timestamp_to_s (open_time);
tm = gmtime (&tt);
if (NULL != tm)
@@ -257,10 +265,19 @@ record_cb (
char *epayto;
char otbuf[64];
char ctbuf[64];
- size_t len = strlen (comments);
+ const char *export_comments = comments;
+ size_t len;
size_t plen = strlen (payto.full_payto);
size_t wpos = 0;
+ if (below_opening_threshold)
+ export_comments =
+ "System: transactions remain below the threshold "
+ "that opens an AML file";
+ if (NULL == export_comments)
+ export_comments = "";
+ len = strlen (export_comments);
+
GNUNET_snprintf (otbuf,
sizeof (otbuf),
"%s",
@@ -273,9 +290,9 @@ record_cb (
ecomments = GNUNET_malloc (2 * len + 1);
for (size_t off = 0; off<len; off++)
{
- if ('"' == comments[off])
+ if ('"' == export_comments[off])
ecomments[wpos++] = '"';
- ecomments[wpos++] = comments[off];
+ ecomments[wpos++] = export_comments[off];
}
/* Escape 'payto' to double '"' as per RFC 4180, 2.7. */
epayto = GNUNET_malloc (2 * plen + 1);
diff --git a/src/exchangedb/iterate_kyc_accounts.c b/src/exchangedb/iterate_kyc_accounts.c
@@ -200,8 +200,9 @@ TALER_EXCHANGEDB_iterate_kyc_accounts (
// select most recent outcomes only
" AND COALESCE (lo.is_active, TRUE)"
" AND ($3 OR (COALESCE(lo.to_investigate,FALSE) = $4))"
- // Account is open if we had an AML outcome
- " AND ($5 OR ((lo.outcome_serial_id IS NULL) = $6))"
+ // An AML file is open between ACCOUNT_OPEN and ACCOUNT_IDLE.
+ " AND ($5 OR (((kt.open_time IS NOT NULL)"
+ " AND (kt.close_time IS NULL)) = $6))"
" AND ($7 OR ((COALESCE((lo.jproperties ->>'HIGH_RISK_CUSTOMER')::bool,FALSE) = $8)))"
" ORDER BY kt.kyc_target_serial_id ASC"
" LIMIT $2");
@@ -230,8 +231,9 @@ TALER_EXCHANGEDB_iterate_kyc_accounts (
// select most recent outcomes only
" AND COALESCE (lo.is_active, TRUE)"
" AND ($3 OR (COALESCE(lo.to_investigate,FALSE) = $4))"
- // Account is open if we had an AML outcome
- " AND ($5 OR ((lo.outcome_serial_id IS NULL) = $6))"
+ // An AML file is open between ACCOUNT_OPEN and ACCOUNT_IDLE.
+ " AND ($5 OR (((kt.open_time IS NOT NULL)"
+ " AND (kt.close_time IS NULL)) = $6))"
" AND ($7 OR ((COALESCE((lo.jproperties ->>'HIGH_RISK_CUSTOMER')::bool,FALSE) = $8)))"
" ORDER BY kt.kyc_target_serial_id DESC"
" LIMIT $2");
diff --git a/src/exchangedb/test_kyc_targets.c b/src/exchangedb/test_kyc_targets.c
@@ -51,6 +51,44 @@ static struct TDB_Account other;
/**
+ * Set the AML file lifecycle timestamps for an account.
+ *
+ * @param pg database context
+ * @param acc account to update
+ * @param opened true to set an opening timestamp
+ * @param closed true to set a closing timestamp
+ */
+static void
+set_file_lifecycle (struct TALER_EXCHANGEDB_PostgresContext *pg,
+ const struct TDB_Account *acc,
+ bool opened,
+ bool closed)
+{
+ struct GNUNET_TIME_Timestamp now = GNUNET_TIME_timestamp_get ();
+ struct GNUNET_PQ_QueryParam params[] = {
+ GNUNET_PQ_query_param_auto_from_type (&acc->h_normalized),
+ opened
+ ? GNUNET_PQ_query_param_timestamp (&now)
+ : GNUNET_PQ_query_param_null (),
+ closed
+ ? GNUNET_PQ_query_param_timestamp (&now)
+ : GNUNET_PQ_query_param_null (),
+ GNUNET_PQ_query_param_end
+ };
+
+ GNUNET_assert (GNUNET_OK ==
+ GNUNET_PQ_prepare_anon (pg->conn,
+ "UPDATE kyc_targets"
+ " SET open_time=$2, close_time=$3"
+ " WHERE h_normalized_payto=$1;"));
+ GNUNET_assert (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT ==
+ GNUNET_PQ_eval_prepared_non_select (pg->conn,
+ "",
+ params));
+}
+
+
+/**
* Read the access token the database generated for an account.
*
* @param pg the database context
@@ -435,20 +473,91 @@ check_iterate (struct TALER_EXCHANGEDB_PostgresContext *pg)
&ctx));
FAILIF (0 != ctx.total);
- /* and no account has an AML outcome, which is what "open" means here */
+ /* Neither AML file has been opened yet. */
memset (&ctx,
0,
sizeof (ctx));
FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
TALER_EXCHANGEDB_iterate_kyc_accounts (pg,
TALER_EXCHANGE_YNA_ALL,
+ TALER_EXCHANGE_YNA_YES,
+ TALER_EXCHANGE_YNA_ALL,
+ 0,
+ 10,
+ &account_cb,
+ &ctx));
+ memset (&ctx,
+ 0,
+ sizeof (ctx));
+ FAILIF (2 !=
+ TALER_EXCHANGEDB_iterate_kyc_accounts (pg,
+ TALER_EXCHANGE_YNA_ALL,
+ TALER_EXCHANGE_YNA_NO,
+ TALER_EXCHANGE_YNA_ALL,
+ 0,
+ 10,
+ &account_cb,
+ &ctx));
+
+ /* Opening one AML file splits the YES and NO result sets. */
+ set_file_lifecycle (pg,
+ &account,
+ true,
+ false);
+ memset (&ctx,
+ 0,
+ sizeof (ctx));
+ FAILIF (1 !=
+ TALER_EXCHANGEDB_iterate_kyc_accounts (pg,
+ TALER_EXCHANGE_YNA_ALL,
+ TALER_EXCHANGE_YNA_YES,
+ TALER_EXCHANGE_YNA_ALL,
+ 0,
+ 10,
+ &account_cb,
+ &ctx));
+ memset (&ctx,
+ 0,
+ sizeof (ctx));
+ FAILIF (1 !=
+ TALER_EXCHANGEDB_iterate_kyc_accounts (pg,
+ TALER_EXCHANGE_YNA_ALL,
+ TALER_EXCHANGE_YNA_NO,
+ TALER_EXCHANGE_YNA_ALL,
+ 0,
+ 10,
+ &account_cb,
+ &ctx));
+
+ /* Closing it moves the file back into the NO result set. */
+ set_file_lifecycle (pg,
+ &account,
+ true,
+ true);
+ memset (&ctx,
+ 0,
+ sizeof (ctx));
+ FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
+ TALER_EXCHANGEDB_iterate_kyc_accounts (pg,
+ TALER_EXCHANGE_YNA_ALL,
+ TALER_EXCHANGE_YNA_YES,
+ TALER_EXCHANGE_YNA_ALL,
+ 0,
+ 10,
+ &account_cb,
+ &ctx));
+ memset (&ctx,
+ 0,
+ sizeof (ctx));
+ FAILIF (2 !=
+ TALER_EXCHANGEDB_iterate_kyc_accounts (pg,
+ TALER_EXCHANGE_YNA_ALL,
TALER_EXCHANGE_YNA_NO,
TALER_EXCHANGE_YNA_ALL,
0,
10,
&account_cb,
&ctx));
- FAILIF (0 != ctx.total);
return 0;
}
diff --git a/src/include/taler/exchange/get-aml-OFFICER_PUB-accounts.h b/src/include/taler/exchange/get-aml-OFFICER_PUB-accounts.h
@@ -286,11 +286,18 @@ struct TALER_EXCHANGE_GetAmlAccountsAccountSummary
bool high_risk;
/**
- * Latest comments about the account. Can be NULL.
+ * Latest officer-authored file note about the account. Can be NULL.
*/
const char *comments;
/**
+ * Machine-readable system note about the account. Can be NULL.
+ * The currently defined value is
+ * "BELOW_ACCOUNT_OPENING_THRESHOLD".
+ */
+ const char *system_note;
+
+ /**
* Row of the account in the exchange tables. Useful to filter by offset.
*/
uint64_t rowid;