summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core/src/operations/pay.ts
diff options
context:
space:
mode:
authorFlorian Dold <florian.dold@gmail.com>2020-09-09 02:18:03 +0530
committerFlorian Dold <florian.dold@gmail.com>2020-09-09 02:18:03 +0530
commit67df550b4f6d67f8de346985df26133dc8da5c05 (patch)
tree575b514c1f6a9723fd32678da42f21c3c7ab523b /packages/taler-wallet-core/src/operations/pay.ts
parent68ca4600e0e3460423a6c33530bd4bb8096afa65 (diff)
downloadwallet-core-67df550b4f6d67f8de346985df26133dc8da5c05.tar.gz
wallet-core-67df550b4f6d67f8de346985df26133dc8da5c05.tar.bz2
wallet-core-67df550b4f6d67f8de346985df26133dc8da5c05.zip
implement payment aborts with integration test
Diffstat (limited to 'packages/taler-wallet-core/src/operations/pay.ts')
-rw-r--r--packages/taler-wallet-core/src/operations/pay.ts138
1 files changed, 77 insertions, 61 deletions
diff --git a/packages/taler-wallet-core/src/operations/pay.ts b/packages/taler-wallet-core/src/operations/pay.ts
index 3dc5e1600..8dbc2af41 100644
--- a/packages/taler-wallet-core/src/operations/pay.ts
+++ b/packages/taler-wallet-core/src/operations/pay.ts
@@ -35,6 +35,7 @@ import {
CoinRecord,
DenominationRecord,
PayCoinSelection,
+ AbortStatus,
} from "../types/dbTypes";
import { NotificationType } from "../types/notifications";
import {
@@ -77,7 +78,11 @@ import {
} from "../util/http";
import { TalerErrorCode } from "../TalerErrorCode";
import { URL } from "../util/url";
-import { initRetryInfo, updateRetryInfoTimeout, getRetryDuration } from "../util/retries";
+import {
+ initRetryInfo,
+ updateRetryInfoTimeout,
+ getRetryDuration,
+} from "../util/retries";
/**
* Logger.
@@ -111,7 +116,6 @@ export interface AvailableCoinInfo {
feeDeposit: AmountJson;
}
-
/**
* Compute the total cost of a payment to the customer.
*
@@ -429,8 +433,7 @@ async function recordConfirmPay(
logger.trace(`recording payment with session ID ${sessionId}`);
const payCostInfo = await getTotalPaymentCost(ws, coinSelection);
const t: PurchaseRecord = {
- abortDone: false,
- abortRequested: false,
+ abortStatus: AbortStatus.None,
contractTermsRaw: d.contractTermsRaw,
contractData: d.contractData,
lastSessionId: sessionId,
@@ -444,7 +447,7 @@ async function recordConfirmPay(
lastRefundStatusError: undefined,
payRetryInfo: initRetryInfo(),
refundStatusRetryInfo: initRetryInfo(),
- refundStatusRequested: false,
+ refundQueryRequested: false,
timestampFirstSuccessfulPay: undefined,
autoRefundDeadline: undefined,
paymentSubmitPending: true,
@@ -522,6 +525,10 @@ async function incrementProposalRetry(
}
}
+/**
+ * FIXME: currently pay operations aren't ever automatically retried.
+ * But we still keep a payRetryInfo around in the database.
+ */
async function incrementPurchasePayRetry(
ws: InternalWalletState,
proposalId: string,
@@ -579,7 +586,10 @@ function getProposalRequestTimeout(proposal: ProposalRecord): Duration {
}
function getPayRequestTimeout(purchase: PurchaseRecord): Duration {
- return durationMul({ d_ms: 5000 }, 1 + purchase.payCoinSelection.coinPubs.length / 20);
+ return durationMul(
+ { d_ms: 5000 },
+ 1 + purchase.payCoinSelection.coinPubs.length / 20,
+ );
}
async function processDownloadProposalImpl(
@@ -794,40 +804,37 @@ async function storeFirstPaySuccess(
paySig: string,
): Promise<void> {
const now = getTimestampNow();
- await ws.db.runWithWriteTransaction(
- [Stores.purchases],
- async (tx) => {
- const purchase = await tx.get(Stores.purchases, proposalId);
+ await ws.db.runWithWriteTransaction([Stores.purchases], async (tx) => {
+ const purchase = await tx.get(Stores.purchases, proposalId);
- if (!purchase) {
- logger.warn("purchase does not exist anymore");
- return;
- }
- const isFirst = purchase.timestampFirstSuccessfulPay === undefined;
- if (!isFirst) {
- logger.warn("payment success already stored");
- return;
- }
- purchase.timestampFirstSuccessfulPay = now;
- purchase.paymentSubmitPending = false;
- purchase.lastPayError = undefined;
- purchase.lastSessionId = sessionId;
- purchase.payRetryInfo = initRetryInfo(false);
- purchase.merchantPaySig = paySig;
- if (isFirst) {
- const ar = purchase.contractData.autoRefund;
- if (ar) {
- logger.info("auto_refund present");
- purchase.refundStatusRequested = true;
- purchase.refundStatusRetryInfo = initRetryInfo();
- purchase.lastRefundStatusError = undefined;
- purchase.autoRefundDeadline = timestampAddDuration(now, ar);
- }
+ if (!purchase) {
+ logger.warn("purchase does not exist anymore");
+ return;
+ }
+ const isFirst = purchase.timestampFirstSuccessfulPay === undefined;
+ if (!isFirst) {
+ logger.warn("payment success already stored");
+ return;
+ }
+ purchase.timestampFirstSuccessfulPay = now;
+ purchase.paymentSubmitPending = false;
+ purchase.lastPayError = undefined;
+ purchase.lastSessionId = sessionId;
+ purchase.payRetryInfo = initRetryInfo(false);
+ purchase.merchantPaySig = paySig;
+ if (isFirst) {
+ const ar = purchase.contractData.autoRefund;
+ if (ar) {
+ logger.info("auto_refund present");
+ purchase.refundQueryRequested = true;
+ purchase.refundStatusRetryInfo = initRetryInfo();
+ purchase.lastRefundStatusError = undefined;
+ purchase.autoRefundDeadline = timestampAddDuration(now, ar);
}
+ }
- await tx.put(Stores.purchases, purchase);
- },
- );
+ await tx.put(Stores.purchases, purchase);
+ });
}
async function storePayReplaySuccess(
@@ -835,26 +842,23 @@ async function storePayReplaySuccess(
proposalId: string,
sessionId: string | undefined,
): Promise<void> {
- await ws.db.runWithWriteTransaction(
- [Stores.purchases],
- async (tx) => {
- const purchase = await tx.get(Stores.purchases, proposalId);
+ await ws.db.runWithWriteTransaction([Stores.purchases], async (tx) => {
+ const purchase = await tx.get(Stores.purchases, proposalId);
- if (!purchase) {
- logger.warn("purchase does not exist anymore");
- return;
- }
- const isFirst = purchase.timestampFirstSuccessfulPay === undefined;
- if (isFirst) {
- throw Error("invalid payment state");
- }
- purchase.paymentSubmitPending = false;
- purchase.lastPayError = undefined;
- purchase.payRetryInfo = initRetryInfo(false);
- purchase.lastSessionId = sessionId;
- await tx.put(Stores.purchases, purchase);
- },
- );
+ if (!purchase) {
+ logger.warn("purchase does not exist anymore");
+ return;
+ }
+ const isFirst = purchase.timestampFirstSuccessfulPay === undefined;
+ if (isFirst) {
+ throw Error("invalid payment state");
+ }
+ purchase.paymentSubmitPending = false;
+ purchase.lastPayError = undefined;
+ purchase.payRetryInfo = initRetryInfo(false);
+ purchase.lastSessionId = sessionId;
+ await tx.put(Stores.purchases, purchase);
+ });
}
/**
@@ -863,7 +867,7 @@ async function storePayReplaySuccess(
* If the wallet has previously paid, it just transmits the merchant's
* own signature certifying that the wallet has previously paid.
*/
-export async function submitPay(
+async function submitPay(
ws: InternalWalletState,
proposalId: string,
): Promise<ConfirmPayResult> {
@@ -871,7 +875,7 @@ export async function submitPay(
if (!purchase) {
throw Error("Purchase not found: " + proposalId);
}
- if (purchase.abortRequested) {
+ if (purchase.abortStatus !== AbortStatus.None) {
throw Error("not submitting payment for aborted purchase");
}
const sessionId = purchase.lastSessionId;
@@ -1047,7 +1051,11 @@ export async function preparePayForUri(
p.lastSessionId = uriResult.sessionId;
await tx.put(Stores.purchases, p);
});
- const r = await submitPay(ws, proposalId);
+ const r = await guardOperationException(
+ () => submitPay(ws, proposalId),
+ (e: TalerErrorDetails): Promise<void> =>
+ incrementPurchasePayRetry(ws, proposalId, e),
+ );
if (r.type !== ConfirmPayResultType.Done) {
throw Error("submitting pay failed");
}
@@ -1125,7 +1133,11 @@ export async function confirmPay(
});
}
logger.trace("confirmPay: submitting payment for existing purchase");
- return submitPay(ws, proposalId);
+ return await guardOperationException(
+ () => submitPay(ws, proposalId),
+ (e: TalerErrorDetails): Promise<void> =>
+ incrementPurchasePayRetry(ws, proposalId, e),
+ );
}
logger.trace("confirmPay: purchase record does not exist yet");
@@ -1179,7 +1191,11 @@ export async function confirmPay(
sessionIdOverride,
);
- return submitPay(ws, proposalId);
+ return await guardOperationException(
+ () => submitPay(ws, proposalId),
+ (e: TalerErrorDetails): Promise<void> =>
+ incrementPurchasePayRetry(ws, proposalId, e),
+ );
}
export async function processPurchasePay(