From 143a4fe4ac5b8724cf6e13a704e88daa99dd4202 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Wed, 11 Jan 2023 17:12:08 +0100 Subject: wallet-core: refresh when aborting payments --- .../src/operations/pay-merchant.ts | 118 ++++++++++++--------- 1 file changed, 65 insertions(+), 53 deletions(-) (limited to 'packages/taler-wallet-core/src/operations/pay-merchant.ts') diff --git a/packages/taler-wallet-core/src/operations/pay-merchant.ts b/packages/taler-wallet-core/src/operations/pay-merchant.ts index c8e3230b9..2f1f3375c 100644 --- a/packages/taler-wallet-core/src/operations/pay-merchant.ts +++ b/packages/taler-wallet-core/src/operations/pay-merchant.ts @@ -125,6 +125,7 @@ import { } from "../util/retries.js"; import { makeTransactionId, + runOperationWithErrorReporting, spendCoins, storeOperationError, storeOperationPending, @@ -1135,9 +1136,9 @@ export function selectForced( export type SelectPayCoinsResult = | { - type: "failure"; - insufficientBalanceDetails: PayMerchantInsufficientBalanceDetails; - } + type: "failure"; + insufficientBalanceDetails: PayMerchantInsufficientBalanceDetails; + } | { type: "success"; coinSel: PayCoinSelection }; /** @@ -1594,7 +1595,12 @@ export async function runPayForConfirmPay( ws: InternalWalletState, proposalId: string, ): Promise { - const res = await processPurchasePay(ws, proposalId, { forceNow: true }); + logger.trace("processing proposal for confirmPay"); + const opId = RetryTags.byPaymentProposalId(proposalId); + const res = await runOperationWithErrorReporting(ws, opId, async () => { + return await processPurchasePay(ws, proposalId, { forceNow: true }); + }); + logger.trace(`processPurchasePay response type ${res.type}`); switch (res.type) { case OperationAttemptResultType.Finished: { const purchase = await ws.db @@ -1623,9 +1629,10 @@ export async function runPayForConfirmPay( const numRetry = opRetry?.retryInfo.retryCounter ?? 0; if ( res.errorDetail.code === - TalerErrorCode.WALLET_PAY_MERCHANT_SERVER_ERROR && + TalerErrorCode.WALLET_PAY_MERCHANT_SERVER_ERROR && numRetry < maxRetry ) { + logger.trace("hiding transient error from caller"); // Pretend the operation is pending instead of reporting // an error, but only up to maxRetry attempts. await storeOperationPending( @@ -1638,20 +1645,11 @@ export async function runPayForConfirmPay( transactionId: makeTransactionId(TransactionType.Payment, proposalId), }; } else { - // FIXME: allocate error code! - await storeOperationError( - ws, - RetryTags.byPaymentProposalId(proposalId), - res.errorDetail, - ); throw Error("payment failed"); } } case OperationAttemptResultType.Pending: - await storeOperationPending( - ws, - `${PendingTaskType.Purchase}:${proposalId}`, - ); + logger.trace("reporting pending as confirmPay response"); return { type: ConfirmPayResultType.Pending, transactionId: makeTransactionId(TransactionType.Payment, proposalId), @@ -1968,29 +1966,12 @@ export async function processPurchasePay( result: undefined, }; } + } - if (resp.status >= 400 && resp.status <= 499) { - const errDetails = await readUnexpectedResponseDetails(resp); - logger.warn(`server returned ${resp.status} response for /pay`); - logger.warn(j2s(errDetails)); - await ws.db - .mktx((x) => [x.purchases]) - .runReadWrite(async (tx) => { - const purch = await tx.purchases.get(proposalId); - if (!purch) { - return; - } - // FIXME: Should be some "PayPermanentlyFailed" and error info should be stored - purch.purchaseStatus = PurchaseStatus.PaymentAbortFinished; - await tx.purchases.put(purch); - }); - throw makePendingOperationFailedError( - errDetails, - TransactionType.Payment, - proposalId, - ); - } - + if (resp.status >= 400 && resp.status <= 499) { + logger.trace("got generic 4xx from merchant"); + const err = await readTalerErrorResponse(resp); + throwUnexpectedRequestError(resp, err); } const merchantResp = await readSuccessResponseJsonOrThrow( @@ -2395,16 +2376,18 @@ async function acceptRefunds( } } - const refreshCoinsPubs = Object.values(refreshCoinsMap); - logger.info(`refreshCoinMap ${j2s(refreshCoinsMap)}`); - if (refreshCoinsPubs.length > 0) { - await createRefreshGroup( - ws, - tx, - Amounts.currencyOf(refreshCoinsPubs[0].amount), - refreshCoinsPubs, - RefreshReason.Refund, - ); + if (reason === RefundReason.AbortRefund) { + const refreshCoinsPubs = Object.values(refreshCoinsMap); + logger.info(`refreshCoinMap ${j2s(refreshCoinsMap)}`); + if (refreshCoinsPubs.length > 0) { + await createRefreshGroup( + ws, + tx, + Amounts.currencyOf(refreshCoinsPubs[0].amount), + refreshCoinsPubs, + RefreshReason.Refund, + ); + } } // Are we done with querying yet, or do we need to do another round @@ -2808,12 +2791,21 @@ export async function processPurchaseQueryRefund( return OperationAttemptResult.finishedEmpty(); } -export async function abortFailedPayWithRefund( +export async function abortPay( ws: InternalWalletState, proposalId: string, + cancelImmediately?: boolean, ): Promise { + const opId = RetryTags.byPaymentProposalId(proposalId); await ws.db - .mktx((x) => [x.purchases]) + .mktx((x) => [ + x.purchases, + x.refreshGroups, + x.denominations, + x.coinAvailability, + x.coins, + x.operationRetries, + ]) .runReadWrite(async (tx) => { const purchase = await tx.purchases.get(proposalId); if (!purchase) { @@ -2828,10 +2820,30 @@ export async function abortFailedPayWithRefund( purchase.purchaseStatus = PurchaseStatus.AbortingWithRefund; } await tx.purchases.put(purchase); + await tx.operationRetries.delete(opId); + if (purchase.payInfo) { + 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( + ws, + tx, + currency, + refreshCoins, + RefreshReason.AbortPay, + ); + } + }); + + runOperationWithErrorReporting(ws, opId, async () => { + return await processPurchaseQueryRefund(ws, proposalId, { + forceNow: true, }); - processPurchaseQueryRefund(ws, proposalId, { - forceNow: true, - }).catch((e) => { - logger.trace(`error during refund processing after abort pay: ${e}`); }); } -- cgit v1.2.3