diff options
author | Florian Dold <florian@dold.me> | 2024-01-15 17:36:50 +0100 |
---|---|---|
committer | Florian Dold <florian@dold.me> | 2024-01-15 18:43:20 +0100 |
commit | 8da08fe4205c1e03eec3d4925c598be0b6769ba5 (patch) | |
tree | e81d59fa7a1ef40687fc16199e4d44dd70e02b5c /packages/taler-wallet-core/src/operations/pay-merchant.ts | |
parent | 68f3bcdc6cece62176849ab065e82630ebc4deae (diff) | |
download | wallet-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.ts | 422 |
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 { |