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:
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,