summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core/src
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2022-05-14 18:09:33 -0300
committerSebastian <sebasjm@gmail.com>2022-05-14 18:09:49 -0300
commite4ea2019430fb3c4b788f67427fbd743f604b7e5 (patch)
treee7426a82a2cc523c15d7f8b64e16c53722f7a87b /packages/taler-wallet-core/src
parentc02dbc833bc384b72e5b18450a47ae2b212b0a8e (diff)
downloadwallet-core-e4ea2019430fb3c4b788f67427fbd743f604b7e5.tar.gz
wallet-core-e4ea2019430fb3c4b788f67427fbd743f604b7e5.tar.bz2
wallet-core-e4ea2019430fb3c4b788f67427fbd743f604b7e5.zip
feat: awaiting refund
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/backup/import.ts7
-rw-r--r--packages/taler-wallet-core/src/operations/pay.ts23
-rw-r--r--packages/taler-wallet-core/src/operations/refund.ts185
-rw-r--r--packages/taler-wallet-core/src/operations/transactions.ts85
-rw-r--r--packages/taler-wallet-core/src/wallet.ts4
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;