commit ecd2aee1818eb76240608ed3d92f64f8bb5598ec
parent 729fa8b322e181728838385605521e1c9eb3babb
Author: Florian Dold <dold@taler.net>
Date: Tue, 3 Nov 2026 23:17:59 +0100
merchant web UI: invalidate order, transfer and admin caches
Diffstat:
5 files changed, 147 insertions(+), 3 deletions(-)
diff --git a/packages/taler-merchant-webui/src/api/hooks/useManagedInstances.ts b/packages/taler-merchant-webui/src/api/hooks/useManagedInstances.ts
@@ -7,7 +7,7 @@
Foundation; either version 3, or (at your option) any later version.
*/
-import useSWR from "swr";
+import useSWR, { mutate as mutateCache } from "swr";
import {
AccessToken,
HttpStatusCode,
@@ -105,6 +105,16 @@ export function useManagedInstances(options: { enabled?: boolean } = {}) {
const challengeResponse = challengeFrom(result);
if (challengeResponse) return { challengeResponse };
unwrapEmpty(result as never);
+ await mutateCache(
+ [
+ "managementInstance",
+ currentConfig.rootUrl.href,
+ instanceId,
+ currentToken,
+ ],
+ undefined,
+ { revalidate: true },
+ );
await swr.mutate();
return { ok: undefined };
};
@@ -157,6 +167,11 @@ export function useManagedInstance(instanceId: string | undefined) {
const challengeResponse = challengeFrom(result);
if (challengeResponse) return { challengeResponse };
unwrapEmpty(result as never);
+ await mutateCache(
+ ["managementInstances", currentConfig.rootUrl.href, currentToken],
+ undefined,
+ { revalidate: true },
+ );
await swr.mutate();
return { ok: undefined };
};
diff --git a/packages/taler-merchant-webui/src/api/hooks/useOrders.ts b/packages/taler-merchant-webui/src/api/hooks/useOrders.ts
@@ -35,6 +35,7 @@ import type {
OrderPaymentChoice,
} from "../../types/domain.js";
import { getClientConfig, isNonZeroAmount } from "./common.js";
+import { invalidateMerchantCache } from "../cache.js";
import { remoteResource } from "../contracts.js";
import { useLiveRequest } from "../liveRequest.js";
@@ -700,6 +701,8 @@ export function useOrderDetails(orderId?: string) {
const client = merchantClient(config);
const res = await client.deleteOrder(token as AccessToken, orderId, force);
unwrapEmpty(res);
+ await invalidateMerchantCache(config, token, "getOrderDetails", orderId);
+ await invalidateMerchantCache(config, token, "listOrders");
};
const issueRefund = async (amount: string, reason: string) => {
@@ -710,6 +713,7 @@ export function useOrderDetails(orderId?: string) {
reason,
});
unwrap(res);
+ await invalidateMerchantCache(config, token, "listOrders");
await mutate();
};
diff --git a/packages/taler-merchant-webui/src/api/hooks/useTransfersAndKyc.ts b/packages/taler-merchant-webui/src/api/hooks/useTransfersAndKyc.ts
@@ -15,6 +15,7 @@
*/
import { useMerchantSWR as useSWR } from "../swr.js";
+import { invalidateMerchantCache } from "../cache.js";
import { useEffect, useRef } from "preact/hooks";
import type { AccessToken, TalerMerchantApi } from "@gnu-taler/taler-util";
import { Amounts, HttpStatusCode, Paytos } from "@gnu-taler/taler-util";
@@ -115,7 +116,7 @@ export function useIncomingTransfers(paytoUri?: string) {
};
const res = await client.informWireTransfer(token as AccessToken, body);
unwrapEmpty(res);
- await mutate();
+ await invalidateMerchantCache(config, token, "merchant-incoming-transfers");
};
const unconfirmTransfer = async (
@@ -139,7 +140,7 @@ export function useIncomingTransfers(paytoUri?: string) {
transfer.transfer_serial_id,
);
unwrapEmpty(res);
- await mutate();
+ await invalidateMerchantCache(config, token, "merchant-incoming-transfers");
};
return {
diff --git a/packages/taler-merchant-webui/src/api/mutationCache.test.tsx b/packages/taler-merchant-webui/src/api/mutationCache.test.tsx
@@ -0,0 +1,121 @@
+/*
+ This file is part of GNU Taler
+ (C) 2026 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+ */
+
+import assert from "node:assert/strict";
+import { test } from "node:test";
+import { render } from "preact";
+import { act } from "preact/test-utils";
+import { mutate } from "swr";
+import { seedSWRCache as seed } from "../testing/swr-cache.js";
+import type { TalerMerchantApi } from "@gnu-taler/taler-util";
+import { useOrderDetails } from "./hooks/useOrders.js";
+import { useIncomingTransfers } from "./hooks/useTransfersAndKyc.js";
+import {
+ useManagedInstance,
+ useManagedInstances,
+} from "./hooks/useManagedInstances.js";
+import { useHttpLibForTesting } from "./client.js";
+import { FakeHttpLib, noContent, ok } from "../testing/fake-http.js";
+import { signIn, signOut } from "../stores/session.js";
+
+for (const operation of [
+ "refund",
+ "delete-order",
+ "confirm-transfer",
+ "update-account",
+ "delete-account",
+] as const) {
+ for (const failed of [false, true]) {
+ test(`${operation} refreshes related views only on success (${failed ? "failure" : "success"})`, async () => {
+ const backend = `https://${operation}-${failed}.example.test/`;
+ const token = "secret-token:admin";
+ const key = (resource: string, suffix: string) => [
+ resource,
+ backend,
+ "admin",
+ token,
+ suffix,
+ ];
+ const affected =
+ operation === "update-account"
+ ? [["managementInstances", backend, token]]
+ : operation === "delete-account"
+ ? [["managementInstance", backend, "shop", token]]
+ : operation === "confirm-transfer"
+ ? [
+ key("merchant-incoming-transfers", "all"),
+ key("merchant-incoming-transfers", "payto://iban/DE123"),
+ ]
+ : [
+ key("listOrders", "all"),
+ key("listOrders", "refunded"),
+ ...(operation === "delete-order"
+ ? [key("getOrderDetails", "item")]
+ : []),
+ ];
+ for (const k of affected) await seed(k, { old: true });
+ const http = new FakeHttpLib().handle((request) => {
+ if (failed) throw new Error("write rejected");
+ if (operation === "refund")
+ return ok({
+ taler_refund_uri: "taler://refund/example.test/item/",
+ h_contract: "hash",
+ });
+ return noContent();
+ });
+ const restore = useHttpLibForTesting(http);
+ signIn("admin", token, backend);
+ const container = document.body.appendChild(
+ document.createElement("div"),
+ );
+ let run!: () => Promise<unknown>;
+ function Probe() {
+ const order = useOrderDetails("item");
+ const transfers = useIncomingTransfers();
+ const account = useManagedInstance("shop");
+ const accounts = useManagedInstances({ enabled: false });
+ run = {
+ refund: () => order.issueRefund("EUR:1", "Test"),
+ "delete-order": () => order.deleteOrder(),
+ "confirm-transfer": () =>
+ transfers.confirmTransfer({
+ expected_credit_amount: "EUR:1",
+ wtid: "wire",
+ payto_uri: "payto://iban/DE123",
+ exchange_url: "https://exchange.example.test/",
+ } as TalerMerchantApi.ExpectedTransferEntry),
+ "update-account": () =>
+ account.updateInstance({
+ name: "New",
+ address: {},
+ jurisdiction: {},
+ use_stefan: false,
+ }),
+ "delete-account": () => accounts.deleteInstance("shop", false),
+ }[operation];
+ return null;
+ }
+ try {
+ await act(async () => render(<Probe />, container));
+ if (failed) await assert.rejects(run(), /write rejected/);
+ else
+ await act(async () => {
+ await run();
+ });
+ for (const k of affected)
+ assert.deepEqual(await mutate(k), failed ? { old: true } : undefined);
+ } finally {
+ act(() => render(null, container));
+ container.remove();
+ restore();
+ signOut();
+ }
+ });
+ }
+}
diff --git a/packages/taler-merchant-webui/src/routes/PosRoute.tsx b/packages/taler-merchant-webui/src/routes/PosRoute.tsx
@@ -25,6 +25,7 @@ import {
useOrderDetails,
useTokenFamilies,
} from "../api/hooks.js";
+import { invalidateMerchantCache } from "../api/cache.js";
import { unwrap } from "../api/failure.js";
import {
executeCreateOrder,
@@ -71,6 +72,8 @@ export async function issuePosRefund(
if (!refund) return false;
const client = merchantClient(config);
unwrap(await client.addRefund(token, orderId, { refund, reason }));
+ await invalidateMerchantCache(config, token, "getOrderDetails", orderId);
+ await invalidateMerchantCache(config, token, "listOrders");
return true;
}