commit ca516cd0906f28e11e88281398786738ab86530a
parent c1980605f8ea61bcf32c714311b45c6c226b8b03
Author: Florian Dold <florian@dold.me>
Date: Tue, 24 Sep 2024 12:51:46 +0200
wallet-core: auto-abort payments when merchant has KYC problems
Diffstat:
6 files changed, 190 insertions(+), 1 deletion(-)
diff --git a/packages/taler-harness/src/integrationtests/test-kyc-threshold-withdrawal.ts b/packages/taler-harness/src/integrationtests/test-kyc-threshold-withdrawal.ts
@@ -268,6 +268,9 @@ export async function runKycThresholdWithdrawalTest(t: GlobalTestState) {
withdrawalOperationId: wop.withdrawal_id,
});
+
+ t.logStep("waiting for pending(kyc-required)");
+
const kycNotificationCond = walletClient.waitForNotificationCond((x) => {
if (
x.type === NotificationType.TransactionStateTransition &&
@@ -291,6 +294,8 @@ export async function runKycThresholdWithdrawalTest(t: GlobalTestState) {
const kycPaytoHash = txDet.kycPaytoHash;
t.assertTrue(!!kycPaytoHash);
+ t.logStep("posting aml decision");
+
await postAmlDecisionNoRules(t, {
amlPriv: amlKeypair.priv,
amlPub: amlKeypair.pub,
@@ -298,6 +303,8 @@ export async function runKycThresholdWithdrawalTest(t: GlobalTestState) {
paytoHash: kycPaytoHash,
});
+ t.logStep("waiting for withdrawal to be done");
+
const doneNotificationCond = walletClient.waitForNotificationCond((x) => {
if (
x.type === NotificationType.TransactionStateTransition &&
diff --git a/packages/taler-util/src/errors.ts b/packages/taler-util/src/errors.ts
@@ -35,6 +35,9 @@ import {
type empty = Record<string, never>;
export interface DetailsMap {
+ [TalerErrorCode.WALLET_PAY_MERCHANT_KYC_MISSING]: {
+ exchangeResponse: any;
+ };
[TalerErrorCode.WALLET_PENDING_OPERATION_FAILED]: {
innerError: TalerErrorDetail;
transactionId?: string;
diff --git a/packages/taler-util/src/taler-error-codes.ts b/packages/taler-util/src/taler-error-codes.ts
@@ -689,6 +689,38 @@ export enum TalerErrorCode {
/**
+ * The KYC operation failed. This could be because the KYC provider rejected the KYC data provided, or because the user aborted the KYC process.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_GENERIC_KYC_FAILED = 1038,
+
+
+ /**
+ * A fallback measure for a KYC operation failed. This is a bug. Users should contact the exchange operator.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_GENERIC_KYC_FALLBACK_FAILED = 1039,
+
+
+ /**
+ * The specified fallback measure for a KYC operation is unknown. This is a bug. Users should contact the exchange operator.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_GENERIC_KYC_FALLBACK_UNKNOWN = 1040,
+
+
+ /**
+ * The exchange is not aware of the bank account (payto URI or hash thereof) specified in the request and thus cannot perform the requested operation. The client should check that the select account is correct.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_GENERIC_BANK_ACCOUNT_UNKNOWN = 1041,
+
+
+ /**
* The exchange did not find information about the specified transaction in the database.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
* (A value of 0 indicates that the error is generated client-side).
@@ -1921,6 +1953,54 @@ export enum TalerErrorCode {
/**
+ * The KYC info access token is not recognized. Hence the request was denied.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_KYC_INFO_AUTHORIZATION_FAILED = 1919,
+
+
+ /**
+ * The exchange got stuck in a long series of (likely recursive) KYC rules without user-inputs that did not result in a timely conclusion. This is a configuration failure. Please contact the administrator.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_KYC_RECURSIVE_RULE_DETECTED = 1920,
+
+
+ /**
+ * The submitted KYC data lacks an attribute that is required by the KYC form. Please submit the complete form.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_KYC_AML_FORM_INCOMPLETE = 1921,
+
+
+ /**
+ * The request requires an AML program which is no longer configured at the exchange. Contact the exchange operator to address the configuration issue.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_KYC_GENERIC_AML_PROGRAM_GONE = 1922,
+
+
+ /**
+ * The given check is not of type 'form' and thus using this handler for form submission is incorrect.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_KYC_NOT_A_FORM = 1923,
+
+
+ /**
+ * The request requires a check which is no longer configured at the exchange. Contact the exchange operator to address the configuration issue.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_KYC_GENERIC_CHECK_GONE = 1924,
+
+
+ /**
* The signature affirming the wallet's KYC request was invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
* (A value of 0 indicates that the error is generated client-side).
@@ -2081,6 +2161,46 @@ export enum TalerErrorCode {
/**
+ * The AML program failed. This is either caused by a configuration change or a bug. Please contact technical support.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_KYC_AML_PROGRAM_FAILURE = 1945,
+
+
+ /**
+ * The AML program returned a malformed result. This is a bug. Please contact technical support.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_KYC_AML_PROGRAM_MALFORMED_RESULT = 1946,
+
+
+ /**
+ * The response from the KYC provider lacked required attributes. Please contact technical support.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_KYC_GENERIC_PROVIDER_INCOMPLETE_REPLY = 1947,
+
+
+ /**
+ * The context of the KYC check lacked required fields. This is a bug. Please contact technical support.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_KYC_GENERIC_PROVIDER_INCOMPLETE_CONTEXT = 1948,
+
+
+ /**
+ * The logic plugin had a bug in its AML processing. This is a bug. Please contact technical support.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_KYC_GENERIC_AML_LOGIC_BUG = 1949,
+
+
+ /**
* The exchange does not know a contract under the given contract public key.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
* (A value of 0 indicates that the error is generated client-side).
@@ -2337,6 +2457,14 @@ export enum TalerErrorCode {
/**
+ * The exchange specified in the operation is not trusted by this exchange. The client should limit its operation to exchanges enabled by the merchant, or ask the merchant to enable additional exchanges in the configuration.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_GENERIC_EXCHANGE_UNTRUSTED = 2025,
+
+
+ /**
* The exchange failed to provide a valid answer to the tracking request, thus those details are not in the response.
* Returned with an HTTP status code of #MHD_HTTP_OK (200).
* (A value of 0 indicates that the error is generated client-side).
@@ -2641,6 +2769,14 @@ export enum TalerErrorCode {
/**
+ * The payment violates a transaction limit configured at the given exchange. The wallet has a bug in that it failed to check exchange limits during coin selection. Please report the bug to your wallet developer.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_ORDERS_ID_PAY_EXCHANGE_TRANSACTION_LIMIT_VIOLATION = 2184,
+
+
+ /**
* The contract hash does not match the given order ID.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
* (A value of 0 indicates that the error is generated client-side).
@@ -2953,6 +3089,22 @@ export enum TalerErrorCode {
/**
+ * The refund amount would violate a refund transaction limit configured at the given exchange. Please find another way to refund the customer, and inquire with your legislator why they make strange banking regulations.
+ * Returned with an HTTP status code of #MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS (451).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_ORDERS_ID_REFUND_EXCHANGE_TRANSACTION_LIMIT_VIOLATION = 2512,
+
+
+ /**
+ * The total order amount exceeds hard legal transaction limits from the available exchanges, thus a customer could never legally make this payment. You may try to increase your limits by passing legitimization checks with exchange operators. You could also inquire with your legislator why the limits are prohibitively low for your business.
+ * Returned with an HTTP status code of #MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS (451).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_POST_ORDERS_AMOUNT_EXCEEDS_LEGAL_LIMITS = 2513,
+
+
+ /**
* The order provided to the backend could not be deleted, our offer is still valid and awaiting payment. Deletion may work later after the offer has expired if it remains unpaid.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
* (A value of 0 indicates that the error is generated client-side).
@@ -4201,6 +4353,14 @@ export enum TalerErrorCode {
/**
+ * The merchant needs to do KYC first, the payment could not be completed.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_PAY_MERCHANT_KYC_MISSING = 7040,
+
+
+ /**
* We encountered a timeout with our payment backend.
* Returned with an HTTP status code of #MHD_HTTP_GATEWAY_TIMEOUT (504).
* (A value of 0 indicates that the error is generated client-side).
diff --git a/packages/taler-util/src/types-taler-wallet-transactions.ts b/packages/taler-util/src/types-taler-wallet-transactions.ts
@@ -219,6 +219,10 @@ export interface TransactionCommon {
error?: TalerErrorDetail;
+ abortReason?: TalerErrorDetail;
+
+ failReason?: TalerErrorDetail;
+
/**
* If the transaction minor state is in KycRequired this field is going to
* have the location where the user need to go to complete KYC information.
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts
@@ -1287,6 +1287,8 @@ export interface PurchaseRecord {
purchaseStatus: PurchaseStatus;
+ abortReason?: TalerErrorDetail;
+
/**
* Private key for the nonce.
*/
diff --git a/packages/taler-wallet-core/src/pay-merchant.ts b/packages/taler-wallet-core/src/pay-merchant.ts
@@ -58,6 +58,7 @@ import {
Logger,
makeErrorDetail,
makePendingOperationFailedError,
+ makeTalerErrorDetail,
MerchantCoinRefundStatus,
MerchantContractTerms,
MerchantPayResponse,
@@ -302,6 +303,7 @@ export class PayMerchantTransactionContext implements TransactionContext {
proposalId: purchaseRec.proposalId,
}),
proposalId: purchaseRec.proposalId,
+ abortReason: purchaseRec.abortReason,
info,
refundQueryActive:
purchaseRec.purchaseStatus === PurchaseStatus.PendingQueryingRefund,
@@ -422,7 +424,7 @@ export class PayMerchantTransactionContext implements TransactionContext {
notifyTransition(wex, transactionId, transitionInfo);
}
- async abortTransaction(): Promise<void> {
+ async abortTransaction(reason?: TalerErrorDetail): Promise<void> {
const { wex, proposalId, transactionId } = this;
const transitionInfo = await wex.db.runReadWriteTx(
{
@@ -450,6 +452,7 @@ export class PayMerchantTransactionContext implements TransactionContext {
return;
case PurchaseStatus.PendingPaying:
case PurchaseStatus.SuspendedPaying: {
+ purchase.abortReason = reason;
purchase.purchaseStatus = PurchaseStatus.AbortingWithRefund;
if (purchase.payInfo && purchase.payInfo.payCoinSelection) {
const coinSel = purchase.payInfo.payCoinSelection;
@@ -2679,6 +2682,16 @@ async function processPurchasePay(
}
}
+ if (resp.status === HttpStatusCode.UnavailableForLegalReasons) {
+ logger.warn(`pay transaction aborted, merchant has KYC problems`);
+ await ctx.abortTransaction(
+ makeTalerErrorDetail(TalerErrorCode.WALLET_PAY_MERCHANT_KYC_MISSING, {
+ exchangeResponse: await resp.json(),
+ }),
+ );
+ return TaskRunResult.progress();
+ }
+
if (resp.status >= 400 && resp.status <= 499) {
logger.trace("got generic 4xx from merchant");
const err = await readTalerErrorResponse(resp);