taler-typescript-core

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

commit 762e764bc58f789642af156179c2c6be8bfcd9a4
parent 702b552cb63f09053c8353d71bde69436437096d
Author: Florian Dold <florian@dold.me>
Date:   Thu, 12 Mar 2026 14:02:59 +0100

wallet-core: improve handling of refresh in pay aborts

Diffstat:
Mpackages/taler-wallet-core/src/db.ts | 6++++++
Mpackages/taler-wallet-core/src/pay-merchant.ts | 64++++++++++++++++++++++++++++++++++++++++------------------------
2 files changed, 46 insertions(+), 24 deletions(-)

diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts @@ -1555,6 +1555,12 @@ export interface PurchaseRecord { purchaseStatus: PurchaseStatus; + /** + * Refresh group ID of the refresh transaction that + * has been created to abort the payment. + */ + abortRefreshGroupId?: string; + abortReason?: TalerErrorDetail; failReason?: TalerErrorDetail; diff --git a/packages/taler-wallet-core/src/pay-merchant.ts b/packages/taler-wallet-core/src/pay-merchant.ts @@ -476,25 +476,6 @@ export class PayMerchantTransactionContext implements TransactionContext { case PurchaseStatus.SuspendedPaying: { purchase.abortReason = reason; purchase.purchaseStatus = PurchaseStatus.AbortingWithRefund; - if (purchase.payInfo && purchase.payInfo.payCoinSelection) { - const coinSel = purchase.payInfo.payCoinSelection; - const currency = Amounts.currencyOf(purchase.payInfo.totalPayCost); - const refreshCoins: CoinRefreshRequest[] = []; - for (let i = 0; i < coinSel.coinPubs.length; i++) { - refreshCoins.push({ - amount: coinSel.coinContributions[i], - coinPub: coinSel.coinPubs[i], - }); - } - await createRefreshGroup( - wex, - tx, - currency, - refreshCoins, - RefreshReason.AbortPay, - this.transactionId, - ); - } break; } case PurchaseStatus.PendingQueryingAutoRefund: @@ -4422,9 +4403,9 @@ async function processPurchaseAbortingRefund( purchase: PurchaseRecord, ): Promise<TaskRunResult> { const proposalId = purchase.proposalId; + const ctx = new PayMerchantTransactionContext(wex, proposalId); const download = await expectProposalDownload(wex, purchase); logger.trace(`processing aborting-refund for proposal ${proposalId}`); - const requestUrl = new URL( `orders/${download.contractTerms.order_id}/abort`, download.contractTerms.merchant_base_url, @@ -4434,10 +4415,47 @@ async function processPurchaseAbortingRefund( const payCoinSelection = purchase.payInfo?.payCoinSelection; if (!payCoinSelection) { - throw Error("can't abort, no coins selected"); + await wex.db.runAllStoresReadWriteTx({}, async (tx) => { + const [rec, h] = await ctx.getRecordHandle(tx); + if (rec?.purchaseStatus !== PurchaseStatus.AbortingWithRefund) { + return; + } + if (purchase.payInfo?.payCoinSelection != null) { + return; + } + rec.purchaseStatus = PurchaseStatus.AbortedOrderDeleted; + await h.update(rec); + }); + return TaskRunResult.finished(); } - await wex.db.runReadOnlyTx({ storeNames: ["coins"] }, async (tx) => { + await wex.db.runAllStoresReadWriteTx({}, async (tx) => { + const [rec, h] = await ctx.getRecordHandle(tx); + if (!rec) { + return; + } + if (rec.payInfo?.payCoinSelection && rec.abortRefreshGroupId == null) { + const coinSel = rec.payInfo.payCoinSelection; + const currency = Amounts.currencyOf(rec.payInfo.totalPayCost); + const refreshCoins: CoinRefreshRequest[] = []; + for (let i = 0; i < coinSel.coinPubs.length; i++) { + refreshCoins.push({ + amount: coinSel.coinContributions[i], + coinPub: coinSel.coinPubs[i], + }); + } + const res = await createRefreshGroup( + wex, + tx, + currency, + refreshCoins, + RefreshReason.AbortPay, + ctx.transactionId, + ); + rec.abortRefreshGroupId = res.refreshGroupId; + await h.update(rec); + } + for (let i = 0; i < payCoinSelection.coinPubs.length; i++) { const coinPub = payCoinSelection.coinPubs[i]; const coin = await tx.coins.get(coinPub); @@ -4469,7 +4487,6 @@ async function processPurchaseAbortingRefund( err.code === TalerErrorCode.MERCHANT_POST_ORDERS_ID_ABORT_CONTRACT_NOT_FOUND ) { - const ctx = new PayMerchantTransactionContext(wex, proposalId); await wex.db.runAllStoresReadWriteTx({}, async (tx) => { const [rec, h] = await ctx.getRecordHandle(tx); if (rec?.purchaseStatus !== PurchaseStatus.AbortingWithRefund) { @@ -4779,7 +4796,6 @@ async function storeRefunds( rf.coin_pub, rf.rtransaction_id, ]); - let oldTxState: TransactionState | undefined = undefined; if (oldItem) { logger.info("already have refund in database"); if (oldItem.status === RefundItemStatus.Done) {