commit eb86e454e1ad49525043bb839004694d5904770a
parent cefa2afe9690764b1487b30b1d97071b8745d049
Author: Christian Grothoff <christian@grothoff.org>
Date: Mon, 8 Jun 2026 22:29:25 +0200
implement backend logic for #11183: allow customer to accept ToS before KYC AUTH transfer and have merchant backend fake-accept after KYC AUTH is done
Diffstat:
2 files changed, 407 insertions(+), 16 deletions(-)
diff --git a/src/backend/taler-merchant-kyccheck.c b/src/backend/taler-merchant-kyccheck.c
@@ -21,6 +21,8 @@
#include "platform.h"
struct Inquiry;
#define TALER_EXCHANGE_GET_KYC_CHECK_RESULT_CLOSURE struct Inquiry
+#define TALER_EXCHANGE_GET_KYC_INFO_RESULT_CLOSURE struct Inquiry
+#define TALER_EXCHANGE_POST_KYC_UPLOAD_RESULT_CLOSURE struct Inquiry
#include "microhttpd.h"
#include <gnunet/gnunet_util_lib.h>
#include <jansson.h>
@@ -29,13 +31,16 @@ struct Inquiry;
#include <taler/taler_dbevents.h>
#include <taler/taler_json_lib.h>
#include <taler/taler_exchange_service.h>
+#include <taler/exchange/post-kyc-upload-ID.h>
#include "taler/taler_merchant_util.h"
#include "taler/taler_merchant_bank_lib.h"
#include "merchantdb_lib.h"
#include "merchantdb_lib.h"
#include "merchant-database/account_kyc_get_outdated.h"
#include "merchant-database/account_kyc_set_status.h"
+#include "merchant-database/delete_tos_accepted_early.h"
#include "merchant-database/get_kyc_status.h"
+#include "merchant-database/lookup_tos_accepted_early.h"
#include "merchant-database/set_instance.h"
#include "merchant-database/select_accounts.h"
#include "merchant-database/select_exchange_keys.h"
@@ -90,6 +95,20 @@ static struct GNUNET_TIME_Relative aml_low_freq;
*/
#define OPEN_INQUIRY_LIMIT 1024
+/**
+ * Name of the KYC form (``FORM_ID``) the exchange uses to affirm
+ * acceptance of the terms of service. Must match the value submitted
+ * by #TALER_EXCHANGE_post_kyc_upload_accept_tos_create().
+ */
+#define ACCEPT_TOS_FORM "accept-tos"
+
+/**
+ * Minimum delay before we retry after the exchange returned an
+ * internal error to our attempt to automatically accept the terms
+ * of service on behalf of the user.
+ */
+#define TOS_ERROR_RETRY_DELAY GNUNET_TIME_UNIT_HOURS
+
/**
* Information about an exchange.
@@ -216,6 +235,26 @@ struct Inquiry
struct TALER_EXCHANGE_GetKycCheckHandle *kyc;
/**
+ * Handle for fetching /kyc-info to discover the upload ID used to
+ * automatically accept the terms of service, NULL if not active.
+ */
+ struct TALER_EXCHANGE_GetKycInfoHandle *kyc_info;
+
+ /**
+ * Handle for the /kyc-upload request submitting the automatic
+ * terms-of-service acceptance, NULL if not active.
+ */
+ struct TALER_EXCHANGE_PostKycUploadHandle *tos_upload;
+
+ /**
+ * If non-NULL, the ``Taler-Terms-Version`` of the terms of service
+ * that the user accepted early (via ``POST /private/accept-tos-early``)
+ * and that we are trying to submit to the exchange on their behalf.
+ * Owned by the inquiry.
+ */
+ char *tos_etag;
+
+ /**
* Access token for the /kyc-info API.
*/
struct TALER_AccountAccessTokenP access_token;
@@ -527,6 +566,345 @@ store_kyc_status (
/**
+ * The current interaction with the exchange for inquiry @a i is
+ * complete (or was aborted). Schedule the next periodic KYC check
+ * at @a i->due and release the active-inquiry slot.
+ *
+ * @param[in,out] i the inquiry to reschedule
+ */
+static void
+finish_inquiry (struct Inquiry *i)
+{
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Will repeat inquiry in %s\n",
+ GNUNET_TIME_relative2s (
+ GNUNET_TIME_absolute_get_remaining (i->due),
+ true));
+ if (! GNUNET_TIME_absolute_is_never (i->due))
+ i->task = GNUNET_SCHEDULER_add_at (i->due,
+ &inquiry_work,
+ i);
+ end_inquiry ();
+}
+
+
+/**
+ * Clear the tos-accepted data from the user, we do not
+ * need the flag anymore, either because we passed it on
+ * to the exchange or because they are too old.
+ *
+ * @param i inquiry this is about
+ */
+static void
+clear_tos (const struct Inquiry *i)
+{
+ enum GNUNET_DB_QueryStatus qs;
+
+ qs = TALER_MERCHANTDB_set_instance (pg,
+ i->a->instance_id);
+ if (qs < 0)
+ {
+ GNUNET_break (0);
+ global_ret = EXIT_FAILURE;
+ GNUNET_SCHEDULER_shutdown ();
+ return;
+ }
+ qs = TALER_MERCHANTDB_delete_tos_accepted_early (
+ pg,
+ i->a->instance_id,
+ i->e->keys->exchange_url);
+ GNUNET_break (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT ==
+ TALER_MERCHANTDB_set_instance (pg,
+ NULL));
+ if (qs < 0)
+ {
+ GNUNET_break (0);
+ global_ret = EXIT_FAILURE;
+ GNUNET_SCHEDULER_shutdown ();
+ return;
+ }
+}
+
+
+/**
+ * Function called with the result of submitting an automatic
+ * terms-of-service acceptance to the exchange via /kyc-upload.
+ *
+ * @param i the inquiry the acceptance was for
+ * @param pr the exchange's response
+ */
+static void
+tos_upload_cb (struct Inquiry *i,
+ const struct TALER_EXCHANGE_PostKycUploadResponse *pr)
+{
+ unsigned int http_status = pr->hr.http_status;
+
+ i->tos_upload = NULL;
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Automatic ToS acceptance for `%s' at `%s' returned HTTP %u\n",
+ i->a->merchant_account_uri.full_payto,
+ i->e->keys->exchange_url,
+ http_status);
+ switch (http_status)
+ {
+ case MHD_HTTP_OK:
+ case MHD_HTTP_NO_CONTENT:
+ /* Exchange accepted the terms of service: re-check KYC now. */
+ i->due = GNUNET_TIME_UNIT_ZERO_ABS;
+ clear_tos (i);
+ break;
+ case 0: /* no answer, like network failure */
+ case MHD_HTTP_INTERNAL_SERVER_ERROR:
+ case MHD_HTTP_BAD_GATEWAY:
+ case MHD_HTTP_REQUEST_ENTITY_TOO_LARGE: /* Wild error */
+ /* Internal/transient error at the exchange: do NOT clear the early
+ acceptance, but back off for at least an hour before retrying
+ with a regular periodic KYC check. */
+ GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
+ "Exchange `%s' failed to process automatic ToS acceptance (HTTP %u); retrying later\n",
+ i->e->keys->exchange_url,
+ http_status);
+ i->due = GNUNET_TIME_relative_to_absolute (
+ GNUNET_TIME_randomize (TOS_ERROR_RETRY_DELAY));
+ break;
+ case MHD_HTTP_NOT_FOUND:
+ /* Something must have changed exchange-side, try again
+ immediately, but do not clear ToS acceptance */
+ i->due = GNUNET_TIME_UNIT_ZERO_ABS;
+ clear_tos (i);
+ break;
+ case MHD_HTTP_BAD_REQUEST:
+ /* This should not happen, go back to manual KYC */
+ GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
+ "Exchange `%s' failed to process automatic ToS acceptance (HTTP %u); retrying later\n",
+ i->e->keys->exchange_url,
+ http_status);
+ i->due = GNUNET_TIME_relative_to_absolute (
+ GNUNET_TIME_randomize (TOS_ERROR_RETRY_DELAY));
+ break;
+ case MHD_HTTP_CONFLICT:
+ /* Exchange rejected the accepted ToS version (ETag not acceptable
+ or ToS acceptance disappeared):
+ clear the early acceptance so we do not loop, then re-check KYC
+ (the user will have to accept the ToS through the regular flow
+ if it still applies). */
+ GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
+ "Exchange `%s' rejected early ToS acceptance (version `%s', HTTP %u); clearing early acceptance\n",
+ i->e->keys->exchange_url,
+ i->tos_etag,
+ http_status);
+ clear_tos (i);
+ i->due = GNUNET_TIME_UNIT_ZERO_ABS;
+ break;
+ }
+ GNUNET_free (i->tos_etag);
+ finish_inquiry (i);
+}
+
+
+/**
+ * Submit an automatic terms-of-service acceptance for inquiry @a i to
+ * the exchange, using the @a id of the corresponding KYC requirement
+ * (obtained from /kyc-info) and the early-accepted version in
+ * @a i->tos_etag.
+ *
+ * @param[in,out] i inquiry to submit the ToS acceptance for
+ * @param id KYC requirement / upload ID for the terms-of-service form
+ */
+static void
+start_tos_upload (struct Inquiry *i,
+ const char *id)
+{
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Submitting automatic ToS acceptance (version `%s', id `%s') to `%s'\n",
+ i->tos_etag,
+ id,
+ i->e->keys->exchange_url);
+ i->tos_upload = TALER_EXCHANGE_post_kyc_upload_accept_tos_create (
+ ctx,
+ i->e->keys->exchange_url,
+ id,
+ i->tos_etag);
+ if ( (NULL == i->tos_upload) ||
+ (TALER_EC_NONE !=
+ TALER_EXCHANGE_post_kyc_upload_start (i->tos_upload,
+ &tos_upload_cb,
+ i)) )
+ {
+ GNUNET_break (0);
+ if (NULL != i->tos_upload)
+ {
+ TALER_EXCHANGE_post_kyc_upload_cancel (i->tos_upload);
+ i->tos_upload = NULL;
+ }
+ /* Could not even start the upload: treat as transient, keep the
+ early acceptance and retry with a regular periodic check. */
+ GNUNET_free (i->tos_etag);
+ finish_inquiry (i);
+ }
+}
+
+
+/**
+ * Function called with the result of fetching /kyc-info while trying
+ * to automatically accept the terms of service. Finds the ID of the
+ * terms-of-service requirement and submits the acceptance.
+ *
+ * @param i the inquiry the lookup was for
+ * @param ir the exchange's response
+ */
+static void
+tos_info_cb (struct Inquiry *i,
+ const struct TALER_EXCHANGE_GetKycInfoResponse *ir)
+{
+ i->kyc_info = NULL;
+ if (MHD_HTTP_OK == ir->hr.http_status)
+ {
+ const char *id = NULL;
+
+ for (size_t j = 0; j < ir->details.ok.requirements_length; j++)
+ {
+ const struct TALER_EXCHANGE_RequirementInformation *req
+ = &ir->details.ok.requirements[j];
+
+ if ( (NULL != req->form) &&
+ (NULL != req->id) &&
+ (0 == strcmp (req->form,
+ ACCEPT_TOS_FORM)) )
+ {
+ id = req->id;
+ break;
+ }
+ }
+ if (NULL != id)
+ {
+ start_tos_upload (i,
+ id);
+ return;
+ }
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "No `%s' requirement at `%s'; cannot auto-accept ToS, falling back to periodic check\n",
+ ACCEPT_TOS_FORM,
+ i->e->keys->exchange_url);
+ }
+ else
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
+ "GET /kyc-info at `%s' returned HTTP %u; cannot auto-accept ToS now\n",
+ i->e->keys->exchange_url,
+ ir->hr.http_status);
+ }
+ /* Could not determine the upload ID: keep the early acceptance and
+ retry on the next regular periodic KYC check. */
+ GNUNET_free (i->tos_etag);
+ finish_inquiry (i);
+}
+
+
+/**
+ * Start the automatic terms-of-service acceptance for inquiry @a i by
+ * fetching /kyc-info to discover the ID of the terms-of-service
+ * requirement. The early-accepted version is in @a i->tos_etag.
+ *
+ * @param[in,out] i inquiry to auto-accept the terms of service for
+ */
+static void
+start_tos_info (struct Inquiry *i)
+{
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Fetching /kyc-info from `%s' to auto-accept ToS for `%s'\n",
+ i->e->keys->exchange_url,
+ i->a->merchant_account_uri.full_payto);
+ i->kyc_info = TALER_EXCHANGE_get_kyc_info_create (ctx,
+ i->e->keys->exchange_url,
+ &i->access_token);
+ if ( (NULL == i->kyc_info) ||
+ (TALER_EC_NONE !=
+ TALER_EXCHANGE_get_kyc_info_start (i->kyc_info,
+ &tos_info_cb,
+ i)) )
+ {
+ GNUNET_break (0);
+ if (NULL != i->kyc_info)
+ {
+ TALER_EXCHANGE_get_kyc_info_cancel (i->kyc_info);
+ i->kyc_info = NULL;
+ }
+ /* Could not start the lookup: keep the early acceptance and retry
+ with a regular periodic check. */
+ GNUNET_free (i->tos_etag);
+ finish_inquiry (i);
+ }
+}
+
+
+/**
+ * The exchange asked us (via @a tos_required) to accept its terms of
+ * service. Check whether the user already accepted the terms of
+ * service early (via ``POST /private/accept-tos-early``). If so,
+ * remember the accepted version in @a i->tos_etag so that we will try
+ * to submit it to the exchange automatically.
+ *
+ * @param[in,out] i inquiry for which the exchange requires ToS acceptance
+ * @param req required ETag for the accepted ToS
+ */
+static void
+check_early_tos_acceptance (struct Inquiry *i,
+ const char *req)
+{
+ enum GNUNET_DB_QueryStatus qs;
+ char *tos_version = NULL;
+
+ qs = TALER_MERCHANTDB_set_instance (pg,
+ i->a->instance_id);
+ if (qs < 0)
+ {
+ GNUNET_break (0);
+ global_ret = EXIT_FAILURE;
+ GNUNET_SCHEDULER_shutdown ();
+ return;
+ }
+ qs = TALER_MERCHANTDB_lookup_tos_accepted_early (pg,
+ i->a->instance_id,
+ i->e->keys->exchange_url,
+ &tos_version);
+ GNUNET_break (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT ==
+ TALER_MERCHANTDB_set_instance (pg,
+ NULL));
+ if (qs < 0)
+ {
+ GNUNET_break (0);
+ global_ret = EXIT_FAILURE;
+ GNUNET_SCHEDULER_shutdown ();
+ return;
+ }
+ if (NULL == tos_version)
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Exchange `%s' supports early ToS acceptance, but user did not accept ToS early\n",
+ i->e->keys->exchange_url);
+ return;
+ }
+ if (0 != strcmp (tos_version,
+ req))
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "User accepted outdated ToS version `%s' early, but exchange wants `%s'. User will need to accept the ToS again!\n",
+ tos_version,
+ req);
+ GNUNET_free (tos_version);
+ return;
+ }
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "User accepted ToS version `%s' early; will submit to `%s'\n",
+ tos_version,
+ i->e->keys->exchange_url);
+ GNUNET_free (i->tos_etag);
+ i->tos_etag = tos_version;
+}
+
+
+/**
* Function called with the result of a KYC check.
*
* @param cls a `struct Inquiry *`
@@ -597,12 +975,6 @@ exchange_check_cb (
progress = true;
i->rule_gen = ks->details.accepted.rule_gen;
i->last_kyc_check = GNUNET_TIME_timestamp_get ();
- if (NULL != ks->details.accepted.tos_required)
- {
- /* FIXME: #11183: check if our user *did* already
- accept the ToS, if so, send acceptance to exchange
- and try again! */
- }
/* exchange says KYC is required */
i->auth_ok = true;
store_kyc_status (i,
@@ -612,6 +984,14 @@ exchange_check_cb (
if (! progress)
i->due = GNUNET_TIME_absolute_max (i->last_kyc_check.abs_time,
i->timeout);
+ if (NULL != ks->details.accepted.tos_required)
+ {
+ /* Exchange wants the user to accept its terms of service.
+ If the user already accepted them early, try to submit that
+ acceptance to the exchange automatically. */
+ check_early_tos_acceptance (i,
+ ks->details.accepted.tos_required);
+ }
break;
case MHD_HTTP_NO_CONTENT:
i->rule_gen = 0;
@@ -731,16 +1111,15 @@ exchange_check_cb (
(int) qs);
i->not_first_time = true;
}
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Will repeat inquiry in %s\n",
- GNUNET_TIME_relative2s (
- GNUNET_TIME_absolute_get_remaining (i->due),
- true));
- if (! GNUNET_TIME_absolute_is_never (i->due))
- i->task = GNUNET_SCHEDULER_add_at (i->due,
- &inquiry_work,
- i);
- end_inquiry ();
+ if (NULL != i->tos_etag)
+ {
+ /* The user accepted the terms of service early and the exchange now
+ requires acceptance: try to submit it automatically (this keeps
+ the active-inquiry slot) instead of waiting for the next check. */
+ start_tos_info (i);
+ return;
+ }
+ finish_inquiry (i);
}
@@ -951,6 +1330,17 @@ stop_inquiry (struct Inquiry *i)
TALER_EXCHANGE_get_kyc_check_cancel (i->kyc);
i->kyc = NULL;
}
+ if (NULL != i->kyc_info)
+ {
+ TALER_EXCHANGE_get_kyc_info_cancel (i->kyc_info);
+ i->kyc_info = NULL;
+ }
+ if (NULL != i->tos_upload)
+ {
+ TALER_EXCHANGE_post_kyc_upload_cancel (i->tos_upload);
+ i->tos_upload = NULL;
+ }
+ GNUNET_free (i->tos_etag);
if (NULL != i->jlimits)
{
json_decref (i->jlimits);
diff --git a/src/include/merchant-database/all.h b/src/include/merchant-database/all.h
@@ -28,6 +28,7 @@
#include "merchant-database/delete_report.h"
#include "merchant-database/delete_template.h"
#include "merchant-database/delete_token_family.h"
+#include "merchant-database/delete_tos_accepted_early.h"
#include "merchant-database/delete_transfer.h"
#include "merchant-database/delete_unit.h"
#include "merchant-database/delete_webhook.h"