diff options
author | Florian Dold <florian@dold.me> | 2023-01-11 17:12:08 +0100 |
---|---|---|
committer | Florian Dold <florian@dold.me> | 2023-01-11 17:14:49 +0100 |
commit | 143a4fe4ac5b8724cf6e13a704e88daa99dd4202 (patch) | |
tree | 07b2fe6d993ae46838bc4072db8eba609bbb1801 /packages/taler-wallet-core/src/operations | |
parent | 5fc0cb7927e7a81a80129f6165b2027e72b89d33 (diff) | |
download | wallet-core-143a4fe4ac5b8724cf6e13a704e88daa99dd4202.tar.xz |
wallet-core: refresh when aborting payments
Diffstat (limited to 'packages/taler-wallet-core/src/operations')
4 files changed, 135 insertions, 55 deletions
diff --git a/packages/taler-wallet-core/src/operations/backup/export.ts b/packages/taler-wallet-core/src/operations/backup/export.ts index 965d51776..a66444485 100644 --- a/packages/taler-wallet-core/src/operations/backup/export.ts +++ b/packages/taler-wallet-core/src/operations/backup/export.ts @@ -89,7 +89,7 @@ export async function exportBackup( x.config, x.exchanges, x.exchangeDetails, - x.exchangeSignkeys, + x.exchangeSignKeys, x.coins, x.contractTerms, x.denominations, diff --git a/packages/taler-wallet-core/src/operations/exchanges.ts b/packages/taler-wallet-core/src/operations/exchanges.ts index b6e2a9d73..67f77de77 100644 --- a/packages/taler-wallet-core/src/operations/exchanges.ts +++ b/packages/taler-wallet-core/src/operations/exchanges.ts @@ -675,7 +675,7 @@ export async function updateExchangeFromUrlHandler( x.exchanges, x.exchangeTos, x.exchangeDetails, - x.exchangeSignkeys, + x.exchangeSignKeys, x.denominations, x.coins, x.refreshGroups, 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<ConfirmPayResult> { - 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<void> { + 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}`); }); } diff --git a/packages/taler-wallet-core/src/operations/transactions.ts b/packages/taler-wallet-core/src/operations/transactions.ts index 3fb13d2a0..2570a4e5f 100644 --- a/packages/taler-wallet-core/src/operations/transactions.ts +++ b/packages/taler-wallet-core/src/operations/transactions.ts @@ -23,6 +23,7 @@ import { Amounts, constructPayPullUri, constructPayPushUri, + ExtendedStatus, Logger, OrderShortInfo, PaymentStatus, @@ -66,6 +67,7 @@ import { import { processDepositGroup } from "./deposits.js"; import { getExchangeDetails } from "./exchanges.js"; import { + abortPay, expectProposalDownload, extractContractData, processPurchasePay, @@ -352,6 +354,10 @@ function buildTransactionForPushPaymentDebit( summary: contractTerms.summary, }, frozen: false, + extendedStatus: + pi.status != PeerPushPaymentInitiationStatus.PurseCreated + ? ExtendedStatus.Pending + : ExtendedStatus.Done, pending: pi.status != PeerPushPaymentInitiationStatus.PurseCreated, timestamp: pi.timestampCreated, talerUri: constructPayPushUri({ @@ -377,6 +383,7 @@ function buildTransactionForPullPaymentDebit( exchangeBaseUrl: pi.exchangeBaseUrl, frozen: false, pending: false, + extendedStatus: ExtendedStatus.Done, info: { expiration: pi.contractTerms.purse_expiration, summary: pi.contractTerms.summary, @@ -401,6 +408,7 @@ function buildTransactionForPullPaymentCredit( amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue), amountRaw: Amounts.stringify(wsr.instructedAmount), exchangeBaseUrl: wsr.exchangeBaseUrl, + extendedStatus: ExtendedStatus.Done, pending: !wsr.timestampFinish, timestamp: wsr.timestampStart, info: { @@ -435,6 +443,9 @@ function buildTransactionForPushPaymentCredit( expiration: wsr.wgInfo.contractTerms.purse_expiration, summary: wsr.wgInfo.contractTerms.summary, }, + extendedStatus: wsr.timestampFinish + ? ExtendedStatus.Done + : ExtendedStatus.Pending, pending: !wsr.timestampFinish, timestamp: wsr.timestampStart, transactionId: makeTransactionId( @@ -464,6 +475,9 @@ function buildTransactionForBankIntegratedWithdraw( bankConfirmationUrl: wsr.wgInfo.bankInfo.confirmUrl, }, exchangeBaseUrl: wsr.exchangeBaseUrl, + extendedStatus: wsr.timestampFinish + ? ExtendedStatus.Done + : ExtendedStatus.Pending, pending: !wsr.timestampFinish, timestamp: wsr.timestampStart, transactionId: makeTransactionId( @@ -504,6 +518,9 @@ function buildTransactionForManualWithdraw( exchangePaytoUris, }, exchangeBaseUrl: withdrawalGroup.exchangeBaseUrl, + extendedStatus: withdrawalGroup.timestampFinish + ? ExtendedStatus.Done + : ExtendedStatus.Pending, pending: !withdrawalGroup.timestampFinish, timestamp: withdrawalGroup.timestampStart, transactionId: makeTransactionId( @@ -523,6 +540,9 @@ function buildTransactionForDeposit( type: TransactionType.Deposit, amountRaw: Amounts.stringify(dg.effectiveDepositAmount), amountEffective: Amounts.stringify(dg.totalPayCost), + extendedStatus: dg.timestampFinished + ? ExtendedStatus.Done + : ExtendedStatus.Pending, pending: !dg.timestampFinished, frozen: false, timestamp: dg.timestampCreated, @@ -547,6 +567,9 @@ function buildTransactionForTip( type: TransactionType.Tip, amountEffective: Amounts.stringify(tipRecord.tipAmountEffective), amountRaw: Amounts.stringify(tipRecord.tipAmountRaw), + extendedStatus: tipRecord.pickedUpTimestamp + ? ExtendedStatus.Done + : ExtendedStatus.Pending, pending: !tipRecord.pickedUpTimestamp, frozen: false, timestamp: tipRecord.acceptedTimestamp, @@ -654,6 +677,7 @@ async function buildTransactionForRefund( purchaseRecord.refundAmountAwaiting === undefined ? undefined : Amounts.stringify(purchaseRecord.refundAmountAwaiting), + extendedStatus: ExtendedStatus.Done, pending: false, frozen: false, ...(ort?.lastError ? { error: ort.lastError } : {}), @@ -710,6 +734,33 @@ async function buildTransactionForPurchase( checkDbInvariant(!!timestamp); checkDbInvariant(!!purchaseRecord.payInfo); + + let status: ExtendedStatus; + switch (purchaseRecord.purchaseStatus) { + case PurchaseStatus.AbortingWithRefund: + status = ExtendedStatus.Aborting; + break; + case PurchaseStatus.Paid: + case PurchaseStatus.RepurchaseDetected: + status = ExtendedStatus.Done; + break; + case PurchaseStatus.DownloadingProposal: + case PurchaseStatus.QueryingRefund: + case PurchaseStatus.Proposed: + case PurchaseStatus.Paying: + status = ExtendedStatus.Pending; + break; + case PurchaseStatus.ProposalDownloadFailed: + status = ExtendedStatus.Failed; + break; + case PurchaseStatus.PaymentAbortFinished: + status = ExtendedStatus.Aborted; + break; + default: + // FIXME: Should we have some unknown status? + status = ExtendedStatus.Pending; + } + return { type: TransactionType.Payment, amountRaw: Amounts.stringify(contractData.amount), @@ -723,6 +774,7 @@ async function buildTransactionForPurchase( status: purchaseRecord.timestampFirstSuccessfulPay ? PaymentStatus.Paid : PaymentStatus.Accepted, + extendedStatus: status, pending: purchaseRecord.purchaseStatus === PurchaseStatus.Paying, refunds, timestamp, @@ -1163,3 +1215,19 @@ export async function deleteTransaction( throw Error(`can't delete a '${unknownTxType}' transaction`); } } + +export async function abortTransaction( + ws: InternalWalletState, + transactionId: string, + forceImmediateAbort?: boolean, +): Promise<void> { + const { type, args: rest } = parseId("txn", transactionId); + + if (type === TransactionType.Payment) { + const proposalId = rest[0]; + await abortPay(ws, proposalId, forceImmediateAbort); + } else { + const unknownTxType: any = type; + throw Error(`can't abort a '${unknownTxType}' transaction`); + } +} |