From 0406160869e7f9aa9e863acad58a160a14014467 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Thu, 25 May 2023 11:13:19 +0200 Subject: wallet-core: DD37 fixes and FIXME comments for merchant payments --- .../src/operations/pay-merchant.ts | 128 +++++++++++++++------ 1 file changed, 96 insertions(+), 32 deletions(-) (limited to 'packages/taler-wallet-core/src') diff --git a/packages/taler-wallet-core/src/operations/pay-merchant.ts b/packages/taler-wallet-core/src/operations/pay-merchant.ts index 54953246d..13fb2cb18 100644 --- a/packages/taler-wallet-core/src/operations/pay-merchant.ts +++ b/packages/taler-wallet-core/src/operations/pay-merchant.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2019-2022 Taler Systems S.A. + (C) 2019-2023 Taler Systems S.A. GNU Taler is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -40,7 +40,6 @@ import { CoinRefreshRequest, ConfirmPayResult, ConfirmPayResultType, - constructPayUri, ContractTermsUtil, Duration, encodeCrock, @@ -63,6 +62,7 @@ import { randomBytes, RefreshReason, StartRefundQueryForUriResponse, + stringifyTalerUri, TalerError, TalerErrorCode, TalerErrorDetail, @@ -197,16 +197,25 @@ async function failProposalPermanently( proposalId: string, err: TalerErrorDetail, ): Promise { - await ws.db + const transactionId = constructTransactionIdentifier({ + tag: TransactionType.Payment, + proposalId, + }); + const transitionInfo = await ws.db .mktx((x) => [x.purchases]) .runReadWrite(async (tx) => { const p = await tx.purchases.get(proposalId); if (!p) { return; } + // FIXME: We don't store the error detail here?! + const oldTxState = computePayMerchantTransactionState(p); p.purchaseStatus = PurchaseStatus.FailedClaim; + const newTxState = computePayMerchantTransactionState(p); await tx.purchases.put(p); + return { oldTxState, newTxState }; }); + notifyTransition(ws, transactionId, transitionInfo); } function getProposalRequestTimeout(retryInfo?: RetryInfo): Duration { @@ -226,8 +235,6 @@ function getPayRequestTimeout(purchase: PurchaseRecord): Duration { /** * Return the proposal download data for a purchase, throw if not available. - * - * (Async since in the future this will query the DB.) */ export async function expectProposalDownload( ws: InternalWalletState, @@ -314,10 +321,9 @@ export function extractContractData( }; } -export async function processDownloadProposal( +async function processDownloadProposal( ws: InternalWalletState, proposalId: string, - options: object = {}, ): Promise { const proposal = await ws.db .mktx((x) => [x.purchases]) @@ -339,6 +345,11 @@ export async function processDownloadProposal( }; } + const transactionId = constructTransactionIdentifier({ + tag: TransactionType.Payment, + proposalId, + }); + const orderClaimUrl = new URL( `orders/${proposal.orderId}/claim`, proposal.merchantBaseUrl, @@ -363,7 +374,8 @@ export async function processDownloadProposal( }); // FIXME: Do this in the background using the new return value - const httpResponse = await ws.http.postJson(orderClaimUrl, requestBody, { + const httpResponse = await ws.http.fetch(orderClaimUrl, { + body: requestBody, timeout: getProposalRequestTimeout(retryRecord?.retryInfo), }); const r = await readSuccessResponseJsonOrErrorCode( @@ -388,7 +400,7 @@ export async function processDownloadProposal( const proposalResp = r.response; // The proposalResp contains the contract terms as raw JSON, - // as the coded to parse them doesn't necessarily round-trip. + // as the code to parse them doesn't necessarily round-trip. // We need this raw JSON to compute the contract terms hash. // FIXME: Do better error handling, check if the @@ -496,7 +508,7 @@ export async function processDownloadProposal( logger.trace(`extracted contract data: ${j2s(contractData)}`); - await ws.db + const transitionInfo = await ws.db .mktx((x) => [x.purchases, x.contractTerms]) .runReadWrite(async (tx) => { const p = await tx.purchases.get(proposalId); @@ -506,6 +518,7 @@ export async function processDownloadProposal( if (p.purchaseStatus !== PurchaseStatus.DownloadingProposal) { return; } + const oldTxState = computePayMerchantTransactionState(p); p.download = { contractTermsHash, contractTermsMerchantSig: contractData.merchantSig, @@ -523,18 +536,28 @@ export async function processDownloadProposal( ) { const differentPurchase = await tx.purchases.indexes.byFulfillmentUrl.get(fulfillmentUrl); + // FIXME: Adjust this to account for refunds, don't count as repurchase + // if original order is refunded. if (differentPurchase) { logger.warn("repurchase detected"); p.purchaseStatus = PurchaseStatus.RepurchaseDetected; p.repurchaseProposalId = differentPurchase.proposalId; await tx.purchases.put(p); - return; } + } else { + p.purchaseStatus = PurchaseStatus.Proposed; + await tx.purchases.put(p); + } + const newTxState = computePayMerchantTransactionState(p); + return { + oldTxState, + newTxState, } - p.purchaseStatus = PurchaseStatus.Proposed; - await tx.purchases.put(p); }); + notifyTransition(ws, transactionId, transitionInfo); + + // FIXME: Deprecated pre-DD37 notification, remove eventually ws.notify({ type: NotificationType.ProposalDownloaded, proposalId: proposal.proposalId, @@ -547,13 +570,11 @@ export async function processDownloadProposal( } /** - * Download a proposal and store it in the database. - * Returns an id for it to retrieve it later. - * - * @param sessionId Current session ID, if the proposal is being - * downloaded in the context of a session ID. + * Create a new purchase transaction if necessary. If a purchase + * record for the provided arguments already exists, + * return the old proposal ID. */ -async function startDownloadProposal( +async function createPurchase( ws: InternalWalletState, merchantBaseUrl: string, orderId: string, @@ -619,7 +640,7 @@ async function startDownloadProposal( posConfirmation: undefined, }; - await ws.db + const transitionInfo = await ws.db .mktx((x) => [x.purchases]) .runReadWrite(async (tx) => { const existingRecord = await tx.purchases.indexes.byUrlAndOrderId.get([ @@ -628,11 +649,25 @@ async function startDownloadProposal( ]); if (existingRecord) { // Created concurrently - return; + return undefined; } await tx.purchases.put(proposalRecord); + const oldTxState: TransactionState = { + major: TransactionMajorState.None, + }; + const newTxState = computePayMerchantTransactionState(proposalRecord); + return { + oldTxState, + newTxState, + } }); + const transactionId = constructTransactionIdentifier({ + tag: TransactionType.Payment, + proposalId, + }); + notifyTransition(ws, transactionId, transitionInfo); + await processDownloadProposal(ws, proposalId); return proposalId; } @@ -643,8 +678,12 @@ async function storeFirstPaySuccess( sessionId: string | undefined, payResponse: MerchantPayResponse, ): Promise { + const transactionId = constructTransactionIdentifier({ + tag: TransactionType.Payment, + proposalId, + }); const now = AbsoluteTime.toTimestamp(AbsoluteTime.now()); - await ws.db + const transitionInfo = await ws.db .mktx((x) => [x.purchases, x.contractTerms]) .runReadWrite(async (tx) => { const purchase = await tx.purchases.get(proposalId); @@ -658,6 +697,7 @@ async function storeFirstPaySuccess( logger.warn("payment success already stored"); return; } + const oldTxState = computePayMerchantTransactionState(purchase); if (purchase.purchaseStatus === PurchaseStatus.Paying) { purchase.purchaseStatus = PurchaseStatus.Done; } @@ -686,7 +726,13 @@ async function storeFirstPaySuccess( ); } await tx.purchases.put(purchase); + const newTxState = computePayMerchantTransactionState(purchase); + return { + oldTxState, + newTxState, + } }); + notifyTransition(ws, transactionId, transitionInfo); } async function storePayReplaySuccess( @@ -694,7 +740,11 @@ async function storePayReplaySuccess( proposalId: string, sessionId: string | undefined, ): Promise { - await ws.db + const transactionId = constructTransactionIdentifier({ + tag: TransactionType.Payment, + proposalId, + }); + const transitionInfo = await ws.db .mktx((x) => [x.purchases]) .runReadWrite(async (tx) => { const purchase = await tx.purchases.get(proposalId); @@ -707,6 +757,7 @@ async function storePayReplaySuccess( if (isFirst) { throw Error("invalid payment state"); } + const oldTxState = computePayMerchantTransactionState(purchase); if ( purchase.purchaseStatus === PurchaseStatus.Paying || purchase.purchaseStatus === PurchaseStatus.PayingReplay @@ -715,7 +766,10 @@ async function storePayReplaySuccess( } purchase.lastSessionId = sessionId; await tx.purchases.put(purchase); + const newTxState = computePayMerchantTransactionState(purchase); + return { oldTxState, newTxState }; }); + notifyTransition(ws, transactionId, transitionInfo); } /** @@ -876,6 +930,10 @@ async function unblockBackup( }); } +// 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 +// FIXME: Should return immediately. export async function checkPaymentByProposalId( ws: InternalWalletState, proposalId: string, @@ -918,13 +976,14 @@ export async function checkPaymentByProposalId( proposalId, }); - const talerUri = constructPayUri( - proposal.merchantBaseUrl, - proposal.orderId, - proposal.lastSessionId ?? proposal.downloadSessionId ?? "", - proposal.claimToken, - proposal.noncePriv, - ); + const talerUri = stringifyTalerUri({ + type: TalerUriAction.Pay, + merchantBaseUrl: proposal.merchantBaseUrl, + orderId: proposal.orderId, + sessionId: proposal.lastSessionId ?? proposal.downloadSessionId ?? "", + claimToken: proposal.claimToken, + noncePriv: proposal.noncePriv, + }); // First check if we already paid for it. const purchase = await ws.db @@ -989,17 +1048,22 @@ export async function checkPaymentByProposalId( "automatically re-submitting payment with different session ID", ); logger.trace(`last: ${purchase.lastSessionId}, current: ${sessionId}`); - await ws.db + const transitionInfo = await ws.db .mktx((x) => [x.purchases]) .runReadWrite(async (tx) => { const p = await tx.purchases.get(proposalId); if (!p) { return; } + const oldTxState = computePayMerchantTransactionState(p); p.lastSessionId = sessionId; p.purchaseStatus = PurchaseStatus.PayingReplay; await tx.purchases.put(p); + const newTxState = computePayMerchantTransactionState(p); + return { oldTxState, newTxState }; }); + 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 }); if (r.type !== OperationAttemptResultType.Finished) { // FIXME: This does not surface the original error @@ -1092,7 +1156,7 @@ export async function preparePayForUri( ); } - const proposalId = await startDownloadProposal( + const proposalId = await createPurchase( ws, uriResult.merchantBaseUrl, uriResult.orderId, -- cgit v1.2.3