taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit 7215c15997c5da134d22cdca0188cccf4f672330
parent 36d437f59772497b562bd34f3c5d34259d282f0b
Author: Florian Dold <dold@taler.net>
Date:   Thu,  3 Sep 2026 15:41:17 +0200

wallet-core: honour the bank's answer when aborting a withdrawal

A 409 means the user confirmed at the bank meanwhile and the reserve
is being funded, so the withdrawal is suspended rather than abandoned.

Diffstat:
Mpackages/taler-wallet-core/src/withdraw.test.ts | 16++++++++++++++++
Mpackages/taler-wallet-core/src/withdraw.ts | 35++++++++++++++++++++++++++++++++---
2 files changed, 48 insertions(+), 3 deletions(-)

diff --git a/packages/taler-wallet-core/src/withdraw.test.ts b/packages/taler-wallet-core/src/withdraw.test.ts @@ -38,8 +38,24 @@ import { computeWithdrawalTransactionStatus, isWithdrawalFinalStatus, requireWithdrawalBatchCardinality, + withdrawalStatusAfterBankAbort, } from "./withdraw.js"; +test("a bank abort answered with 409 keeps the withdrawal resumable", () => { + assert.strictEqual( + withdrawalStatusAfterBankAbort("ok"), + WithdrawalGroupStatus.AbortedBank, + ); + assert.strictEqual( + withdrawalStatusAfterBankAbort(HttpStatusCode.Conflict), + WithdrawalGroupStatus.SuspendedQueryingStatus, + ); + assert.strictEqual( + withdrawalStatusAfterBankAbort(HttpStatusCode.NotFound), + WithdrawalGroupStatus.FailedBankAborted, + ); +}); + test("every terminal withdrawal status ends final-state waits", () => { for (const status of [ WithdrawalGroupStatus.Done, diff --git a/packages/taler-wallet-core/src/withdraw.ts b/packages/taler-wallet-core/src/withdraw.ts @@ -2457,20 +2457,49 @@ async function processWithdrawalGroupAbortingBank( uriResult.withdrawalOperationId, ); logger.info(`abort response status: ${abortResp.response.status}`); + const newStatus = withdrawalStatusAfterBankAbort(abortResp.case); await ctx.wex.runWalletDbTx(async (tx) => { const [wg, h] = await ctx.getRecordHandle(tx); - if (!wg) { + if (!wg || wg.status !== WithdrawalGroupStatus.AbortingBank) { return; } - wg.status = WithdrawalGroupStatus.AbortedBank; - wg.timestampFinish = timestampPreciseToDb(TalerPreciseTimestamp.now()); + wg.status = newStatus; + if (newStatus !== WithdrawalGroupStatus.SuspendedQueryingStatus) { + wg.timestampFinish = timestampPreciseToDb(TalerPreciseTimestamp.now()); + } await h.update(wg, "aborting-bank"); }); return TaskRunResult.finished(); } +/** + * Where a withdrawal goes after the bank answered the abort request. + * + * A 409 means the user confirmed the operation at the bank in the meantime, + * so the money is on its way to the reserve; the abort becomes a suspension + * from which the withdrawal can be resumed. A 404 means the bank never + * knew the operation. + */ +export function withdrawalStatusAfterBankAbort( + abortCase: "ok" | HttpStatusCode.NotFound | HttpStatusCode.Conflict, +): + | WithdrawalGroupStatus.AbortedBank + | WithdrawalGroupStatus.SuspendedQueryingStatus + | WithdrawalGroupStatus.FailedBankAborted { + switch (abortCase) { + case "ok": + return WithdrawalGroupStatus.AbortedBank; + case HttpStatusCode.Conflict: + return WithdrawalGroupStatus.SuspendedQueryingStatus; + case HttpStatusCode.NotFound: + return WithdrawalGroupStatus.FailedBankAborted; + default: + assertUnreachable(abortCase); + } +} + async function processWithdrawalGroupPendingKyc( wex: WalletExecutionContext, withdrawalGroup: WalletWithdrawalGroup,