aboutsummaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core/src/operations/pay-merchant.ts
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2024-01-15 17:36:50 +0100
committerFlorian Dold <florian@dold.me>2024-01-15 18:43:20 +0100
commit8da08fe4205c1e03eec3d4925c598be0b6769ba5 (patch)
treee81d59fa7a1ef40687fc16199e4d44dd70e02b5c /packages/taler-wallet-core/src/operations/pay-merchant.ts
parent68f3bcdc6cece62176849ab065e82630ebc4deae (diff)
downloadwallet-core-8da08fe4205c1e03eec3d4925c598be0b6769ba5.tar.xz
wallet-core: uniform transaction interface, cleanup
Diffstat (limited to 'packages/taler-wallet-core/src/operations/pay-merchant.ts')
-rw-r--r--packages/taler-wallet-core/src/operations/pay-merchant.ts422
1 files changed, 221 insertions, 201 deletions
diff --git a/packages/taler-wallet-core/src/operations/pay-merchant.ts b/packages/taler-wallet-core/src/operations/pay-merchant.ts
index a81311702..bc9e94a21 100644
--- a/packages/taler-wallet-core/src/operations/pay-merchant.ts
+++ b/packages/taler-wallet-core/src/operations/pay-merchant.ts
@@ -94,7 +94,6 @@ import {
} from "@gnu-taler/taler-util/http";
import { EddsaKeypair } from "../crypto/cryptoImplementation.js";
import {
- BackupProviderStateTag,
CoinRecord,
DenominationRecord,
PurchaseRecord,
@@ -130,6 +129,8 @@ import {
TaskIdentifiers,
TaskRunResult,
TaskRunResultType,
+ TombstoneTag,
+ TransactionContext,
} from "./common.js";
import {
calculateRefreshOutput,
@@ -147,6 +148,224 @@ import {
*/
const logger = new Logger("pay-merchant.ts");
+export class PayMerchantTransactionContext implements TransactionContext {
+ private transactionId: string;
+ private retryTag: string;
+
+ constructor(
+ public ws: InternalWalletState,
+ public proposalId: string,
+ ) {
+ this.transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Payment,
+ proposalId,
+ });
+ this.retryTag = constructTaskIdentifier({
+ tag: PendingTaskType.Purchase,
+ proposalId,
+ });
+ }
+
+ async deleteTransaction(): Promise<void> {
+ const { ws, proposalId } = this;
+ await ws.db
+ .mktx((x) => [x.purchases, x.tombstones])
+ .runReadWrite(async (tx) => {
+ let found = false;
+ const purchase = await tx.purchases.get(proposalId);
+ if (purchase) {
+ found = true;
+ await tx.purchases.delete(proposalId);
+ }
+ if (found) {
+ await tx.tombstones.put({
+ id: TombstoneTag.DeletePayment + ":" + proposalId,
+ });
+ }
+ });
+ }
+
+ async suspendTransaction(): Promise<void> {
+ const { ws, proposalId, transactionId } = this;
+ stopLongpolling(ws, this.retryTag);
+ const transitionInfo = await ws.db
+ .mktx((x) => [x.purchases])
+ .runReadWrite(async (tx) => {
+ const purchase = await tx.purchases.get(proposalId);
+ if (!purchase) {
+ throw Error("purchase not found");
+ }
+ const oldTxState = computePayMerchantTransactionState(purchase);
+ let newStatus = transitionSuspend[purchase.purchaseStatus];
+ if (!newStatus) {
+ return undefined;
+ }
+ await tx.purchases.put(purchase);
+ const newTxState = computePayMerchantTransactionState(purchase);
+ return { oldTxState, newTxState };
+ });
+ notifyTransition(ws, transactionId, transitionInfo);
+ ws.workAvailable.trigger();
+ }
+
+ 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 purchase = await tx.purchases.get(proposalId);
+ if (!purchase) {
+ throw Error("purchase not found");
+ }
+ const oldTxState = computePayMerchantTransactionState(purchase);
+ const oldStatus = purchase.purchaseStatus;
+ if (purchase.timestampFirstSuccessfulPay) {
+ // No point in aborting it. We don't even report an error.
+ 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],
+ });
+ }
+ await createRefreshGroup(
+ ws,
+ tx,
+ currency,
+ refreshCoins,
+ RefreshReason.AbortPay,
+ );
+ }
+ }
+ await tx.operationRetries.delete(this.retryTag);
+ const newTxState = computePayMerchantTransactionState(purchase);
+ return { oldTxState, newTxState };
+ });
+ notifyTransition(ws, transactionId, transitionInfo);
+ ws.workAvailable.trigger();
+ }
+
+ async resumeTransaction(): Promise<void> {
+ const { ws, proposalId, transactionId, retryTag } = this;
+ stopLongpolling(ws, retryTag);
+ const transitionInfo = await ws.db
+ .mktx((x) => [x.purchases])
+ .runReadWrite(async (tx) => {
+ const purchase = await tx.purchases.get(proposalId);
+ if (!purchase) {
+ throw Error("purchase not found");
+ }
+ const oldTxState = computePayMerchantTransactionState(purchase);
+ let newStatus = transitionResume[purchase.purchaseStatus];
+ if (!newStatus) {
+ return undefined;
+ }
+ await tx.purchases.put(purchase);
+ const newTxState = computePayMerchantTransactionState(purchase);
+ return { oldTxState, newTxState };
+ });
+ ws.workAvailable.trigger();
+ notifyTransition(ws, transactionId, transitionInfo);
+ ws.workAvailable.trigger();
+ }
+
+ async failTransaction(): 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 purchase = await tx.purchases.get(proposalId);
+ if (!purchase) {
+ throw Error("purchase not found");
+ }
+ const oldTxState = computePayMerchantTransactionState(purchase);
+ let newState: PurchaseStatus | undefined = undefined;
+ switch (purchase.purchaseStatus) {
+ case PurchaseStatus.AbortingWithRefund:
+ newState = PurchaseStatus.FailedAbort;
+ break;
+ }
+ if (newState) {
+ purchase.purchaseStatus = newState;
+ await tx.purchases.put(purchase);
+ }
+ const newTxState = computePayMerchantTransactionState(purchase);
+ return { oldTxState, newTxState };
+ });
+ notifyTransition(ws, transactionId, transitionInfo);
+ ws.workAvailable.trigger();
+ }
+}
+
+export class RefundTransactionContext implements TransactionContext {
+ public transactionId: string;
+ constructor(
+ public ws: InternalWalletState,
+ public refundGroupId: string,
+ ) {
+ this.transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Refund,
+ refundGroupId,
+ });
+ }
+
+ async deleteTransaction(): Promise<void> {
+ const { ws, refundGroupId, transactionId } = this;
+ await ws.db
+ .mktx((x) => [x.refundGroups, x.tombstones])
+ .runReadWrite(async (tx) => {
+ const refundRecord = await tx.refundGroups.get(refundGroupId);
+ if (!refundRecord) {
+ return;
+ }
+ await tx.refundGroups.delete(refundGroupId);
+ await tx.tombstones.put({ id: transactionId });
+ // FIXME: Also tombstone the refund items, so that they won't reappear.
+ });
+ }
+
+ suspendTransaction(): Promise<void> {
+ throw new Error("Unsupported operation");
+ }
+
+ abortTransaction(): Promise<void> {
+ throw new Error("Unsupported operation");
+ }
+
+ resumeTransaction(): Promise<void> {
+ throw new Error("Unsupported operation");
+ }
+
+ failTransaction(): Promise<void> {
+ throw new Error("Unsupported operation");
+ }
+}
+
/**
* Compute the total cost of a payment to the customer.
*
@@ -949,27 +1168,6 @@ async function handleInsufficientFunds(
});
}
-async function unblockBackup(
- ws: InternalWalletState,
- proposalId: string,
-): Promise<void> {
- await ws.db
- .mktx((x) => [x.backupProviders])
- .runReadWrite(async (tx) => {
- await tx.backupProviders.indexes.byPaymentProposalId
- .iter(proposalId)
- .forEachAsync(async (bp) => {
- bp.state = {
- tag: BackupProviderStateTag.Ready,
- nextBackupTimestamp: timestampPreciseToDb(
- TalerPreciseTimestamp.now(),
- ),
- };
- tx.backupProviders.put(bp);
- });
- });
-}
-
// FIXME: Should probably not be exported in its current state
// FIXME: Should take a transaction ID instead of a proposal ID
// FIXME: Does way more than checking the payment
@@ -1606,7 +1804,7 @@ export async function processPurchase(
}
}
-export async function processPurchasePay(
+async function processPurchasePay(
ws: InternalWalletState,
proposalId: string,
options: unknown = {},
@@ -1772,7 +1970,6 @@ export async function processPurchasePay(
}
await storeFirstPaySuccess(ws, proposalId, sessionId, merchantResp);
- await unblockBackup(ws, proposalId);
} else {
const payAgainUrl = new URL(
`orders/${download.contractData.orderId}/paid`,
@@ -1799,7 +1996,6 @@ export async function processPurchasePay(
);
}
await storePayReplaySuccess(ws, proposalId, sessionId);
- await unblockBackup(ws, proposalId);
}
return TaskRunResult.finished();
@@ -1837,115 +2033,6 @@ export async function refuseProposal(
notifyTransition(ws, transactionId, transitionInfo);
}
-export async function abortPayMerchant(
- ws: InternalWalletState,
- proposalId: string,
-): Promise<void> {
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.Payment,
- proposalId,
- });
- const opId = constructTaskIdentifier({
- tag: PendingTaskType.Purchase,
- proposalId,
- });
- const transitionInfo = await ws.db
- .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) {
- throw Error("purchase not found");
- }
- const oldTxState = computePayMerchantTransactionState(purchase);
- const oldStatus = purchase.purchaseStatus;
- if (purchase.timestampFirstSuccessfulPay) {
- // No point in aborting it. We don't even report an error.
- 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],
- });
- }
- await createRefreshGroup(
- ws,
- tx,
- currency,
- refreshCoins,
- RefreshReason.AbortPay,
- );
- }
- }
- await tx.operationRetries.delete(opId);
- const newTxState = computePayMerchantTransactionState(purchase);
- return { oldTxState, newTxState };
- });
- notifyTransition(ws, transactionId, transitionInfo);
- ws.workAvailable.trigger();
-}
-
-export async function failPaymentTransaction(
- ws: InternalWalletState,
- proposalId: string,
-): Promise<void> {
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.Payment,
- proposalId,
- });
- const opId = constructTaskIdentifier({
- tag: PendingTaskType.Purchase,
- proposalId,
- });
- const transitionInfo = await ws.db
- .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) {
- throw Error("purchase not found");
- }
- const oldTxState = computePayMerchantTransactionState(purchase);
- let newState: PurchaseStatus | undefined = undefined;
- switch (purchase.purchaseStatus) {
- case PurchaseStatus.AbortingWithRefund:
- newState = PurchaseStatus.FailedAbort;
- break;
- }
- if (newState) {
- purchase.purchaseStatus = newState;
- await tx.purchases.put(purchase);
- }
- const newTxState = computePayMerchantTransactionState(purchase);
- return { oldTxState, newTxState };
- });
- notifyTransition(ws, transactionId, transitionInfo);
- ws.workAvailable.trigger();
-}
-
const transitionSuspend: {
[x in PurchaseStatus]?: {
next: PurchaseStatus | undefined;
@@ -1990,73 +2077,6 @@ const transitionResume: {
},
};
-export async function suspendPayMerchant(
- ws: InternalWalletState,
- proposalId: string,
-): Promise<void> {
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.Payment,
- proposalId,
- });
- const opId = constructTaskIdentifier({
- tag: PendingTaskType.Purchase,
- proposalId,
- });
- stopLongpolling(ws, opId);
- const transitionInfo = await ws.db
- .mktx((x) => [x.purchases])
- .runReadWrite(async (tx) => {
- const purchase = await tx.purchases.get(proposalId);
- if (!purchase) {
- throw Error("purchase not found");
- }
- const oldTxState = computePayMerchantTransactionState(purchase);
- let newStatus = transitionSuspend[purchase.purchaseStatus];
- if (!newStatus) {
- return undefined;
- }
- await tx.purchases.put(purchase);
- const newTxState = computePayMerchantTransactionState(purchase);
- return { oldTxState, newTxState };
- });
- notifyTransition(ws, transactionId, transitionInfo);
- ws.workAvailable.trigger();
-}
-
-export async function resumePayMerchant(
- ws: InternalWalletState,
- proposalId: string,
-): Promise<void> {
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.Payment,
- proposalId,
- });
- const opId = constructTaskIdentifier({
- tag: PendingTaskType.Purchase,
- proposalId,
- });
- stopLongpolling(ws, opId);
- const transitionInfo = await ws.db
- .mktx((x) => [x.purchases])
- .runReadWrite(async (tx) => {
- const purchase = await tx.purchases.get(proposalId);
- if (!purchase) {
- throw Error("purchase not found");
- }
- const oldTxState = computePayMerchantTransactionState(purchase);
- let newStatus = transitionResume[purchase.purchaseStatus];
- if (!newStatus) {
- return undefined;
- }
- await tx.purchases.put(purchase);
- const newTxState = computePayMerchantTransactionState(purchase);
- return { oldTxState, newTxState };
- });
- ws.workAvailable.trigger();
- notifyTransition(ws, transactionId, transitionInfo);
- ws.workAvailable.trigger();
-}
-
export function computePayMerchantTransactionState(
purchaseRecord: PurchaseRecord,
): TransactionState {