commit a0e9cc4b58c55499abd0c2a85bbe7c2cc47abb1e
parent 71245575d8da533607fe1ce3c94d4198b176483a
Author: Antoine A <>
Date: Thu, 16 Jan 2025 15:52:13 +0100
bank: new BANK_UPDATE_ABORT_CONFLICT error when selecting aborted withdrawal
Diffstat:
5 files changed, 65 insertions(+), 23 deletions(-)
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/api/BankIntegrationApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/api/BankIntegrationApi.kt
@@ -1,6 +1,6 @@
/*
* This file is part of LibEuFin.
- * Copyright (C) 2024 Taler Systems S.A.
+ * Copyright (C) 2024-2025 Taler Systems S.A.
* LibEuFin is free software; you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@@ -108,6 +108,10 @@ fun Routing.bankIntegrationApi(db: Database, ctx: BankConfig) {
"Amount either to high or too low",
TalerErrorCode.BANK_UNALLOWED_DEBIT
)
+ WithdrawalSelectionResult.AlreadyAborted -> throw conflict(
+ "Cannot update an aborted withdrawal",
+ TalerErrorCode.BANK_UPDATE_ABORT_CONFLICT
+ )
is WithdrawalSelectionResult.Success -> {
call.respond(BankWithdrawalOperationPostResponse(
transfer_done = res.status == WithdrawalStatus.confirmed,
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/WithdrawalDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/WithdrawalDAO.kt
@@ -135,6 +135,7 @@ class WithdrawalDAO(private val db: Database) {
data object AmountDiffers: WithdrawalSelectionResult
data object BalanceInsufficient: WithdrawalSelectionResult
data object BadAmount: WithdrawalSelectionResult
+ data object AlreadyAborted: WithdrawalSelectionResult
}
/** Set details ([exchangePayto] & [reservePub] & [amount]) for withdrawal operation [uuid] */
@@ -157,7 +158,8 @@ class WithdrawalDAO(private val db: Database) {
out_status,
out_amount_differs,
out_balance_insufficient,
- out_bad_amount
+ out_bad_amount,
+ out_aborted
FROM select_taler_withdrawal(
?, ?, ?, ?,
${if (amount != null) "(?, ?)::taler_amount" else "NULL"},
@@ -183,6 +185,7 @@ class WithdrawalDAO(private val db: Database) {
setInt(id+5, maxAmount.frac)
one {
when {
+ it.getBoolean("out_aborted") -> WithdrawalSelectionResult.AlreadyAborted
it.getBoolean("out_balance_insufficient") -> WithdrawalSelectionResult.BalanceInsufficient
it.getBoolean("out_bad_amount") -> WithdrawalSelectionResult.BadAmount
it.getBoolean("out_no_op") -> WithdrawalSelectionResult.UnknownOperation
diff --git a/bank/src/test/kotlin/BankIntegrationApiTest.kt b/bank/src/test/kotlin/BankIntegrationApiTest.kt
@@ -170,6 +170,22 @@ class BankIntegrationApiTest {
}.assertOkJson<BankWithdrawalOperationPostResponse>()
}
+ // Check select aborted
+ client.postA("/accounts/merchant/withdrawals") {
+ json { "amount" to "KUDOS:1" }
+ }.assertOkJson<BankAccountCreateWithdrawalResponse> {
+ val uuid = it.withdrawal_id
+ client.postA("/accounts/merchant/withdrawals/$uuid/abort").assertNoContent()
+
+ // Check error
+ client.postA("/taler-integration/withdrawal-operation/$uuid") {
+ json {
+ "reserve_pub" to EddsaPublicKey.rand()
+ "selected_exchange" to exchangePayto.canonical
+ }
+ }.assertConflict(TalerErrorCode.BANK_UPDATE_ABORT_CONFLICT)
+ }
+
client.postA("/accounts/merchant/withdrawals") {
json {}
}.assertOkJson<BankAccountCreateWithdrawalResponse> {
diff --git a/common/src/main/kotlin/TalerErrorCode.kt b/common/src/main/kotlin/TalerErrorCode.kt
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- Copyright (C) 2012-2024 Taler Systems SA
+ Copyright (C) 2012-2025 Taler Systems SA
GNU Taler is free software: you can redistribute it and/or modify it
under the terms of the GNU Lesser General Public License as published
@@ -285,6 +285,12 @@ enum class TalerErrorCode(val code: Int, val status: Int, val description: Strin
/** 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. */
EXCHANGE_GENERIC_BANK_ACCOUNT_UNKNOWN(1041, 404, "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."),
+ /** The AML processing at the exchange did not terminate in an adequate timeframe. This is likely a configuration problem at the payment service provider. Users should contact the exchange operator. */
+ EXCHANGE_GENERIC_AML_PROGRAM_RECURSION_DETECTED(1042, 500, "The AML processing at the exchange did not terminate in an adequate timeframe. This is likely a configuration problem at the payment service provider. Users should contact the exchange operator."),
+
+ /** A check against sanction lists failed. This is indicative of an internal error in the sanction list processing logic. This needs to be investigated by the exchange operator. */
+ EXCHANGE_GENERIC_KYC_SANCTION_LIST_CHECK_FAILED(1043, 500, "A check against sanction lists failed. This is indicative of an internal error in the sanction list processing logic. This needs to be investigated by the exchange operator."),
+
/** The exchange did not find information about the specified transaction in the database. */
EXCHANGE_DEPOSITS_GET_NOT_FOUND(1100, 404, "The exchange did not find information about the specified transaction in the database."),
@@ -312,8 +318,8 @@ enum class TalerErrorCode(val code: Int, val status: Int, val description: Strin
/** The given reserve does not have sufficient funds to admit the requested withdraw operation at this time. The response includes the current \"balance\" of the reserve as well as the transaction \"history\" that lead to this balance. */
EXCHANGE_WITHDRAW_INSUFFICIENT_FUNDS(1150, 409, "The given reserve does not have sufficient funds to admit the requested withdraw operation at this time. The response includes the current \"balance\" of the reserve as well as the transaction \"history\" that lead to this balance."),
- /** The given reserve does not have sufficient funds to admit the requested age-withdraw operation at this time. The response includes the current \"balance\" of the reserve as well as the transaction \"history\" that lead to this balance. */
- EXCHANGE_AGE_WITHDRAW_INSUFFICIENT_FUNDS(1151, 409, "The given reserve does not have sufficient funds to admit the requested age-withdraw operation at this time. The response includes the current \"balance\" of the reserve as well as the transaction \"history\" that lead to this balance."),
+ /** The amount to withdraw or the fees exceeds the numeric range for Taler amounts. This is not a client failure, as coin values and fees come from the exchange's configuration. */
+ EXCHANGE_WITHDRAW_AMOUNT_OVERFLOW(1151, 500, "The amount to withdraw or the fees exceeds the numeric range for Taler amounts. This is not a client failure, as coin values and fees come from the exchange's configuration."),
/** The amount to withdraw together with the fee exceeds the numeric range for Taler amounts. This is not a client failure, as the coin value and fees come from the exchange's configuration. */
EXCHANGE_WITHDRAW_AMOUNT_FEE_OVERFLOW(1152, 500, "The amount to withdraw together with the fee exceeds the numeric range for Taler amounts. This is not a client failure, as the coin value and fees come from the exchange's configuration."),
@@ -339,23 +345,17 @@ enum class TalerErrorCode(val code: Int, val status: Int, val description: Strin
/** The client re-used a withdraw nonce, which is not allowed. */
EXCHANGE_WITHDRAW_NONCE_REUSE(1160, 409, "The client re-used a withdraw nonce, which is not allowed."),
- /** The client provided an unknown commitment for an age-withdraw request. */
- EXCHANGE_AGE_WITHDRAW_COMMITMENT_UNKNOWN(1161, 400, "The client provided an unknown commitment for an age-withdraw request."),
-
- /** The total sum of amounts from the denominations did overflow. */
- EXCHANGE_AGE_WITHDRAW_AMOUNT_OVERFLOW(1162, 500, "The total sum of amounts from the denominations did overflow."),
-
- /** The total sum of value and fees from the denominations differs from the committed amount with fees. */
- EXCHANGE_AGE_WITHDRAW_AMOUNT_INCORRECT(1163, 400, "The total sum of value and fees from the denominations differs from the committed amount with fees."),
+ /** The client provided an unknown commitment for a withdraw request. */
+ EXCHANGE_WITHDRAW_COMMITMENT_UNKNOWN(1161, 400, "The client provided an unknown commitment for a withdraw request."),
/** The original commitment differs from the calculated hash */
- EXCHANGE_AGE_WITHDRAW_REVEAL_INVALID_HASH(1164, 400, "The original commitment differs from the calculated hash"),
+ EXCHANGE_WITHDRAW_REVEAL_INVALID_HASH(1164, 400, "The original commitment differs from the calculated hash"),
/** The maximum age in the commitment is too large for the reserve */
- EXCHANGE_AGE_WITHDRAW_MAXIMUM_AGE_TOO_LARGE(1165, 409, "The maximum age in the commitment is too large for the reserve"),
+ EXCHANGE_WITHDRAW_MAXIMUM_AGE_TOO_LARGE(1165, 409, "The maximum age in the commitment is too large for the reserve"),
- /** The batch withdraw included a planchet that was already withdrawn. This is not allowed. */
- EXCHANGE_WITHDRAW_BATCH_IDEMPOTENT_PLANCHET(1175, 409, "The batch withdraw included a planchet that was already withdrawn. This is not allowed."),
+ /** The withdraw included a planchet that was already withdrawn. This is not allowed. */
+ EXCHANGE_WITHDRAW_IDEMPOTENT_PLANCHET(1175, 409, "The withdraw included a planchet that was already withdrawn. This is not allowed."),
/** The signature made by the coin over the deposit permission is not valid. */
EXCHANGE_DEPOSIT_COIN_SIGNATURE_INVALID(1205, 403, "The signature made by the coin over the deposit permission is not valid."),
@@ -684,6 +684,12 @@ enum class TalerErrorCode(val code: Int, val status: Int, val description: Strin
/** A more recent decision about the AML officer status is known to the exchange. */
EXCHANGE_MANAGEMENT_AML_OFFICERS_MORE_RECENT_PRESENT(1831, 409, "A more recent decision about the AML officer status is known to the exchange."),
+ /** The exchange already has this denomination key configured, but with different meta data. This should not be possible, contact the developers for support. */
+ EXCHANGE_MANAGEMENT_CONFLICTING_DENOMINATION_META_DATA(1832, 409, "The exchange already has this denomination key configured, but with different meta data. This should not be possible, contact the developers for support."),
+
+ /** The exchange already has this signing key configured, but with different meta data. This should not be possible, contact the developers for support. */
+ EXCHANGE_MANAGEMENT_CONFLICTING_SIGNKEY_META_DATA(1833, 409, "The exchange already has this signing key configured, but with different meta data. This should not be possible, contact the developers for support."),
+
/** The purse was previously created with different meta data. */
EXCHANGE_PURSE_CREATE_CONFLICTING_META_DATA(1850, 409, "The purse was previously created with different meta data."),
@@ -747,6 +753,9 @@ enum class TalerErrorCode(val code: Int, val status: Int, val description: Strin
/** The auditor that was specified is no longer used by this exchange. */
EXCHANGE_AUDITORS_AUDITOR_INACTIVE(1902, 410, "The auditor that was specified is no longer used by this exchange."),
+ /** The exchange tried to run an AML program, but that program did not terminate on time. Contact the exchange operator to address the AML program bug or performance issue. If it is not a performance issue, the timeout might have to be increased (requires changes to the source code). */
+ EXCHANGE_KYC_GENERIC_AML_PROGRAM_TIMEOUT(1918, 500, "The exchange tried to run an AML program, but that program did not terminate on time. Contact the exchange operator to address the AML program bug or performance issue. If it is not a performance issue, the timeout might have to be increased (requires changes to the source code)."),
+
/** The KYC info access token is not recognized. Hence the request was denied. */
EXCHANGE_KYC_INFO_AUTHORIZATION_FAILED(1919, 403, "The KYC info access token is not recognized. Hence the request was denied."),
@@ -942,6 +951,12 @@ enum class TalerErrorCode(val code: Int, val status: Int, val description: Strin
/** 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. */
MERCHANT_GENERIC_EXCHANGE_UNTRUSTED(2025, 400, "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."),
+ /** The token family is not known to the backend. */
+ MERCHANT_GENERIC_TOKEN_FAMILY_UNKNOWN(2026, 404, "The token family is not known to the backend."),
+
+ /** The token family key is not known to the backend. Check the local system time on the client, maybe an expired (or not yet valid) token was used. */
+ MERCHANT_GENERIC_TOKEN_KEY_UNKNOWN(2027, 404, "The token family key is not known to the backend. Check the local system time on the client, maybe an expired (or not yet valid) token was used."),
+
/** The exchange failed to provide a valid answer to the tracking request, thus those details are not in the response. */
MERCHANT_GET_ORDERS_EXCHANGE_TRACKING_FAILURE(2100, 200, "The exchange failed to provide a valid answer to the tracking request, thus those details are not in the response."),
@@ -1167,8 +1182,8 @@ enum class TalerErrorCode(val code: Int, val status: Int, val description: Strin
/** The order creation request is invalid because the given refund deadline is in the past. */
MERCHANT_PRIVATE_POST_ORDERS_REFUND_DEADLINE_IN_PAST(2508, 400, "The order creation request is invalid because the given refund deadline is in the past."),
- /** The backend does not trust any exchange that would allow funds to be wired to any bank account of this instance using the wire method specified with the order. Note that right now, we do not support the use of exchange bank accounts with mandatory currency conversion. */
- MERCHANT_PRIVATE_POST_ORDERS_NO_EXCHANGES_FOR_WIRE_METHOD(2509, 409, "The backend does not trust any exchange that would allow funds to be wired to any bank account of this instance using the wire method specified with the order. Note that right now, we do not support the use of exchange bank accounts with mandatory currency conversion."),
+ /** The backend does not trust any exchange that would allow funds to be wired to any bank account of this instance using the wire method specified with the order. (Note that right now, we do not support the use of exchange bank accounts with mandatory currency conversion.) One likely cause for this is that the taler-merchant-exchangekeyupdate process is not running. */
+ MERCHANT_PRIVATE_POST_ORDERS_NO_EXCHANGES_FOR_WIRE_METHOD(2509, 409, "The backend does not trust any exchange that would allow funds to be wired to any bank account of this instance using the wire method specified with the order. (Note that right now, we do not support the use of exchange bank accounts with mandatory currency conversion.) One likely cause for this is that the taler-merchant-exchangekeyupdate process is not running."),
/** One of the paths to forget is malformed. */
MERCHANT_PRIVATE_PATCH_ORDERS_ID_FORGET_PATH_SYNTAX_INCORRECT(2510, 400, "One of the paths to forget is malformed."),
@@ -1494,6 +1509,9 @@ enum class TalerErrorCode(val code: Int, val status: Int, val description: Strin
/** Bank account is locked and cannot authenticate using his password. */
BANK_ACCOUNT_LOCKED(5152, 403, "Bank account is locked and cannot authenticate using his password."),
+ /** The client attempted to update a transaction' details that was already aborted. */
+ BANK_UPDATE_ABORT_CONFLICT(5153, 409, "The client attempted to update a transaction' details that was already aborted."),
+
/** The sync service failed find the account in its database. */
SYNC_ACCOUNT_UNKNOWN(6100, 404, "The sync service failed find the account in its database."),
diff --git a/database-versioning/libeufin-bank-procedures.sql b/database-versioning/libeufin-bank-procedures.sql
@@ -1044,6 +1044,7 @@ CREATE FUNCTION select_taler_withdrawal(
OUT out_amount_differs BOOLEAN,
OUT out_balance_insufficient BOOLEAN,
OUT out_bad_amount BOOLEAN,
+ OUT out_aborted BOOLEAN,
-- Success return
OUT out_status TEXT
)
@@ -1056,20 +1057,20 @@ BEGIN
-- Check for conflict and idempotence
SELECT
NOT selection_done,
+ aborted,
CASE
WHEN confirmation_done THEN 'confirmed'
- WHEN aborted THEN 'aborted'
ELSE 'selected'
END,
selection_done
AND (selected_exchange_payto != in_selected_exchange_payto OR reserve_pub != in_reserve_pub OR amount != in_amount),
amount != in_amount,
wallet_bank_account
- INTO not_selected, out_status, out_already_selected, out_amount_differs, account_id
+ INTO not_selected, out_aborted, out_status, out_already_selected, out_amount_differs, account_id
FROM taler_withdrawal_operations
WHERE withdrawal_uuid=in_withdrawal_uuid;
-IF NOT FOUND OR out_already_selected OR out_amount_differs THEN
- out_no_op=NOT FOUND;
+out_no_op = NOT FOUND;
+IF out_no_op OR out_aborted OR out_already_selected OR out_amount_differs THEN
RETURN;
END IF;