exchange

Base system with REST service to issue digital coins, run by the payment service provider
Log | Files | Refs | Submodules | README | LICENSE

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:
Msrc/exchange/taler-exchange-httpd_get-aml-OFFICER_PUB-accounts.c | 43++++++++++++++++++++++++++++++-------------
Msrc/exchangedb/iterate_kyc_accounts.c | 10++++++----
Msrc/exchangedb/test_kyc_targets.c | 113+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Msrc/include/taler/exchange/get-aml-OFFICER_PUB-accounts.h | 9++++++++-
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;