taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

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:
Mpackages/taler-merchant-webui/src/api/hooks/useManagedInstances.ts | 17++++++++++++++++-
Mpackages/taler-merchant-webui/src/api/hooks/useOrders.ts | 4++++
Mpackages/taler-merchant-webui/src/api/hooks/useTransfersAndKyc.ts | 5+++--
Apackages/taler-merchant-webui/src/api/mutationCache.test.tsx | 121+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-merchant-webui/src/routes/PosRoute.tsx | 3+++
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; }