diff --git a/packages/wallet-webui/src/ui/error.ts b/packages/wallet-webui/src/ui/error.ts index 1450c2531..023c27ac5 100644 --- a/packages/wallet-webui/src/ui/error.ts +++ b/packages/wallet-webui/src/ui/error.ts @@ -19,7 +19,28 @@ export function localError(message: string): ErrorPresentation { return { message }; } +function insufficientBalanceDetails( + detail: TalerErrorDetail, +): PaymentInsufficientBalanceDetails | undefined { + return ( + detail as { + insufficientBalanceDetails?: PaymentInsufficientBalanceDetails; + } + ).insufficientBalanceDetails; +} + +/** A deposit refused because no exchange can wire to this kind of account. */ +function depositTargetUnsupported(detail: TalerErrorDetail): boolean { + return ( + detail.code === TalerErrorCode.WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE && + insufficientBalanceDetails(detail)?.causeHint === + InsufficientBalanceHint.MerchantDepositInsufficient + ); +} + function errorMessage(detail: TalerErrorDetail, fallback: string): string { + if (depositTargetUnsupported(detail)) + return /* Translators: A deposit sends digital cash from this wallet to an external account, usually a bank account; it does not add funds to the wallet. */ i18n.str`The exchanges of this wallet cannot deposit to this type of bank account.`; switch (detail.code) { case TalerErrorCode.WALLET_ORDER_ALREADY_CLAIMED: return /* Translators: Claiming a merchant order assigns it to a wallet for payment; it does not mean that payment has completed. */ i18n.str`This order was already claimed by another wallet.`; @@ -28,13 +49,10 @@ function errorMessage(detail: TalerErrorDetail, fallback: string): string { case TalerErrorCode.WALLET_MERCHANT_ORDER_NOT_FOUND: return i18n.str`The merchant order no longer exists or has expired.`; case TalerErrorCode.WALLET_PAY_MERCHANT_INSUFFICIENT_BALANCE: - return insufficientBalanceMessage( - ( - detail as { - insufficientBalanceDetails?: PaymentInsufficientBalanceDetails; - } - ).insufficientBalanceDetails, - ); + case TalerErrorCode.WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE: + case TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE: + case TalerErrorCode.WALLET_PEER_PULL_PAYMENT_INSUFFICIENT_BALANCE: + return insufficientBalanceMessage(insufficientBalanceDetails(detail)); case TalerErrorCode.WALLET_NETWORK_ERROR: return i18n.str`The wallet could not reach the service.`; case TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT: @@ -79,6 +97,7 @@ function errorMessage(detail: TalerErrorDetail, fallback: string): string { } function errorGuidance(detail: TalerErrorDetail): string | undefined { + if (depositTargetUnsupported(detail)) return undefined; switch (detail.code) { case TalerErrorCode.WALLET_ORDER_ALREADY_CLAIMED: return i18n.str`Return to the merchant and request a new order.`; @@ -87,6 +106,9 @@ function errorGuidance(detail: TalerErrorDetail): string | undefined { case TalerErrorCode.WALLET_MERCHANT_ORDER_NOT_FOUND: return i18n.str`Return to the merchant and create a new order.`; case TalerErrorCode.WALLET_PAY_MERCHANT_INSUFFICIENT_BALANCE: + case TalerErrorCode.WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE: + case TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE: + case TalerErrorCode.WALLET_PEER_PULL_PAYMENT_INSUFFICIENT_BALANCE: return /* Translators: Withdrawing adds digital cash to this wallet, usually funded by a bank transfer; it does not cash out the wallet to a bank account. */ i18n.str`Withdraw funds or choose another payment option, then try again.`; case TalerErrorCode.WALLET_NETWORK_ERROR: case TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT: diff --git a/packages/wallet-webui/test/error.test.ts b/packages/wallet-webui/test/error.test.ts index ec1288885..cc418b5b0 100644 --- a/packages/wallet-webui/test/error.test.ts +++ b/packages/wallet-webui/test/error.test.ts @@ -1,6 +1,11 @@ import assert from "node:assert/strict"; import test from "node:test"; -import { errorFromException } from "../src/ui/error.js"; +import { + AbsoluteTime, + InsufficientBalanceHint, + TalerErrorCode, +} from "@gnu-taler/taler-util"; +import { errorFromException, walletCoreError } from "../src/ui/error.js"; test("generic exceptions do not replace a translated user-facing fallback", () => { const error = errorFromException( @@ -9,3 +14,49 @@ test("generic exceptions do not replace a translated user-facing fallback", () = ); assert.equal(error.message, "Operation failed"); }); + +test("deposit and peer-payment shortfalls are explained like merchant payments", () => { + for (const code of [ + TalerErrorCode.WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE, + TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE, + TalerErrorCode.WALLET_PEER_PULL_PAYMENT_INSUFFICIENT_BALANCE, + ]) { + const error = walletCoreError( + { + code, + when: AbsoluteTime.now(), + insufficientBalanceDetails: { + amountRequested: "KUDOS:50", + balanceAvailable: "KUDOS:20", + causeHint: InsufficientBalanceHint.WalletBalanceAvailableInsufficient, + }, + }, + "Operation failed", + ); + assert.equal( + error.message, + "The wallet has KUDOS:20 available, which is not enough for KUDOS:50.", + ); + assert.ok(error.guidance); + } +}); + +test("a deposit to an account type no exchange serves is not called a shortfall", () => { + const error = walletCoreError( + { + code: TalerErrorCode.WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE, + when: AbsoluteTime.now(), + insufficientBalanceDetails: { + amountRequested: "KUDOS:1", + balanceAvailable: "KUDOS:20", + causeHint: InsufficientBalanceHint.MerchantDepositInsufficient, + }, + }, + "Operation failed", + ); + assert.equal( + error.message, + "The exchanges of this wallet cannot deposit to this type of bank account.", + ); + assert.equal(error.guidance, undefined); +});