From e4ea2019430fb3c4b788f67427fbd743f604b7e5 Mon Sep 17 00:00:00 2001
From: Sebastian
Date: Sat, 14 May 2022 18:09:33 -0300
Subject: [PATCH] feat: awaiting refund
---
packages/taler-util/src/talerTypes.ts | 32 +--
packages/taler-util/src/transactionsTypes.ts | 20 ++
packages/taler-util/src/walletTypes.ts | 10 +-
.../src/harness/merchantApiTypes.ts | 16 +-
packages/taler-wallet-core/src/db.ts | 6 +
.../src/operations/backup/import.ts | 7 +-
.../taler-wallet-core/src/operations/pay.ts | 23 +-
.../src/operations/refund.ts | 185 +++++++++-------
.../src/operations/transactions.ts | 85 ++++---
packages/taler-wallet-core/src/wallet.ts | 4 +-
.../taler-wallet-webextension/compile_core.sh | 2 +
.../taler-wallet-webextension/compile_util.sh | 3 +
.../src/cta/Refund.stories.tsx | 8 +-
.../src/cta/Refund.test.ts | 53 +++--
.../src/cta/Refund.tsx | 82 ++++---
.../src/wallet/History.stories.tsx | 4 +
.../src/wallet/Transaction.stories.tsx | 4 +
.../src/wallet/Transaction.tsx | 208 ++++++++++++------
18 files changed, 484 insertions(+), 268 deletions(-)
create mode 100755 packages/taler-wallet-webextension/compile_core.sh
create mode 100755 packages/taler-wallet-webextension/compile_util.sh
diff --git a/packages/taler-util/src/talerTypes.ts b/packages/taler-util/src/talerTypes.ts
index b21c6caec..d9213ef5d 100644
--- a/packages/taler-util/src/talerTypes.ts
+++ b/packages/taler-util/src/talerTypes.ts
@@ -562,8 +562,8 @@ export interface MerchantAbortPayRefundDetails {
refund_amount: string;
/**
- * Fee for the refund.
- */
+ * Fee for the refund.
+ */
refund_fee: string;
/**
@@ -888,18 +888,18 @@ export type BlindedDenominationSignature =
| RsaBlindedDenominationSignature
| CSBlindedDenominationSignature;
-export const codecForBlindedDenominationSignature = () =>
- buildCodecForUnion()
- .discriminateOn("cipher")
- .alternative(DenomKeyType.Rsa, codecForRsaBlindedDenominationSignature())
- .build("BlindedDenominationSignature");
-
export const codecForRsaBlindedDenominationSignature = () =>
buildCodecForObject()
.property("cipher", codecForConstString(DenomKeyType.Rsa))
.property("blinded_rsa_signature", codecForString())
.build("RsaBlindedDenominationSignature");
+export const codecForBlindedDenominationSignature = () =>
+ buildCodecForUnion()
+ .discriminateOn("cipher")
+ .alternative(DenomKeyType.Rsa, codecForRsaBlindedDenominationSignature())
+ .build("BlindedDenominationSignature");
+
export class WithdrawResponse {
ev_sig: BlindedDenominationSignature;
}
@@ -1024,15 +1024,17 @@ export interface ExchangeRevealResponse {
}
interface MerchantOrderStatusPaid {
- /**
- * Was the payment refunded (even partially, via refund or abort)?
- */
+ // Was the payment refunded (even partially, via refund or abort)?
refunded: boolean;
- /**
- * Amount that was refunded in total.
- */
+ // Is any amount of the refund still waiting to be picked up (even partially)?
+ refund_pending: boolean;
+
+ // Amount that was refunded in total.
refund_amount: AmountString;
+
+ // Amount that already taken by the wallet.
+ refund_taken: AmountString;
}
interface MerchantOrderRefundResponse {
@@ -1528,6 +1530,8 @@ export const codecForMerchantOrderStatusPaid =
(): Codec =>
buildCodecForObject()
.property("refund_amount", codecForString())
+ .property("refund_taken", codecForString())
+ .property("refund_pending", codecForBoolean())
.property("refunded", codecForBoolean())
.build("MerchantOrderStatusPaid");
diff --git a/packages/taler-util/src/transactionsTypes.ts b/packages/taler-util/src/transactionsTypes.ts
index b9a227b68..37c1c7ef1 100644
--- a/packages/taler-util/src/transactionsTypes.ts
+++ b/packages/taler-util/src/transactionsTypes.ts
@@ -228,6 +228,21 @@ export interface TransactionPayment extends TransactionCommon {
* Amount that was paid, including deposit, wire and refresh fees.
*/
amountEffective: AmountString;
+
+ /**
+ * Amount that has been refunded by the merchant
+ */
+ totalRefundRaw: AmountString;
+
+ /**
+ * Amount will be added to the wallet's balance after fees and refreshing
+ */
+ totalRefundEffective: AmountString;
+
+ /**
+ * Amount pending to be picked up
+ */
+ refundPending: AmountString | undefined;
}
export interface OrderShortInfo {
@@ -287,6 +302,11 @@ export interface TransactionRefund extends TransactionCommon {
// Additional information about the refunded payment
info: OrderShortInfo;
+ /**
+ * Amount pending to be picked up
+ */
+ refundPending: AmountString | undefined;
+
// Amount that has been refunded by the merchant
amountRaw: AmountString;
diff --git a/packages/taler-util/src/walletTypes.ts b/packages/taler-util/src/walletTypes.ts
index a8946fbbb..fa884c414 100644
--- a/packages/taler-util/src/walletTypes.ts
+++ b/packages/taler-util/src/walletTypes.ts
@@ -279,11 +279,11 @@ export class ReturnCoinsRequest {
export interface PrepareRefundResult {
proposalId: string;
- applied: number;
- failed: number;
- total: number;
-
- amountEffectivePaid: AmountString;
+ effectivePaid: AmountString;
+ gone: AmountString;
+ granted: AmountString;
+ pending: boolean;
+ awaiting: AmountString;
info: OrderShortInfo;
}
diff --git a/packages/taler-wallet-cli/src/harness/merchantApiTypes.ts b/packages/taler-wallet-cli/src/harness/merchantApiTypes.ts
index 35062b579..8b10bb749 100644
--- a/packages/taler-wallet-cli/src/harness/merchantApiTypes.ts
+++ b/packages/taler-wallet-cli/src/harness/merchantApiTypes.ts
@@ -43,6 +43,8 @@ import {
EddsaPublicKeyString,
codecForAmountString,
TalerProtocolDuration,
+ codecForTimestamp,
+ TalerProtocolTimestamp,
} from "@gnu-taler/taler-util";
export interface PostOrderRequest {
@@ -80,6 +82,15 @@ export const codecForPostOrderResponse = (): Codec =>
.property("token", codecOptional(codecForString()))
.build("PostOrderResponse");
+
+export const codecForRefundDetails = (): Codec =>
+ buildCodecForObject()
+ .property("reason", codecForString())
+ .property("pending", codecForBoolean())
+ .property("amount", codecForString())
+ .property("timestamp", codecForTimestamp)
+ .build("PostOrderResponse");
+
export const codecForCheckPaymentPaidResponse =
(): Codec =>
buildCodecForObject()
@@ -200,7 +211,10 @@ export interface RefundDetails {
reason: string;
// when was the refund approved
- timestamp: AbsoluteTime;
+ timestamp: TalerProtocolTimestamp;
+
+ // has not been taken yet
+ pending: boolean;
// Total amount that was refunded (minus a refund fee).
amount: AmountString;
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
@@ -1288,6 +1288,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 {
+ 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();
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;
diff --git a/packages/taler-wallet-webextension/compile_core.sh b/packages/taler-wallet-webextension/compile_core.sh
new file mode 100755
index 000000000..b93d543f0
--- /dev/null
+++ b/packages/taler-wallet-webextension/compile_core.sh
@@ -0,0 +1,2 @@
+#!/bin/bash
+pnpm run --filter @gnu-taler/taler-wallet-core compile
diff --git a/packages/taler-wallet-webextension/compile_util.sh b/packages/taler-wallet-webextension/compile_util.sh
new file mode 100755
index 000000000..df2c4125a
--- /dev/null
+++ b/packages/taler-wallet-webextension/compile_util.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+pnpm run --filter @gnu-taler/taler-util compile
+
diff --git a/packages/taler-wallet-webextension/src/cta/Refund.stories.tsx b/packages/taler-wallet-webextension/src/cta/Refund.stories.tsx
index 6b7cf4621..bc2939896 100644
--- a/packages/taler-wallet-webextension/src/cta/Refund.stories.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Refund.stories.tsx
@@ -33,6 +33,7 @@ export const Complete = createExample(TestedComponent, {
state: {
status: "completed",
amount: Amounts.parseOrThrow("USD:1"),
+ granted: Amounts.parseOrThrow("USD:1"),
hook: undefined,
merchantName: "the merchant",
products: undefined,
@@ -44,9 +45,10 @@ export const InProgress = createExample(TestedComponent, {
status: "in-progress",
hook: undefined,
amount: Amounts.parseOrThrow("USD:1"),
+ awaitingAmount: Amounts.parseOrThrow("USD:1"),
+ granted: Amounts.parseOrThrow("USD:0"),
merchantName: "the merchant",
products: undefined,
- progress: 0.5,
},
});
@@ -58,6 +60,8 @@ export const Ready = createExample(TestedComponent, {
ignore: {},
amount: Amounts.parseOrThrow("USD:1"),
+ awaitingAmount: Amounts.parseOrThrow("USD:1"),
+ granted: Amounts.parseOrThrow("USD:0"),
merchantName: "the merchant",
products: [],
orderId: "abcdef",
@@ -73,6 +77,8 @@ export const WithAProductList = createExample(TestedComponent, {
accept: {},
ignore: {},
amount: Amounts.parseOrThrow("USD:1"),
+ awaitingAmount: Amounts.parseOrThrow("USD:1"),
+ granted: Amounts.parseOrThrow("USD:0"),
merchantName: "the merchant",
products: [
{
diff --git a/packages/taler-wallet-webextension/src/cta/Refund.test.ts b/packages/taler-wallet-webextension/src/cta/Refund.test.ts
index e77f8e682..864b4f12c 100644
--- a/packages/taler-wallet-webextension/src/cta/Refund.test.ts
+++ b/packages/taler-wallet-webextension/src/cta/Refund.test.ts
@@ -19,7 +19,7 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { Amounts, NotificationType, PrepareRefundResult } from "@gnu-taler/taler-util";
+import { AmountJson, Amounts, NotificationType, PrepareRefundResult } from "@gnu-taler/taler-util";
import { expect } from "chai";
import { mountHook } from "../test-utils.js";
import { SubsHandler } from "./Pay.test.js";
@@ -62,10 +62,12 @@ describe("Refund CTA states", () => {
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() =>
useComponentState("taler://refund/asdasdas", {
prepareRefund: async () => ({
- total: 0,
- applied: 0,
- failed: 0,
- amountEffectivePaid: 'EUR:2',
+ effectivePaid: 'EUR:2',
+ awaiting: 'EUR:2',
+ gone: 'EUR:0',
+ granted: 'EUR:0',
+ pending: false,
+ proposalId: '1',
info: {
contractTermsHash: '123',
merchant: {
@@ -107,10 +109,12 @@ describe("Refund CTA states", () => {
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() =>
useComponentState("taler://refund/asdasdas", {
prepareRefund: async () => ({
- total: 0,
- applied: 0,
- failed: 0,
- amountEffectivePaid: 'EUR:2',
+ effectivePaid: 'EUR:2',
+ awaiting: 'EUR:2',
+ gone: 'EUR:0',
+ granted: 'EUR:0',
+ pending: false,
+ proposalId: '1',
info: {
contractTermsHash: '123',
merchant: {
@@ -161,21 +165,30 @@ describe("Refund CTA states", () => {
});
it("should be in progress when doing refresh", async () => {
- let numApplied = 1;
+ let granted = Amounts.getZero('EUR')
+ const unit: AmountJson = { currency: 'EUR', value: 1, fraction: 0 }
+ const refunded: AmountJson = { currency: 'EUR', value: 2, fraction: 0 }
+ let awaiting: AmountJson = refunded
+ let pending = true;
+
const subscriptions = new SubsHandler();
function notifyMelt(): void {
- numApplied++;
+ granted = Amounts.add(granted, unit).amount;
+ pending = granted.value < refunded.value;
+ awaiting = Amounts.sub(refunded, granted).amount;
subscriptions.notifyEvent(NotificationType.RefreshMelted)
}
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() =>
useComponentState("taler://refund/asdasdas", {
prepareRefund: async () => ({
- total: 3,
- applied: numApplied,
- failed: 0,
- amountEffectivePaid: 'EUR:2',
+ awaiting: Amounts.stringify(awaiting),
+ effectivePaid: 'EUR:2',
+ gone: 'EUR:0',
+ granted: Amounts.stringify(granted),
+ pending,
+ proposalId: '1',
info: {
contractTermsHash: '123',
merchant: {
@@ -201,12 +214,12 @@ describe("Refund CTA states", () => {
{
const state = getLastResultOrThrow()
- if (state.status !== 'in-progress') expect.fail();
+ if (state.status !== 'in-progress') expect.fail('1');
if (state.hook) expect.fail();
expect(state.merchantName).eq('the merchant name');
expect(state.products).undefined;
expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:2"))
- expect(state.progress).closeTo(1 / 3, 0.01)
+ // expect(state.progress).closeTo(1 / 3, 0.01)
notifyMelt()
}
@@ -216,12 +229,12 @@ describe("Refund CTA states", () => {
{
const state = getLastResultOrThrow()
- if (state.status !== 'in-progress') expect.fail();
+ if (state.status !== 'in-progress') expect.fail('2');
if (state.hook) expect.fail();
expect(state.merchantName).eq('the merchant name');
expect(state.products).undefined;
expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:2"))
- expect(state.progress).closeTo(2 / 3, 0.01)
+ // expect(state.progress).closeTo(2 / 3, 0.01)
notifyMelt()
}
@@ -231,7 +244,7 @@ describe("Refund CTA states", () => {
{
const state = getLastResultOrThrow()
- if (state.status !== 'completed') expect.fail();
+ if (state.status !== 'completed') expect.fail('3');
if (state.hook) expect.fail();
expect(state.merchantName).eq('the merchant name');
expect(state.products).undefined;
diff --git a/packages/taler-wallet-webextension/src/cta/Refund.tsx b/packages/taler-wallet-webextension/src/cta/Refund.tsx
index f69fc4311..5387a1782 100644
--- a/packages/taler-wallet-webextension/src/cta/Refund.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Refund.tsx
@@ -34,7 +34,6 @@ import { LoadingError } from "../components/LoadingError.js";
import { LogoHeader } from "../components/LogoHeader.js";
import { Part } from "../components/Part.js";
import {
- Button,
ButtonSuccess,
SubTitle,
WalletAction,
@@ -99,6 +98,12 @@ export function View({ state }: ViewProps): VNode {
Total to refund}
+ text={}
+ kind="negative"
+ />
+ Refunded}
text={}
kind="negative"
/>
@@ -108,9 +113,9 @@ export function View({ state }: ViewProps): VNode {
) : undefined}
- */}
);
}
@@ -128,6 +133,14 @@ export function View({ state }: ViewProps): VNode {
this refund is already accepted.
+
+ Total to refunded}
+ text={}
+ kind="negative"
+ />
+
);
}
@@ -150,9 +163,23 @@ export function View({ state }: ViewProps): VNode {
Total to refund}
+ title={Order amount}
text={}
- kind="negative"
+ kind="neutral"
+ />
+ {Amounts.isNonZero(state.granted) && (
+ Already refunded}
+ text={}
+ kind="neutral"
+ />
+ )}
+ Refund offered}
+ text={}
+ kind="positive"
/>
{state.products && state.products.length ? (
@@ -164,9 +191,6 @@ export function View({ state }: ViewProps): VNode {
Confirm refund
-
);
@@ -184,6 +208,8 @@ interface Ready {
merchantName: string;
products: Product[] | undefined;
amount: AmountJson;
+ awaitingAmount: AmountJson;
+ granted: AmountJson;
accept: ButtonHandler;
ignore: ButtonHandler;
orderId: string;
@@ -199,7 +225,8 @@ interface InProgress {
merchantName: string;
products: Product[] | undefined;
amount: AmountJson;
- progress: number;
+ awaitingAmount: AmountJson;
+ granted: AmountJson;
}
interface Completed {
status: "completed";
@@ -207,6 +234,7 @@ interface Completed {
merchantName: string;
products: Product[] | undefined;
amount: AmountJson;
+ granted: AmountJson;
}
export function useComponentState(
@@ -253,25 +281,27 @@ export function useComponentState(
};
}
- const pending = refund.total > refund.applied + refund.failed;
- const completed = refund.total > 0 && refund.applied === refund.total;
+ const awaitingAmount = Amounts.parseOrThrow(refund.awaiting);
- if (pending) {
- return {
- status: "in-progress",
- hook: undefined,
- amount: Amounts.parseOrThrow(info.response.refund.amountEffectivePaid),
- merchantName: info.response.refund.info.merchant.name,
- products: info.response.refund.info.products,
- progress: (refund.applied + refund.failed) / refund.total,
- };
- }
-
- if (completed) {
+ if (Amounts.isZero(awaitingAmount)) {
return {
status: "completed",
hook: undefined,
- amount: Amounts.parseOrThrow(info.response.refund.amountEffectivePaid),
+ amount: Amounts.parseOrThrow(info.response.refund.effectivePaid),
+ granted: Amounts.parseOrThrow(info.response.refund.granted),
+ merchantName: info.response.refund.info.merchant.name,
+ products: info.response.refund.info.products,
+ };
+ }
+
+ if (refund.pending) {
+ return {
+ status: "in-progress",
+ hook: undefined,
+ awaitingAmount,
+ amount: Amounts.parseOrThrow(info.response.refund.effectivePaid),
+ granted: Amounts.parseOrThrow(info.response.refund.granted),
+
merchantName: info.response.refund.info.merchant.name,
products: info.response.refund.info.products,
};
@@ -280,7 +310,9 @@ export function useComponentState(
return {
status: "ready",
hook: undefined,
- amount: Amounts.parseOrThrow(info.response.refund.amountEffectivePaid),
+ amount: Amounts.parseOrThrow(info.response.refund.effectivePaid),
+ granted: Amounts.parseOrThrow(info.response.refund.granted),
+ awaitingAmount,
merchantName: info.response.refund.info.merchant.name,
products: info.response.refund.info.products,
orderId: info.response.refund.info.orderId,
diff --git a/packages/taler-wallet-webextension/src/wallet/History.stories.tsx b/packages/taler-wallet-webextension/src/wallet/History.stories.tsx
index 92f1dea1b..3080a866e 100644
--- a/packages/taler-wallet-webextension/src/wallet/History.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/History.stories.tsx
@@ -78,6 +78,9 @@ const exampleData = {
summary: "the summary",
fulfillmentMessage: "",
},
+ refundPending: undefined,
+ totalRefundEffective: "USD:0",
+ totalRefundRaw: "USD:0",
proposalId: "1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0",
status: PaymentStatus.Accepted,
} as TransactionPayment,
@@ -112,6 +115,7 @@ const exampleData = {
summary: "the summary",
fulfillmentMessage: "",
},
+ refundPending: undefined,
} as TransactionRefund,
};
diff --git a/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx b/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx
index b4dfb6ce0..f162543ae 100644
--- a/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx
@@ -83,6 +83,9 @@ const exampleData = {
summary: "Essay: Why the Devil's Advocate Doesn't Help Reach the Truth",
fulfillmentMessage: "",
},
+ refundPending: undefined,
+ totalRefundEffective: "USD:0",
+ totalRefundRaw: "USD:0",
proposalId: "1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0",
status: PaymentStatus.Accepted,
} as TransactionPayment,
@@ -117,6 +120,7 @@ const exampleData = {
summary: "the summary",
fulfillmentMessage: "",
},
+ refundPending: undefined,
} as TransactionRefund,
};
diff --git a/packages/taler-wallet-webextension/src/wallet/Transaction.tsx b/packages/taler-wallet-webextension/src/wallet/Transaction.tsx
index bcf6114a1..3377f98c7 100644
--- a/packages/taler-wallet-webextension/src/wallet/Transaction.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Transaction.tsx
@@ -40,7 +40,6 @@ import {
ButtonPrimary,
CenteredDialog,
InfoBox,
- LargeText,
ListOfProducts,
Overlay,
RowBorderGray,
@@ -51,6 +50,7 @@ import {
import { Time } from "../components/Time.js";
import { useTranslationContext } from "../context/translation.js";
import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
+import { Pages } from "../NavigationBar.js";
import * as wxApi from "../wxApi.js";
interface Props {
@@ -344,6 +344,17 @@ export function TransactionView({
Amounts.parseOrThrow(transaction.amountRaw),
).amount;
+ const refundFee = Amounts.sub(
+ Amounts.parseOrThrow(transaction.totalRefundRaw),
+ Amounts.parseOrThrow(transaction.totalRefundEffective),
+ ).amount;
+ const refunded = Amounts.isNonZero(
+ Amounts.parseOrThrow(transaction.totalRefundRaw),
+ );
+ const pendingRefund =
+ transaction.refundPending === undefined
+ ? undefined
+ : Amounts.parseOrThrow(transaction.refundPending);
return (
@@ -360,18 +371,54 @@ export function TransactionView({
text={}
kind="negative"
/>
- Purchase amount}
- text={}
- kind="neutral"
- />
- Fee}
- text={}
- kind="negative"
- />
+ {Amounts.isNonZero(fee) && (
+
+ Purchase amount}
+ text={}
+ kind="neutral"
+ />
+ Purchase Fee}
+ text={}
+ kind="negative"
+ />
+
+ )}
+ {refunded && (
+
+ Total refunded}
+ text={}
+ kind="positive"
+ />
+ {Amounts.isNonZero(refundFee) && (
+
+ Refund amount}
+ text={}
+ kind="neutral"
+ />
+ Refund fee}
+ text={}
+ kind="negative"
+ />
+
+ )}
+
+ )}
+ {pendingRefund !== undefined && Amounts.isNonZero(pendingRefund) && (
+ Refund pending}
+ text={}
+ kind="positive"
+ />
+ )}
Merchant}
text={transaction.info.merchant.name}
@@ -447,18 +494,22 @@ export function TransactionView({
text={}
kind="neutral"
/>
- Deposit amount}
- text={}
- kind="positive"
- />
- Fee}
- text={}
- kind="negative"
- />
+ {Amounts.isNonZero(fee) && (
+
+ Deposit amount}
+ text={}
+ kind="positive"
+ />
+ Fee}
+ text={}
+ kind="negative"
+ />
+
+ )}
{payto && }
);
@@ -485,18 +536,22 @@ export function TransactionView({
text={}
kind="negative"
/>
- Refresh amount}
- text={}
- kind="neutral"
- />
- Fee}
- text={}
- kind="negative"
- />
+ {Amounts.isNonZero(fee) && (
+
+ Refresh amount}
+ text={}
+ kind="neutral"
+ />
+ Fee}
+ text={}
+ kind="negative"
+ />
+
+ )}
);
}
@@ -522,18 +577,22 @@ export function TransactionView({
text={}
kind="positive"
/>
- Received amount}
- text={}
- kind="neutral"
- />
- Fee}
- text={}
- kind="negative"
- />
+ {Amounts.isNonZero(fee) && (
+
+ Received amount}
+ text={}
+ kind="neutral"
+ />
+ Fee}
+ text={}
+ kind="negative"
+ />
+
+ )}
);
}
@@ -559,37 +618,42 @@ export function TransactionView({
text={}
kind="positive"
/>
- Refund amount}
- text={}
- kind="neutral"
- />
- Fee}
- text={}
- kind="negative"
- />
+ {Amounts.isNonZero(fee) && (
+
+ Refund amount}
+ text={}
+ kind="neutral"
+ />
+ Fee}
+ text={}
+ kind="negative"
+ />
+
+ )}
Merchant}
text={transaction.info.merchant.name}
kind="neutral"
/>
+
Purchase}
text={
- transaction.info.fulfillmentUrl ? (
-
- {transaction.info.summary}
-
- ) : (
- transaction.info.summary
- )
+
+ {transaction.info.summary}
+
}
kind="neutral"
/>