merchant

Merchant backend to process payments, run by merchants
Log | Files | Refs | Submodules | README | LICENSE

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:
Msrc/backend/taler-merchant-kyccheck.c | 422++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Msrc/include/merchant-database/all.h | 1+
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"