commit 2dc0f4f8c9104cc296613cf3abba876f35055f7e
parent 2ad56046aad4b172e6e75bdfb7d88c8bb27cdfc5
Author: Florian Dold <dold@taler.net>
Date: Thu, 20 Aug 2026 19:06:47 +0200
wallet-core: wait for payment session replay
Diffstat:
2 files changed, 51 insertions(+), 12 deletions(-)
diff --git a/packages/taler-wallet-core/src/pay-merchant.test.ts b/packages/taler-wallet-core/src/pay-merchant.test.ts
@@ -23,7 +23,9 @@ import {
import assert from "node:assert";
import { test } from "node:test";
import {
+ PurchaseStatus,
RefundGroupStatus,
+ WalletPurchase,
WalletRefundGroup,
WalletToken,
} from "./db-common.js";
@@ -31,6 +33,7 @@ import { WalletDbTransaction } from "./dbtx.js";
import {
getCoinsToSpendForMerchantRepair,
getRefundTotals,
+ isPaymentSessionComplete,
releasePaymentTokensInTx,
setRefundGroupEffectiveAmount,
splitPaymentOutputTokenSignatures,
@@ -205,3 +208,22 @@ test("claimed contract terms are bound to the requested order and nonce", () =>
/wallet nonce/,
);
});
+
+test("payment replay waits for the requested session to finish", () => {
+ const purchase = {
+ timestampFirstSuccessfulPay: 1,
+ lastSessionId: "new-session",
+ purchaseStatus: PurchaseStatus.PendingPayingReplay,
+ } as Pick<
+ WalletPurchase,
+ "timestampFirstSuccessfulPay" | "lastSessionId" | "purchaseStatus"
+ >;
+
+ assert.strictEqual(
+ isPaymentSessionComplete(purchase, "new-session"),
+ false,
+ );
+ purchase.purchaseStatus = PurchaseStatus.Done;
+ assert.strictEqual(isPaymentSessionComplete(purchase, "new-session"), true);
+ assert.strictEqual(isPaymentSessionComplete(purchase, "other-session"), false);
+});
diff --git a/packages/taler-wallet-core/src/pay-merchant.ts b/packages/taler-wallet-core/src/pay-merchant.ts
@@ -2338,17 +2338,12 @@ async function waitPaymentResult(
const d = await expectProposalDownload(ctx.wex, purchase);
- if (txRes.purchase.timestampFirstSuccessfulPay) {
- if (
- waitSessionId == null ||
- txRes.purchase.lastSessionId === waitSessionId
- ) {
- return {
- type: ConfirmPayResultType.Done,
- contractTerms: d.contractTermsRaw,
- transactionId: ctx.transactionId,
- };
- }
+ if (isPaymentSessionComplete(txRes.purchase, waitSessionId)) {
+ return {
+ type: ConfirmPayResultType.Done,
+ contractTerms: d.contractTermsRaw,
+ transactionId: ctx.transactionId,
+ };
}
if (txRes.retryRecord && txRes.retryRecord.retryInfo.retryCounter > 0) {
@@ -2391,6 +2386,28 @@ async function waitPaymentResult(
});
}
+export function isPaymentSessionComplete(
+ purchase: Pick<
+ WalletPurchase,
+ "timestampFirstSuccessfulPay" | "lastSessionId" | "purchaseStatus"
+ >,
+ waitSessionId?: string,
+): boolean {
+ if (!purchase.timestampFirstSuccessfulPay) {
+ return false;
+ }
+ if (waitSessionId == null) {
+ return true;
+ }
+ if (purchase.lastSessionId !== waitSessionId) {
+ return false;
+ }
+ return ![
+ PurchaseStatus.PendingPayingReplay,
+ PurchaseStatus.SuspendedPayingReplay,
+ ].includes(purchase.purchaseStatus);
+}
+
export async function getChoicesForPayment(
wex: WalletExecutionContext,
transactionId: string,
@@ -2768,7 +2785,7 @@ export async function confirmPay(
lastError: undefined,
};
}
- return waitPaymentResult(wex, proposalId);
+ return waitPaymentResult(wex, proposalId, sessionIdOverride);
}
logger.trace("confirmPay: purchase record does not exist yet");