summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core/src
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2024-01-15 19:38:34 +0100
committerFlorian Dold <florian@dold.me>2024-01-15 19:38:41 +0100
commitcc07d767abb0c1ba37be92014b06a94d3a3206d9 (patch)
treedbd037f08b4a438a3cc786778876b83762fc175e /packages/taler-wallet-core/src
parent728bab6584ee5632def40f22103dc7578ec3d64c (diff)
downloadwallet-core-cc07d767abb0c1ba37be92014b06a94d3a3206d9.tar.gz
wallet-core-cc07d767abb0c1ba37be92014b06a94d3a3206d9.tar.bz2
wallet-core-cc07d767abb0c1ba37be92014b06a94d3a3206d9.zip
wallet-core: fix pay state machine when order is deleted
Diffstat (limited to 'packages/taler-wallet-core/src')
-rw-r--r--packages/taler-wallet-core/src/db.ts6
-rw-r--r--packages/taler-wallet-core/src/operations/pay-merchant.ts175
2 files changed, 138 insertions, 43 deletions
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts
index 84066aaf0..5a412fb27 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -1179,12 +1179,14 @@ export enum PurchaseStatus {
*/
AbortedIncompletePayment = 0x0503_0000,
+ AbortedRefunded = 0x0503_0001,
+
+ AbortedOrderDeleted = 0x0503_0002,
+
/**
* Tried to abort, but aborting failed or was cancelled.
*/
FailedAbort = 0x0501_0001,
-
- AbortedRefunded = 0x0503_0000,
}
/**
diff --git a/packages/taler-wallet-core/src/operations/pay-merchant.ts b/packages/taler-wallet-core/src/operations/pay-merchant.ts
index bc9e94a21..50b73acb7 100644
--- a/packages/taler-wallet-core/src/operations/pay-merchant.ts
+++ b/packages/taler-wallet-core/src/operations/pay-merchant.ts
@@ -119,7 +119,11 @@ import {
import { assertUnreachable } from "../util/assertUnreachable.js";
import { PreviousPayCoins, selectPayCoinsNew } from "../util/coinSelection.js";
import { checkDbInvariant } from "../util/invariants.js";
-import { GetReadOnlyAccess } from "../util/query.js";
+import {
+ DbReadWriteTransactionArr,
+ GetReadOnlyAccess,
+ StoreNames,
+} from "../util/query.js";
import {
constructTaskIdentifier,
DbRetryInfo,
@@ -131,6 +135,7 @@ import {
TaskRunResultType,
TombstoneTag,
TransactionContext,
+ TransitionResult,
} from "./common.js";
import {
calculateRefreshOutput,
@@ -166,6 +171,64 @@ export class PayMerchantTransactionContext implements TransactionContext {
});
}
+ /**
+ * Transition a payment transition.
+ */
+ async transition(
+ f: (rec: PurchaseRecord) => Promise<TransitionResult>,
+ ): Promise<void> {
+ return this.transitionExtra(
+ {
+ extraStores: [],
+ },
+ f,
+ );
+ }
+
+ /**
+ * Transition a payment transition.
+ * Extra object stores may be accessed during the transition.
+ */
+ async transitionExtra<
+ StoreNameArray extends Array<StoreNames<typeof WalletStoresV1>> = [],
+ >(
+ opts: { extraStores: StoreNameArray },
+ f: (
+ rec: PurchaseRecord,
+ tx: DbReadWriteTransactionArr<
+ typeof WalletStoresV1,
+ ["purchases", ...StoreNameArray]
+ >,
+ ) => Promise<TransitionResult>,
+ ): Promise<void> {
+ const ws = this.ws;
+ const extraStores = opts.extraStores ?? [];
+ const transitionInfo = await ws.db.runReadWriteTx(
+ ["purchases", ...extraStores],
+ async (tx) => {
+ const purchaseRec = await tx.purchases.get(this.proposalId);
+ if (!purchaseRec) {
+ throw Error("purchase not found anymore");
+ }
+ const oldTxState = computePayMerchantTransactionState(purchaseRec);
+ const res = await f(purchaseRec, tx);
+ switch (res) {
+ case TransitionResult.Transition: {
+ await tx.purchases.put(purchaseRec);
+ const newTxState = computePayMerchantTransactionState(purchaseRec);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ }
+ default:
+ return undefined;
+ }
+ },
+ );
+ notifyTransition(ws, this.transactionId, transitionInfo);
+ }
+
async deleteTransaction(): Promise<void> {
const { ws, proposalId } = this;
await ws.db
@@ -210,16 +273,16 @@ export class PayMerchantTransactionContext implements TransactionContext {
async abortTransaction(): Promise<void> {
const { ws, proposalId, transactionId } = this;
- const transitionInfo = await ws.db
- .mktx((x) => [
- x.purchases,
- x.refreshGroups,
- x.denominations,
- x.coinAvailability,
- x.coins,
- x.operationRetries,
- ])
- .runReadWrite(async (tx) => {
+ const transitionInfo = await ws.db.runReadWriteTx(
+ [
+ "purchases",
+ "refreshGroups",
+ "denominations",
+ "coinAvailability",
+ "coins",
+ "operationRetries",
+ ],
+ async (tx) => {
const purchase = await tx.purchases.get(proposalId);
if (!purchase) {
throw Error("purchase not found");
@@ -231,34 +294,44 @@ export class PayMerchantTransactionContext implements TransactionContext {
logger.warn(`tried to abort successful payment`);
return;
}
- if (oldStatus === PurchaseStatus.PendingPaying) {
- purchase.purchaseStatus = PurchaseStatus.AbortingWithRefund;
- }
- await tx.purchases.put(purchase);
- if (oldStatus === PurchaseStatus.PendingPaying) {
- 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],
- });
+ switch (oldStatus) {
+ case PurchaseStatus.Done:
+ return;
+ case PurchaseStatus.PendingPaying:
+ case PurchaseStatus.SuspendedPaying: {
+ purchase.purchaseStatus = PurchaseStatus.AbortingWithRefund;
+ 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,
+ );
}
- await createRefreshGroup(
- ws,
- tx,
- currency,
- refreshCoins,
- RefreshReason.AbortPay,
- );
+ break;
}
+ case PurchaseStatus.DialogProposed:
+ purchase.purchaseStatus = PurchaseStatus.AbortedProposalRefused;
+ break;
}
+ await tx.purchases.put(purchase);
await tx.operationRetries.delete(this.retryTag);
const newTxState = computePayMerchantTransactionState(purchase);
return { oldTxState, newTxState };
- });
+ },
+ );
notifyTransition(ws, transactionId, transitionInfo);
ws.workAvailable.trigger();
}
@@ -1302,7 +1375,7 @@ export async function checkPaymentByProposalId(
});
notifyTransition(ws, transactionId, transitionInfo);
// FIXME: What about error handling?! This doesn't properly store errors in the DB.
- const r = await processPurchasePay(ws, proposalId, { forceNow: true });
+ const r = await processPurchasePay(ws, proposalId);
if (r.type !== TaskRunResultType.Finished) {
// FIXME: This does not surface the original error
throw Error("submitting pay failed");
@@ -1536,7 +1609,7 @@ async function runPayForConfirmPay(
proposalId,
});
const res = await runTaskWithErrorReporting(ws, taskId, async () => {
- return await processPurchasePay(ws, proposalId, { forceNow: true });
+ return await processPurchasePay(ws, proposalId);
});
logger.trace(`processPurchasePay response type ${res.type}`);
switch (res.type) {
@@ -1788,6 +1861,7 @@ export async function processPurchase(
case PurchaseStatus.DialogProposed:
case PurchaseStatus.AbortedProposalRefused:
case PurchaseStatus.AbortedIncompletePayment:
+ case PurchaseStatus.AbortedOrderDeleted:
case PurchaseStatus.AbortedRefunded:
case PurchaseStatus.SuspendedAbortingWithRefund:
case PurchaseStatus.SuspendedDownloadingProposal:
@@ -1807,7 +1881,6 @@ export async function processPurchase(
async function processPurchasePay(
ws: InternalWalletState,
proposalId: string,
- options: unknown = {},
): Promise<TaskRunResult> {
const purchase = await ws.db
.mktx((x) => [x.purchases])
@@ -2170,6 +2243,7 @@ export function computePayMerchantTransactionState(
major: TransactionMajorState.Failed,
minor: TransactionMinorState.Refused,
};
+ case PurchaseStatus.AbortedOrderDeleted:
case PurchaseStatus.AbortedRefunded:
return {
major: TransactionMajorState.Aborted,
@@ -2250,7 +2324,7 @@ export function computePayMerchantTransactionActions(
return [];
// Final States
case PurchaseStatus.AbortedProposalRefused:
- return [TransactionAction.Delete];
+ case PurchaseStatus.AbortedOrderDeleted:
case PurchaseStatus.AbortedRefunded:
return [TransactionAction.Delete];
case PurchaseStatus.Done:
@@ -2554,9 +2628,30 @@ async function processPurchaseAbortingRefund(
logger.trace(`making order abort request to ${requestUrl.href}`);
- const request = await ws.http.postJson(requestUrl.href, abortReq);
+ const abortHttpResp = await ws.http.fetch(requestUrl.href, {
+ method: "POST",
+ body: abortReq,
+ });
+
+ if (abortHttpResp.status === HttpStatusCode.NotFound) {
+ const err = await readTalerErrorResponse(abortHttpResp);
+ if (
+ err.code ===
+ TalerErrorCode.MERCHANT_POST_ORDERS_ID_ABORT_CONTRACT_NOT_FOUND
+ ) {
+ const ctx = new PayMerchantTransactionContext(ws, proposalId);
+ await ctx.transition(async (rec) => {
+ if (rec.purchaseStatus === PurchaseStatus.AbortingWithRefund) {
+ rec.purchaseStatus = PurchaseStatus.AbortedOrderDeleted;
+ return TransitionResult.Transition;
+ }
+ return TransitionResult.Stay;
+ });
+ }
+ }
+
const abortResp = await readSuccessResponseJsonOrThrow(
- request,
+ abortHttpResp,
codecForAbortResponse(),
);
@@ -2668,8 +2763,6 @@ async function processPurchaseAcceptRefund(
ws: InternalWalletState,
purchase: PurchaseRecord,
): Promise<TaskRunResult> {
- const proposalId = purchase.proposalId;
-
const download = await expectProposalDownload(ws, purchase);
const requestUrl = new URL(