gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-wallet-core] branch master updated: feat: awaiting refund


From: gnunet
Subject: [taler-wallet-core] branch master updated: feat: awaiting refund
Date: Sat, 14 May 2022 23:09:57 +0200

This is an automated email from the git hooks/post-receive script.

sebasjm pushed a commit to branch master
in repository wallet-core.

The following commit(s) were added to refs/heads/master by this push:
     new e4ea2019 feat: awaiting refund
e4ea2019 is described below

commit e4ea2019430fb3c4b788f67427fbd743f604b7e5
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Sat May 14 18:09:33 2022 -0300

    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 +-
 packages/taler-wallet-core/src/operations/pay.ts   |  23 ++-
 .../taler-wallet-core/src/operations/refund.ts     | 185 ++++++++++--------
 .../src/operations/transactions.ts                 |  85 +++++----
 packages/taler-wallet-core/src/wallet.ts           |   4 +-
 packages/taler-wallet-webextension/compile_core.sh |   2 +
 packages/taler-wallet-webextension/compile_util.sh |   3 +
 .../src/cta/Refund.stories.tsx                     |   8 +-
 .../src/cta/Refund.test.ts                         |  53 ++++--
 .../taler-wallet-webextension/src/cta/Refund.tsx   |  70 +++++--
 .../src/wallet/History.stories.tsx                 |   4 +
 .../src/wallet/Transaction.stories.tsx             |   4 +
 .../src/wallet/Transaction.tsx                     | 208 ++++++++++++++-------
 18 files changed, 478 insertions(+), 262 deletions(-)

diff --git a/packages/taler-util/src/talerTypes.ts 
b/packages/taler-util/src/talerTypes.ts
index b21c6cae..d9213ef5 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<BlindedDenominationSignature>()
-    .discriminateOn("cipher")
-    .alternative(DenomKeyType.Rsa, codecForRsaBlindedDenominationSignature())
-    .build("BlindedDenominationSignature");
-
 export const codecForRsaBlindedDenominationSignature = () =>
   buildCodecForObject<RsaBlindedDenominationSignature>()
     .property("cipher", codecForConstString(DenomKeyType.Rsa))
     .property("blinded_rsa_signature", codecForString())
     .build("RsaBlindedDenominationSignature");
 
+export const codecForBlindedDenominationSignature = () =>
+  buildCodecForUnion<BlindedDenominationSignature>()
+    .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<MerchantOrderStatusPaid> =>
     buildCodecForObject<MerchantOrderStatusPaid>()
       .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 b9a227b6..37c1c7ef 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 a8946fbb..fa884c41 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 35062b57..8b10bb74 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<PostOrderResponse> =>
     .property("token", codecOptional(codecForString()))
     .build("PostOrderResponse");
 
+
+export const codecForRefundDetails = (): Codec<RefundDetails> =>
+  buildCodecForObject<RefundDetails>()
+    .property("reason", codecForString())
+    .property("pending", codecForBoolean())
+    .property("amount", codecForString())
+    .property("timestamp", codecForTimestamp)
+    .build("PostOrderResponse");
+
 export const codecForCheckPaymentPaidResponse =
   (): Codec<CheckPaymentPaidResponse> =>
     buildCodecForObject<CheckPaymentPaidResponse>()
@@ -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 e8c46c7e..8fe1937a 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 37e97fbc..a0a603ca 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 db157257..325d07bd 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 dad8c600..e5ce37a8 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 0a354945..87b109d9 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 053a0763..905d9220 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 00000000..b93d543f
--- /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 00000000..df2c4125
--- /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 6b7cf462..bc293989 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 e77f8e68..864b4f12 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 f69fc431..5387a178 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 {
           <Part
             big
             title={<i18n.Translate>Total to refund</i18n.Translate>}
+            text={<Amount value={state.awaitingAmount} />}
+            kind="negative"
+          />
+          <Part
+            big
+            title={<i18n.Translate>Refunded</i18n.Translate>}
             text={<Amount value={state.amount} />}
             kind="negative"
           />
@@ -108,9 +113,9 @@ export function View({ state }: ViewProps): VNode {
             <ProductList products={state.products} />
           </section>
         ) : undefined}
-        <section>
+        {/* <section>
           <ProgressBar value={state.progress} />
-        </section>
+        </section> */}
       </WalletAction>
     );
   }
@@ -128,6 +133,14 @@ export function View({ state }: ViewProps): VNode {
             <i18n.Translate>this refund is already accepted.</i18n.Translate>
           </p>
         </section>
+        <section>
+          <Part
+            big
+            title={<i18n.Translate>Total to refunded</i18n.Translate>}
+            text={<Amount value={state.granted} />}
+            kind="negative"
+          />
+        </section>
       </WalletAction>
     );
   }
@@ -150,9 +163,23 @@ export function View({ state }: ViewProps): VNode {
       <section>
         <Part
           big
-          title={<i18n.Translate>Total to refund</i18n.Translate>}
+          title={<i18n.Translate>Order amount</i18n.Translate>}
           text={<Amount value={state.amount} />}
-          kind="negative"
+          kind="neutral"
+        />
+        {Amounts.isNonZero(state.granted) && (
+          <Part
+            big
+            title={<i18n.Translate>Already refunded</i18n.Translate>}
+            text={<Amount value={state.granted} />}
+            kind="neutral"
+          />
+        )}
+        <Part
+          big
+          title={<i18n.Translate>Refund offered</i18n.Translate>}
+          text={<Amount value={state.awaitingAmount} />}
+          kind="positive"
         />
       </section>
       {state.products && state.products.length ? (
@@ -164,9 +191,6 @@ export function View({ state }: ViewProps): VNode {
         <ButtonSuccess onClick={state.accept.onClick}>
           <i18n.Translate>Confirm refund</i18n.Translate>
         </ButtonSuccess>
-        <Button onClick={state.ignore.onClick}>
-          <i18n.Translate>Ignore</i18n.Translate>
-        </Button>
       </section>
     </WalletAction>
   );
@@ -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) {
+  if (Amounts.isZero(awaitingAmount)) {
     return {
-      status: "in-progress",
+      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,
-      progress: (refund.applied + refund.failed) / refund.total,
     };
   }
 
-  if (completed) {
+  if (refund.pending) {
     return {
-      status: "completed",
+      status: "in-progress",
       hook: undefined,
-      amount: Amounts.parseOrThrow(info.response.refund.amountEffectivePaid),
+      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 92f1dea1..3080a866 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 b4dfb6ce..f162543a 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 bcf6114a..3377f98c 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 (
       <TransactionTemplate>
         <SubTitle>
@@ -360,18 +371,54 @@ export function TransactionView({
           text={<Amount value={transaction.amountEffective} />}
           kind="negative"
         />
-        <Part
-          big
-          title={<i18n.Translate>Purchase amount</i18n.Translate>}
-          text={<Amount value={transaction.amountRaw} />}
-          kind="neutral"
-        />
-        <Part
-          big
-          title={<i18n.Translate>Fee</i18n.Translate>}
-          text={<Amount value={fee} />}
-          kind="negative"
-        />
+        {Amounts.isNonZero(fee) && (
+          <Fragment>
+            <Part
+              big
+              title={<i18n.Translate>Purchase amount</i18n.Translate>}
+              text={<Amount value={transaction.amountRaw} />}
+              kind="neutral"
+            />
+            <Part
+              title={<i18n.Translate>Purchase Fee</i18n.Translate>}
+              text={<Amount value={fee} />}
+              kind="negative"
+            />
+          </Fragment>
+        )}
+        {refunded && (
+          <Fragment>
+            <Part
+              big
+              title={<i18n.Translate>Total refunded</i18n.Translate>}
+              text={<Amount value={transaction.totalRefundEffective} />}
+              kind="positive"
+            />
+            {Amounts.isNonZero(refundFee) && (
+              <Fragment>
+                <Part
+                  big
+                  title={<i18n.Translate>Refund amount</i18n.Translate>}
+                  text={<Amount value={transaction.totalRefundRaw} />}
+                  kind="neutral"
+                />
+                <Part
+                  title={<i18n.Translate>Refund fee</i18n.Translate>}
+                  text={<Amount value={refundFee} />}
+                  kind="negative"
+                />
+              </Fragment>
+            )}
+          </Fragment>
+        )}
+        {pendingRefund !== undefined && Amounts.isNonZero(pendingRefund) && (
+          <Part
+            big
+            title={<i18n.Translate>Refund pending</i18n.Translate>}
+            text={<Amount value={pendingRefund} />}
+            kind="positive"
+          />
+        )}
         <Part
           title={<i18n.Translate>Merchant</i18n.Translate>}
           text={transaction.info.merchant.name}
@@ -447,18 +494,22 @@ export function TransactionView({
           text={<Amount value={transaction.amountEffective} />}
           kind="neutral"
         />
-        <Part
-          big
-          title={<i18n.Translate>Deposit amount</i18n.Translate>}
-          text={<Amount value={transaction.amountRaw} />}
-          kind="positive"
-        />
-        <Part
-          big
-          title={<i18n.Translate>Fee</i18n.Translate>}
-          text={<Amount value={fee} />}
-          kind="negative"
-        />
+        {Amounts.isNonZero(fee) && (
+          <Fragment>
+            <Part
+              big
+              title={<i18n.Translate>Deposit amount</i18n.Translate>}
+              text={<Amount value={transaction.amountRaw} />}
+              kind="positive"
+            />
+            <Part
+              big
+              title={<i18n.Translate>Fee</i18n.Translate>}
+              text={<Amount value={fee} />}
+              kind="negative"
+            />
+          </Fragment>
+        )}
         {payto && <PartPayto big payto={payto} kind="neutral" />}
       </TransactionTemplate>
     );
@@ -485,18 +536,22 @@ export function TransactionView({
           text={<Amount value={transaction.amountEffective} />}
           kind="negative"
         />
-        <Part
-          big
-          title={<i18n.Translate>Refresh amount</i18n.Translate>}
-          text={<Amount value={transaction.amountRaw} />}
-          kind="neutral"
-        />
-        <Part
-          big
-          title={<i18n.Translate>Fee</i18n.Translate>}
-          text={<Amount value={fee} />}
-          kind="negative"
-        />
+        {Amounts.isNonZero(fee) && (
+          <Fragment>
+            <Part
+              big
+              title={<i18n.Translate>Refresh amount</i18n.Translate>}
+              text={<Amount value={transaction.amountRaw} />}
+              kind="neutral"
+            />
+            <Part
+              big
+              title={<i18n.Translate>Fee</i18n.Translate>}
+              text={<Amount value={fee} />}
+              kind="negative"
+            />
+          </Fragment>
+        )}
       </TransactionTemplate>
     );
   }
@@ -522,18 +577,22 @@ export function TransactionView({
           text={<Amount value={transaction.amountRaw} />}
           kind="positive"
         />
-        <Part
-          big
-          title={<i18n.Translate>Received amount</i18n.Translate>}
-          text={<Amount value={transaction.amountEffective} />}
-          kind="neutral"
-        />
-        <Part
-          big
-          title={<i18n.Translate>Fee</i18n.Translate>}
-          text={<Amount value={fee} />}
-          kind="negative"
-        />
+        {Amounts.isNonZero(fee) && (
+          <Fragment>
+            <Part
+              big
+              title={<i18n.Translate>Received amount</i18n.Translate>}
+              text={<Amount value={transaction.amountEffective} />}
+              kind="neutral"
+            />
+            <Part
+              big
+              title={<i18n.Translate>Fee</i18n.Translate>}
+              text={<Amount value={fee} />}
+              kind="negative"
+            />
+          </Fragment>
+        )}
       </TransactionTemplate>
     );
   }
@@ -559,37 +618,42 @@ export function TransactionView({
           text={<Amount value={transaction.amountEffective} />}
           kind="positive"
         />
-        <Part
-          big
-          title={<i18n.Translate>Refund amount</i18n.Translate>}
-          text={<Amount value={transaction.amountRaw} />}
-          kind="neutral"
-        />
-        <Part
-          big
-          title={<i18n.Translate>Fee</i18n.Translate>}
-          text={<Amount value={fee} />}
-          kind="negative"
-        />
+        {Amounts.isNonZero(fee) && (
+          <Fragment>
+            <Part
+              big
+              title={<i18n.Translate>Refund amount</i18n.Translate>}
+              text={<Amount value={transaction.amountRaw} />}
+              kind="neutral"
+            />
+            <Part
+              big
+              title={<i18n.Translate>Fee</i18n.Translate>}
+              text={<Amount value={fee} />}
+              kind="negative"
+            />
+          </Fragment>
+        )}
         <Part
           title={<i18n.Translate>Merchant</i18n.Translate>}
           text={transaction.info.merchant.name}
           kind="neutral"
         />
+
         <Part
           title={<i18n.Translate>Purchase</i18n.Translate>}
           text={
-            transaction.info.fulfillmentUrl ? (
-              <a
-                href={transaction.info.fulfillmentUrl}
-                target="_bank"
-                rel="noreferrer"
-              >
-                {transaction.info.summary}
-              </a>
-            ) : (
-              transaction.info.summary
-            )
+            <a
+              href={Pages.balance_transaction.replace(
+                ":tid",
+                transaction.refundedTransactionId,
+              )}
+              // href={transaction.info.fulfillmentUrl}
+              // target="_bank"
+              // rel="noreferrer"
+            >
+              {transaction.info.summary}
+            </a>
           }
           kind="neutral"
         />

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]