diff options
author | Sebastian <sebasjm@gmail.com> | 2022-05-14 18:09:33 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2022-05-14 18:09:49 -0300 |
commit | e4ea2019430fb3c4b788f67427fbd743f604b7e5 (patch) | |
tree | e7426a82a2cc523c15d7f8b64e16c53722f7a87b /packages/taler-wallet-core | |
parent | c02dbc833bc384b72e5b18450a47ae2b212b0a8e (diff) | |
download | wallet-core-e4ea2019430fb3c4b788f67427fbd743f604b7e5.tar.xz |
feat: awaiting refund
Diffstat (limited to 'packages/taler-wallet-core')
-rw-r--r-- | packages/taler-wallet-core/src/db.ts | 6 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/operations/backup/import.ts | 7 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/operations/pay.ts | 23 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/operations/refund.ts | 185 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/operations/transactions.ts | 85 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/wallet.ts | 4 |
6 files changed, 180 insertions, 130 deletions
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index e8c46c7e3..8fe1937aa 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -1289,6 +1289,12 @@ export interface PurchaseRecord { autoRefundDeadline: TalerProtocolTimestamp | undefined; /** + * How much merchant has refund to be taken but the wallet + * did not picked up yet + */ + refundAwaiting: AmountJson | undefined; + + /** * Is the payment frozen? I.e. did we encounter * an error where it doesn't make sense to retry. */ diff --git a/packages/taler-wallet-core/src/operations/backup/import.ts b/packages/taler-wallet-core/src/operations/backup/import.ts index 37e97fbc8..a0a603ca3 100644 --- a/packages/taler-wallet-core/src/operations/backup/import.ts +++ b/packages/taler-wallet-core/src/operations/backup/import.ts @@ -345,7 +345,7 @@ export async function importBackup( } const denomPubHash = cryptoComp.rsaDenomPubToHash[ - backupDenomination.denom_pub.rsa_public_key + backupDenomination.denom_pub.rsa_public_key ]; checkLogicInvariant(!!denomPubHash); const existingDenom = await tx.denominations.get([ @@ -560,7 +560,7 @@ export async function importBackup( const amount = Amounts.parseOrThrow(parsedContractTerms.amount); const contractTermsHash = cryptoComp.proposalIdToContractTermsHash[ - backupProposal.proposal_id + backupProposal.proposal_id ]; let maxWireFee: AmountJson; if (parsedContractTerms.max_wire_fee) { @@ -704,7 +704,7 @@ export async function importBackup( const amount = Amounts.parseOrThrow(parsedContractTerms.amount); const contractTermsHash = cryptoComp.proposalIdToContractTermsHash[ - backupPurchase.proposal_id + backupPurchase.proposal_id ]; let maxWireFee: AmountJson; if (parsedContractTerms.max_wire_fee) { @@ -755,6 +755,7 @@ export async function importBackup( autoRefundDeadline: TalerProtocolTimestamp.never(), refundStatusRetryInfo: resetRetryInfo(), lastRefundStatusError: undefined, + refundAwaiting: undefined, timestampAccept: backupPurchase.timestamp_accept, timestampFirstSuccessfulPay: backupPurchase.timestamp_first_successful_pay, diff --git a/packages/taler-wallet-core/src/operations/pay.ts b/packages/taler-wallet-core/src/operations/pay.ts index db157257a..325d07bd1 100644 --- a/packages/taler-wallet-core/src/operations/pay.ts +++ b/packages/taler-wallet-core/src/operations/pay.ts @@ -443,6 +443,7 @@ async function recordConfirmPay( refundQueryRequested: false, timestampFirstSuccessfulPay: undefined, autoRefundDeadline: undefined, + refundAwaiting: undefined, paymentSubmitPending: true, refunds: {}, merchantPaySig: undefined, @@ -987,18 +988,16 @@ async function storeFirstPaySuccess( purchase.lastSessionId = sessionId; purchase.payRetryInfo = resetRetryInfo(); purchase.merchantPaySig = paySig; - if (isFirst) { - const protoAr = purchase.download.contractData.autoRefund; - if (protoAr) { - const ar = Duration.fromTalerProtocolDuration(protoAr); - logger.info("auto_refund present"); - purchase.refundQueryRequested = true; - purchase.refundStatusRetryInfo = resetRetryInfo(); - purchase.lastRefundStatusError = undefined; - purchase.autoRefundDeadline = AbsoluteTime.toTimestamp( - AbsoluteTime.addDuration(AbsoluteTime.now(), ar), - ); - } + const protoAr = purchase.download.contractData.autoRefund; + if (protoAr) { + const ar = Duration.fromTalerProtocolDuration(protoAr); + logger.info("auto_refund present"); + purchase.refundQueryRequested = true; + purchase.refundStatusRetryInfo = resetRetryInfo(); + purchase.lastRefundStatusError = undefined; + purchase.autoRefundDeadline = AbsoluteTime.toTimestamp( + AbsoluteTime.addDuration(AbsoluteTime.now(), ar), + ); } await tx.purchases.put(purchase); }); diff --git a/packages/taler-wallet-core/src/operations/refund.ts b/packages/taler-wallet-core/src/operations/refund.ts index dad8c6001..e5ce37a83 100644 --- a/packages/taler-wallet-core/src/operations/refund.ts +++ b/packages/taler-wallet-core/src/operations/refund.ts @@ -101,29 +101,19 @@ export async function prepareRefund( ); } + const awaiting = await queryAndSaveAwaitingRefund(ws, purchase) + const summary = calculateRefundSummary(purchase) const proposalId = purchase.proposalId; - const rfs = Object.values(purchase.refunds) - - let applied = 0; - let failed = 0; - const total = rfs.length; - rfs.forEach((refund) => { - if (refund.type === RefundState.Failed) { - failed = failed + 1; - } - if (refund.type === RefundState.Applied) { - applied = applied + 1; - } - }); const { contractData: c } = purchase.download return { proposalId, - amountEffectivePaid: Amounts.stringify(purchase.totalPayCost), - applied, - failed, - total, + effectivePaid: Amounts.stringify(summary.amountEffectivePaid), + gone: Amounts.stringify(summary.amountRefundGone), + granted: Amounts.stringify(summary.amountRefundGranted), + pending: summary.pendingAtExchange, + awaiting: Amounts.stringify(awaiting), info: { contractTermsHash: c.contractTermsHash, merchant: c.merchant, @@ -533,6 +523,44 @@ async function acceptRefunds( }); } + +function calculateRefundSummary(p: PurchaseRecord): RefundSummary { + let amountRefundGranted = Amounts.getZero( + p.download.contractData.amount.currency, + ); + let amountRefundGone = Amounts.getZero( + p.download.contractData.amount.currency, + ); + + let pendingAtExchange = false; + + Object.keys(p.refunds).forEach((rk) => { + const refund = p.refunds[rk]; + if (refund.type === RefundState.Pending) { + pendingAtExchange = true; + } + if ( + refund.type === RefundState.Applied || + refund.type === RefundState.Pending + ) { + amountRefundGranted = Amounts.add( + amountRefundGranted, + Amounts.sub( + refund.refundAmount, + refund.refundFee, + refund.totalRefreshCostBound, + ).amount, + ).amount; + } else { + amountRefundGone = Amounts.add( + amountRefundGone, + refund.refundAmount, + ).amount; + } + }); + return { amountEffectivePaid: p.totalPayCost, amountRefundGone, amountRefundGranted, pendingAtExchange } +} + /** * Summary of the refund status of a purchase. */ @@ -618,49 +646,15 @@ export async function applyRefund( throw Error("purchase no longer exists"); } - const p = purchase; - - let amountRefundGranted = Amounts.getZero( - purchase.download.contractData.amount.currency, - ); - let amountRefundGone = Amounts.getZero( - purchase.download.contractData.amount.currency, - ); - - let pendingAtExchange = false; - - Object.keys(purchase.refunds).forEach((rk) => { - const refund = p.refunds[rk]; - if (refund.type === RefundState.Pending) { - pendingAtExchange = true; - } - if ( - refund.type === RefundState.Applied || - refund.type === RefundState.Pending - ) { - amountRefundGranted = Amounts.add( - amountRefundGranted, - Amounts.sub( - refund.refundAmount, - refund.refundFee, - refund.totalRefreshCostBound, - ).amount, - ).amount; - } else { - amountRefundGone = Amounts.add( - amountRefundGone, - refund.refundAmount, - ).amount; - } - }); + const summary = calculateRefundSummary(purchase) return { contractTermsHash: purchase.download.contractData.contractTermsHash, proposalId: purchase.proposalId, - amountEffectivePaid: Amounts.stringify(purchase.totalPayCost), - amountRefundGone: Amounts.stringify(amountRefundGone), - amountRefundGranted: Amounts.stringify(amountRefundGranted), - pendingAtExchange, + amountEffectivePaid: Amounts.stringify(summary.amountEffectivePaid), + amountRefundGone: Amounts.stringify(summary.amountRefundGone), + amountRefundGranted: Amounts.stringify(summary.amountRefundGranted), + pendingAtExchange: summary.pendingAtExchange, info: { contractTermsHash: purchase.download.contractData.contractTermsHash, merchant: purchase.download.contractData.merchant, @@ -691,6 +685,59 @@ export async function processPurchaseQueryRefund( ); } +async function queryAndSaveAwaitingRefund( + ws: InternalWalletState, + purchase: PurchaseRecord, + waitForAutoRefund?: boolean): Promise<AmountJson> { + const requestUrl = new URL( + `orders/${purchase.download.contractData.orderId}`, + purchase.download.contractData.merchantBaseUrl, + ); + requestUrl.searchParams.set( + "h_contract", + purchase.download.contractData.contractTermsHash, + ); + // Long-poll for one second + if (waitForAutoRefund) { + requestUrl.searchParams.set("timeout_ms", "1000"); + requestUrl.searchParams.set("await_refund_obtained", "yes"); + logger.trace("making long-polling request for auto-refund"); + } + const resp = await ws.http.get(requestUrl.href); + const orderStatus = await readSuccessResponseJsonOrThrow( + resp, + codecForMerchantOrderStatusPaid(), + ); + if (!orderStatus.refunded) { + // Wait for retry ... + return Amounts.getZero(purchase.totalPayCost.currency); + } + + const refundAwaiting = Amounts.sub( + Amounts.parseOrThrow(orderStatus.refund_amount), + Amounts.parseOrThrow(orderStatus.refund_taken) + ).amount + + console.log("refund waiting found, ", refundAwaiting, orderStatus, purchase.refundAwaiting, purchase.refundAwaiting && Amounts.cmp(refundAwaiting, purchase.refundAwaiting)) + + if (purchase.refundAwaiting === undefined || Amounts.cmp(refundAwaiting, purchase.refundAwaiting) !== 0) { + await ws.db + .mktx((x) => ({ purchases: x.purchases })) + .runReadWrite(async (tx) => { + const p = await tx.purchases.get(purchase.proposalId); + if (!p) { + logger.warn("purchase does not exist anymore"); + return; + } + p.refundAwaiting = refundAwaiting + await tx.purchases.put(p); + }); + } + + return refundAwaiting; +} + + async function processPurchaseQueryRefundImpl( ws: InternalWalletState, proposalId: string, @@ -719,33 +766,13 @@ async function processPurchaseQueryRefundImpl( if (purchase.timestampFirstSuccessfulPay) { if ( - waitForAutoRefund && - purchase.autoRefundDeadline && + !purchase.autoRefundDeadline || !AbsoluteTime.isExpired( AbsoluteTime.fromTimestamp(purchase.autoRefundDeadline), ) ) { - const requestUrl = new URL( - `orders/${purchase.download.contractData.orderId}`, - purchase.download.contractData.merchantBaseUrl, - ); - requestUrl.searchParams.set( - "h_contract", - purchase.download.contractData.contractTermsHash, - ); - // Long-poll for one second - requestUrl.searchParams.set("timeout_ms", "1000"); - requestUrl.searchParams.set("await_refund_obtained", "yes"); - logger.trace("making long-polling request for auto-refund"); - const resp = await ws.http.get(requestUrl.href); - const orderStatus = await readSuccessResponseJsonOrThrow( - resp, - codecForMerchantOrderStatusPaid(), - ); - if (!orderStatus.refunded) { - // Wait for retry ... - return; - } + const awaitingAmount = await queryAndSaveAwaitingRefund(ws, purchase, waitForAutoRefund) + if (Amounts.isZero(awaitingAmount)) return; } const requestUrl = new URL( diff --git a/packages/taler-wallet-core/src/operations/transactions.ts b/packages/taler-wallet-core/src/operations/transactions.ts index 0a3549451..87b109d98 100644 --- a/packages/taler-wallet-core/src/operations/transactions.ts +++ b/packages/taler-wallet-core/src/operations/transactions.ts @@ -49,6 +49,16 @@ import { processWithdrawGroup } from "./withdraw.js"; const logger = new Logger("taler-wallet-core:transactions.ts"); +export enum TombstoneTag { + DeleteWithdrawalGroup = "delete-withdrawal-group", + DeleteReserve = "delete-reserve", + DeletePayment = "delete-payment", + DeleteTip = "delete-tip", + DeleteRefreshGroup = "delete-refresh-group", + DeleteDepositGroup = "delete-deposit-group", + DeleteRefund = "delete-refund", +} + /** * Create an event ID from the type and the primary key for the event. */ @@ -286,25 +296,6 @@ export async function getTransactions( TransactionType.Payment, pr.proposalId, ); - const err = pr.lastPayError ?? pr.lastRefundStatusError; - transactions.push({ - type: TransactionType.Payment, - amountRaw: Amounts.stringify(contractData.amount), - amountEffective: Amounts.stringify(pr.totalPayCost), - status: pr.timestampFirstSuccessfulPay - ? PaymentStatus.Paid - : PaymentStatus.Accepted, - pending: - !pr.timestampFirstSuccessfulPay && - pr.abortStatus === AbortStatus.None, - timestamp: pr.timestampAccept, - transactionId: paymentTransactionId, - proposalId: pr.proposalId, - info: info, - frozen: pr.payFrozen ?? false, - ...(err ? { error: err } : {}), - }); - const refundGroupKeys = new Set<string>(); for (const rk of Object.keys(pr.refunds)) { @@ -313,6 +304,9 @@ export async function getTransactions( refundGroupKeys.add(groupKey); } + let totalRefundRaw = Amounts.getZero(contractData.amount.currency); + let totalRefundEffective = Amounts.getZero(contractData.amount.currency); + for (const groupKey of refundGroupKeys.values()) { const refundTombstoneId = makeEventId( TombstoneTag.DeleteRefund, @@ -356,6 +350,10 @@ export async function getTransactions( if (!r0) { throw Error("invariant violated"); } + + totalRefundRaw = Amounts.add(totalRefundRaw, amountRaw).amount; + totalRefundEffective = Amounts.add(totalRefundEffective, amountEffective).amount; + transactions.push({ type: TransactionType.Refund, info, @@ -364,10 +362,34 @@ export async function getTransactions( timestamp: r0.obtainedTime, amountEffective: Amounts.stringify(amountEffective), amountRaw: Amounts.stringify(amountRaw), + refundPending: pr.refundAwaiting === undefined ? undefined : Amounts.stringify(pr.refundAwaiting), pending: false, frozen: false, }); } + + const err = pr.lastPayError ?? pr.lastRefundStatusError; + transactions.push({ + type: TransactionType.Payment, + amountRaw: Amounts.stringify(contractData.amount), + amountEffective: Amounts.stringify(pr.totalPayCost), + totalRefundRaw: Amounts.stringify(totalRefundRaw), + totalRefundEffective: Amounts.stringify(totalRefundEffective), + refundPending: pr.refundAwaiting === undefined ? undefined : Amounts.stringify(pr.refundAwaiting), + status: pr.timestampFirstSuccessfulPay + ? PaymentStatus.Paid + : PaymentStatus.Accepted, + pending: + !pr.timestampFirstSuccessfulPay && + pr.abortStatus === AbortStatus.None, + timestamp: pr.timestampAccept, + transactionId: paymentTransactionId, + proposalId: pr.proposalId, + info: info, + frozen: pr.payFrozen ?? false, + ...(err ? { error: err } : {}), + }); + }); tx.tips.iter().forEachAsync(async (tipRecord) => { @@ -419,16 +441,6 @@ export async function getTransactions( return { transactions: [...txNotPending, ...txPending] }; } -export enum TombstoneTag { - DeleteWithdrawalGroup = "delete-withdrawal-group", - DeleteReserve = "delete-reserve", - DeletePayment = "delete-payment", - DeleteTip = "delete-tip", - DeleteRefreshGroup = "delete-refresh-group", - DeleteDepositGroup = "delete-deposit-group", - DeleteRefund = "delete-refund", -} - /** * Immediately retry the underlying operation * of a transaction. @@ -442,28 +454,33 @@ export async function retryTransaction( const [type, ...rest] = transactionId.split(":"); switch (type) { - case TransactionType.Deposit: + case TransactionType.Deposit: { const depositGroupId = rest[0]; processDepositGroup(ws, depositGroupId, { forceNow: true, }); break; - case TransactionType.Withdrawal: + } + case TransactionType.Withdrawal: { const withdrawalGroupId = rest[0]; await processWithdrawGroup(ws, withdrawalGroupId, { forceNow: true }); break; - case TransactionType.Payment: + } + case TransactionType.Payment: { const proposalId = rest[0]; await processPurchasePay(ws, proposalId, { forceNow: true }); break; - case TransactionType.Tip: + } + case TransactionType.Tip: { const walletTipId = rest[0]; await processTip(ws, walletTipId, { forceNow: true }); break; - case TransactionType.Refresh: + } + case TransactionType.Refresh: { const refreshGroupId = rest[0]; await processRefreshGroup(ws, refreshGroupId, { forceNow: true }); break; + } default: break; } diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts index 053a0763b..905d9220a 100644 --- a/packages/taler-wallet-core/src/wallet.ts +++ b/packages/taler-wallet-core/src/wallet.ts @@ -1235,10 +1235,10 @@ class InternalWalletStateImpl implements InternalWalletState { const key = `${exchangeBaseUrl}:${denomPubHash}`; const cached = this.denomCache[key]; if (cached) { - logger.info("using cached denom"); + logger.trace("using cached denom"); return cached; } - logger.info("looking up denom denom"); + logger.trace("looking up denom denom"); const d = await tx.denominations.get([exchangeBaseUrl, denomPubHash]); if (d) { this.denomCache[key] = d; |