diff options
Diffstat (limited to 'packages/auditor-backoffice-ui/src/hooks')
26 files changed, 428 insertions, 5718 deletions
diff --git a/packages/auditor-backoffice-ui/src/hooks/async.ts b/packages/auditor-backoffice-ui/src/hooks/async.ts deleted file mode 100644 index f22badc88..000000000 --- a/packages/auditor-backoffice-ui/src/hooks/async.ts +++ /dev/null @@ -1,77 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ -import { useState } from "preact/hooks"; - -export interface Options { - slowTolerance: number; -} - -export interface AsyncOperationApi<T> { - request: (...a: any) => void; - cancel: () => void; - data: T | undefined; - isSlow: boolean; - isLoading: boolean; - error: string | undefined; -} - -export function useAsync<T>( - fn?: (...args: any) => Promise<T>, - { slowTolerance: tooLong }: Options = { slowTolerance: 1000 }, -): AsyncOperationApi<T> { - const [data, setData] = useState<T | undefined>(undefined); - const [isLoading, setLoading] = useState<boolean>(false); - const [error, setError] = useState<any>(undefined); - const [isSlow, setSlow] = useState(false); - - const request = async (...args: any) => { - if (!fn) return; - setLoading(true); - - const handler = setTimeout(() => { - setSlow(true); - }, tooLong); - - try { - const result = await fn(...args); - setData(result); - } catch (error) { - setError(error); - } - setLoading(false); - setSlow(false); - clearTimeout(handler); - }; - - function cancel(): void { - setLoading(false); - setSlow(false); - } - - return { - request, - cancel, - data, - isSlow, - isLoading, - error, - }; -} diff --git a/packages/auditor-backoffice-ui/src/hooks/backend.ts b/packages/auditor-backoffice-ui/src/hooks/backend.ts index 8d99546a8..69b63e02b 100644 --- a/packages/auditor-backoffice-ui/src/hooks/backend.ts +++ b/packages/auditor-backoffice-ui/src/hooks/backend.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (C) 2021-2024 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 @@ -17,12 +17,13 @@ /** * * @author Sebastian Javier Marchano (sebasjm) + * @author Nic Eigel */ -import { AbsoluteTime, HttpStatusCode } from "@gnu-taler/taler-util"; +/** + * Imports. + */ import { - ErrorType, - HttpError, HttpResponse, HttpResponseOk, RequestError, @@ -32,9 +33,7 @@ import { import { useCallback, useEffect, useState } from "preact/hooks"; import { useSWRConfig } from "swr"; import { useBackendContext } from "../context/backend.js"; -import { useInstanceContext } from "../context/instance.js"; -import { AccessToken, LoginToken, MerchantBackend, Timestamp } from "../declaration.js"; - +import { AuditorBackend } from "../declaration.js"; export function useMatchMutate(): ( re?: RegExp, @@ -49,78 +48,111 @@ export function useMatchMutate(): ( } return function matchRegexMutate(re?: RegExp) { - return mutate((key) => { - // evict if no key or regex === all - if (!key || !re) return true - // match string - if (typeof key === 'string' && re.test(key)) return true - // record or object have the path at [0] - if (typeof key === 'object' && re.test(key[0])) return true - //key didn't match regex - return false - }, undefined, { - revalidate: true, - }); + return mutate( + (key) => { + // evict if no key or regex === all + if (!key || !re) return true; + // match string + if (typeof key === "string" && re.test(key)) return true; + // record or object have the path at [0] + if (typeof key === "object" && re.test(key[0])) return true; + //key didn't match regex + return false; + }, + undefined, + { + revalidate: true, + }, + ); }; } -export function useBackendInstancesTestForAdmin(): HttpResponse< - MerchantBackend.Instances.InstancesResponse, - MerchantBackend.ErrorDetail +const CHECK_CONFIG_INTERVAL_OK = 5 * 60 * 1000; +const CHECK_CONFIG_INTERVAL_FAIL = 2 * 1000; + +export function useBackendConfig(): HttpResponse< + AuditorBackend.VersionResponse | undefined, + RequestError<AuditorBackend.ErrorDetail> > { const { request } = useBackendBaseRequest(); - type Type = MerchantBackend.Instances.InstancesResponse; - - const [result, setResult] = useState< - HttpResponse<Type, MerchantBackend.ErrorDetail> - >({ loading: true }); + type Type = AuditorBackend.VersionResponse; + type State = { + data: HttpResponse<Type, RequestError<AuditorBackend.ErrorDetail>>; + timer: number; + }; + const [result, setResult] = useState<State>({ + data: { loading: true }, + timer: 0, + }); useEffect(() => { - request<Type>(`/management/instances`) - .then((data) => setResult(data)) - .catch((error: RequestError<MerchantBackend.ErrorDetail>) => - setResult(error.cause), - ); + if (result.timer) { + clearTimeout(result.timer); + } + + function tryConfig(): void { + request<Type>("config") + .then((data) => { + const timer: any = setTimeout(() => { + tryConfig(); + }, CHECK_CONFIG_INTERVAL_OK); + setResult({ data, timer }); + }) + .catch((error) => { + const timer: any = setTimeout(() => { + tryConfig(); + }, CHECK_CONFIG_INTERVAL_FAIL); + const data = error.cause; + setResult({ data, timer }); + }); + } + + tryConfig(); }, [request]); - return result; + return result.data; } -const CHECK_CONFIG_INTERVAL_OK = 5 * 60 * 1000; -const CHECK_CONFIG_INTERVAL_FAIL = 2 * 1000; - -export function useBackendConfig(): HttpResponse< - MerchantBackend.VersionResponse | undefined, - RequestError<MerchantBackend.ErrorDetail> +export function useBackendToken(): HttpResponse< + AuditorBackend.VersionResponse, + RequestError<AuditorBackend.ErrorDetail> > { const { request } = useBackendBaseRequest(); - type Type = MerchantBackend.VersionResponse; - type State = { data: HttpResponse<Type, RequestError<MerchantBackend.ErrorDetail>>, timer: number } - const [result, setResult] = useState<State>({ data: { loading: true }, timer: 0 }); + type Type = AuditorBackend.VersionResponse; + type State = { + data: HttpResponse<Type, RequestError<AuditorBackend.ErrorDetail>>; + timer: number; + }; + const [result, setResult] = useState<State>({ + data: { loading: true }, + timer: 0, + }); useEffect(() => { if (result.timer) { - clearTimeout(result.timer) + clearTimeout(result.timer); } - function tryConfig(): void { - request<Type>(`/config`) + + function tryToken(): void { + request<Type>(`/monitoring/balances`) .then((data) => { const timer: any = setTimeout(() => { - tryConfig() - }, CHECK_CONFIG_INTERVAL_OK) - setResult({ data, timer }) + tryToken(); + }, CHECK_CONFIG_INTERVAL_OK); + setResult({ data, timer }); }) .catch((error) => { const timer: any = setTimeout(() => { - tryConfig() - }, CHECK_CONFIG_INTERVAL_FAIL) - const data = error.cause - setResult({ data, timer }) + tryToken(); + }, CHECK_CONFIG_INTERVAL_FAIL); + const data = error.cause; + setResult({ data, timer }); }); } - tryConfig() + + tryToken(); }, [request]); return result.data; @@ -132,35 +164,9 @@ interface useBackendInstanceRequestType { options?: RequestOptions, ) => Promise<HttpResponseOk<T>>; fetcher: <T>(endpoint: string) => Promise<HttpResponseOk<T>>; - reserveDetailFetcher: <T>(endpoint: string) => Promise<HttpResponseOk<T>>; - rewardsDetailFetcher: <T>(endpoint: string) => Promise<HttpResponseOk<T>>; - multiFetcher: <T>(params: [url: string[]]) => Promise<HttpResponseOk<T>[]>; - orderFetcher: <T>( - params: [endpoint: string, - paid?: YesOrNo, - refunded?: YesOrNo, - wired?: YesOrNo, - searchDate?: Date, - delta?: number,] - ) => Promise<HttpResponseOk<T>>; - transferFetcher: <T>( - params: [endpoint: string, - payto_uri?: string, - verified?: string, - position?: string, - delta?: number,] - ) => Promise<HttpResponseOk<T>>; - templateFetcher: <T>( - params: [endpoint: string, - position?: string, - delta?: number] - ) => Promise<HttpResponseOk<T>>; - webhookFetcher: <T>( - params: [endpoint: string, - position?: string, - delta?: number] - ) => Promise<HttpResponseOk<T>>; + multiFetcher: <T>(params: string[]) => Promise<HttpResponseOk<T>[]>; } + interface useBackendBaseRequestType { request: <T>( endpoint: string, @@ -168,310 +174,74 @@ interface useBackendBaseRequestType { ) => Promise<HttpResponseOk<T>>; } -type YesOrNo = "yes" | "no"; -type LoginResult = { - valid: true; - token: string; - expiration: Timestamp; -} | { - valid: false; - cause: HttpError<{}>; -} - -export function useCredentialsChecker() { - const { request } = useApiContext(); - //check against instance details endpoint - //while merchant backend doesn't have a login endpoint - async function requestNewLoginToken( - baseUrl: string, - token: AccessToken, - ): Promise<LoginResult> { - const data: MerchantBackend.Instances.LoginTokenRequest = { - scope: "write", - duration: { - d_us: "forever" - }, - refreshable: true, - } - try { - const response = await request<MerchantBackend.Instances.LoginTokenSuccessResponse>(baseUrl, `/private/token`, { - method: "POST", - token, - data - }); - return { valid: true, token: response.data.token, expiration: response.data.expiration }; - } catch (error) { - if (error instanceof RequestError) { - return { valid: false, cause: error.cause }; - } - - return { - valid: false, cause: { - type: ErrorType.UNEXPECTED, - loading: false, - info: { - hasToken: true, - status: 0, - options: {}, - url: `/private/token`, - payload: {} - }, - exception: error, - message: (error instanceof Error ? error.message : "unpexepected error") - } - }; - } - }; - - async function refreshLoginToken( - baseUrl: string, - token: LoginToken - ): Promise<LoginResult> { - - if (AbsoluteTime.isExpired(AbsoluteTime.fromProtocolTimestamp(token.expiration))) { - return { - valid: false, cause: { - type: ErrorType.CLIENT, - status: HttpStatusCode.Unauthorized, - message: "login token expired, login again.", - info: { - hasToken: true, - status: 401, - options: {}, - url: `/private/token`, - payload: {} - }, - payload: {} - }, - } - } - - return requestNewLoginToken(baseUrl, token.token as AccessToken) - } - return { requestNewLoginToken, refreshLoginToken } -} - /** * * @param root the request is intended to the base URL and no the instance URL * @returns request handler to */ +//TODO: Add token export function useBackendBaseRequest(): useBackendBaseRequestType { - const { url: backend, token: loginToken } = useBackendContext(); + const { url: backend } = useBackendContext(); const { request: requestHandler } = useApiContext(); - const token = loginToken?.token; const request = useCallback( function requestImpl<T>( endpoint: string, + //todo: remove options: RequestOptions = {}, ): Promise<HttpResponseOk<T>> { - return requestHandler<T>(backend, endpoint, { ...options, token }).then(res => { - return res - }).catch(err => { - throw err - }); + return requestHandler<T>(backend, endpoint, { ...options }) + .then((res) => { + return res; + }) + .catch((err) => { + throw err; + }); }, - [backend, token], + [backend], ); return { request }; } -export function useBackendInstanceRequest(): useBackendInstanceRequestType { - const { url: rootBackendUrl, token: rootToken } = useBackendContext(); - const { token: instanceToken, id, admin } = useInstanceContext(); +export function useBackendRequest(): useBackendInstanceRequestType { + const { url: baseUrl } = useBackendContext(); const { request: requestHandler } = useApiContext(); - const { baseUrl, token: loginToken } = !admin - ? { baseUrl: rootBackendUrl, token: rootToken } - : { baseUrl: `${rootBackendUrl}/instances/${id}`, token: instanceToken }; - - const token = loginToken?.token; - const request = useCallback( function requestImpl<T>( endpoint: string, options: RequestOptions = {}, ): Promise<HttpResponseOk<T>> { - return requestHandler<T>(baseUrl, endpoint, { token, ...options }); + return requestHandler<T>(baseUrl, endpoint, { ...options }); }, - [baseUrl, token], + [baseUrl], ); const multiFetcher = useCallback( function multiFetcherImpl<T>( - args: [endpoints: string[]], + params: string[], + options: RequestOptions = {}, ): Promise<HttpResponseOk<T>[]> { - const [endpoints] = args return Promise.all( - endpoints.map((endpoint) => - requestHandler<T>(baseUrl, endpoint, { token }), + params.map((endpoint) => + requestHandler<T>(baseUrl, endpoint, { ...options }), ), ); }, - [baseUrl, token], + [baseUrl], ); const fetcher = useCallback( function fetcherImpl<T>(endpoint: string): Promise<HttpResponseOk<T>> { - return requestHandler<T>(baseUrl, endpoint, { token }); - }, - [baseUrl, token], - ); - - const orderFetcher = useCallback( - function orderFetcherImpl<T>( - args: [endpoint: string, - paid?: YesOrNo, - refunded?: YesOrNo, - wired?: YesOrNo, - searchDate?: Date, - delta?: number,] - ): Promise<HttpResponseOk<T>> { - const [endpoint, paid, refunded, wired, searchDate, delta] = args - const date_s = - delta && delta < 0 && searchDate - ? Math.floor(searchDate.getTime() / 1000) + 1 - : searchDate !== undefined ? Math.floor(searchDate.getTime() / 1000) : undefined; - const params: any = {}; - if (paid !== undefined) params.paid = paid; - if (delta !== undefined) params.delta = delta; - if (refunded !== undefined) params.refunded = refunded; - if (wired !== undefined) params.wired = wired; - if (date_s !== undefined) params.date_s = date_s; - if (delta === 0) { - //in this case we can already assume the response - //and avoid network - return Promise.resolve({ - ok: true, - data: { orders: [] } as T, - }) - } - return requestHandler<T>(baseUrl, endpoint, { params, token }); - }, - [baseUrl, token], - ); - - const reserveDetailFetcher = useCallback( - function reserveDetailFetcherImpl<T>( - endpoint: string, - ): Promise<HttpResponseOk<T>> { - return requestHandler<T>(baseUrl, endpoint, { - params: { - rewards: "yes", - }, - token, - }); - }, - [baseUrl, token], - ); - - const rewardsDetailFetcher = useCallback( - function rewardsDetailFetcherImpl<T>( - endpoint: string, - ): Promise<HttpResponseOk<T>> { - return requestHandler<T>(baseUrl, endpoint, { - params: { - pickups: "yes", - }, - token, - }); - }, - [baseUrl, token], - ); - - const transferFetcher = useCallback( - function transferFetcherImpl<T>( - args: [endpoint: string, - payto_uri?: string, - verified?: string, - position?: string, - delta?: number,] - ): Promise<HttpResponseOk<T>> { - const [endpoint, payto_uri, verified, position, delta] = args - const params: any = {}; - if (payto_uri !== undefined) params.payto_uri = payto_uri; - if (verified !== undefined) params.verified = verified; - if (delta === 0) { - //in this case we can already assume the response - //and avoid network - return Promise.resolve({ - ok: true, - data: { transfers: [] } as T, - }) - } - if (delta !== undefined) { - params.limit = delta; - } - if (position !== undefined) params.offset = position; - - return requestHandler<T>(baseUrl, endpoint, { params, token }); - }, - [baseUrl, token], - ); - - const templateFetcher = useCallback( - function templateFetcherImpl<T>( - args: [endpoint: string, - position?: string, - delta?: number,] - ): Promise<HttpResponseOk<T>> { - const [endpoint, position, delta] = args - const params: any = {}; - if (delta === 0) { - //in this case we can already assume the response - //and avoid network - return Promise.resolve({ - ok: true, - data: { templates: [] } as T, - }) - } - if (delta !== undefined) { - params.limit = delta; - } - if (position !== undefined) params.offset = position; - - return requestHandler<T>(baseUrl, endpoint, { params, token }); - }, - [baseUrl, token], - ); - - const webhookFetcher = useCallback( - function webhookFetcherImpl<T>( - args: [endpoint: string, - position?: string, - delta?: number,] - ): Promise<HttpResponseOk<T>> { - const [endpoint, position, delta] = args - const params: any = {}; - if (delta === 0) { - //in this case we can already assume the response - //and avoid network - return Promise.resolve({ - ok: true, - data: { webhooks: [] } as T, - }) - } - if (delta !== undefined) { - params.limit = delta; - } - if (position !== undefined) params.offset = position; - - return requestHandler<T>(baseUrl, endpoint, { params, token }); + return requestHandler<T>(baseUrl, endpoint, {}); }, - [baseUrl, token], + [baseUrl], ); return { request, fetcher, multiFetcher, - orderFetcher, - reserveDetailFetcher, - rewardsDetailFetcher, - transferFetcher, - templateFetcher, - webhookFetcher, }; } diff --git a/packages/auditor-backoffice-ui/src/hooks/bank.ts b/packages/auditor-backoffice-ui/src/hooks/bank.ts deleted file mode 100644 index 03b064646..000000000 --- a/packages/auditor-backoffice-ui/src/hooks/bank.ts +++ /dev/null @@ -1,217 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ -import { - HttpResponse, - HttpResponseOk, - HttpResponsePaginated, - RequestError, -} from "@gnu-taler/web-util/browser"; -import { useEffect, useState } from "preact/hooks"; -import { MerchantBackend } from "../declaration.js"; -import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js"; -import { useBackendInstanceRequest, useMatchMutate } from "./backend.js"; - -// FIX default import https://github.com/microsoft/TypeScript/issues/49189 -import _useSWR, { SWRHook } from "swr"; -const useSWR = _useSWR as unknown as SWRHook; - -// const MOCKED_ACCOUNTS: Record<string, MerchantBackend.BankAccounts.AccountAddDetails> = { -// "hwire1": { -// h_wire: "hwire1", -// payto_uri: "payto://fake/iban/123", -// salt: "qwe", -// }, -// "hwire2": { -// h_wire: "hwire2", -// payto_uri: "payto://fake/iban/123", -// salt: "qwe2", -// }, -// } - -export function useBankAccountAPI(): BankAccountAPI { - const mutateAll = useMatchMutate(); - const { request } = useBackendInstanceRequest(); - - const createBankAccount = async ( - data: MerchantBackend.BankAccounts.AccountAddDetails, - ): Promise<HttpResponseOk<void>> => { - // MOCKED_ACCOUNTS[data.h_wire] = data - // return Promise.resolve({ ok: true, data: undefined }); - const res = await request<void>(`/private/accounts`, { - method: "POST", - data, - }); - await mutateAll(/.*private\/accounts.*/); - return res; - }; - - const updateBankAccount = async ( - h_wire: string, - data: MerchantBackend.BankAccounts.AccountPatchDetails, - ): Promise<HttpResponseOk<void>> => { - // MOCKED_ACCOUNTS[h_wire].credit_facade_credentials = data.credit_facade_credentials - // MOCKED_ACCOUNTS[h_wire].credit_facade_url = data.credit_facade_url - // return Promise.resolve({ ok: true, data: undefined }); - const res = await request<void>(`/private/accounts/${h_wire}`, { - method: "PATCH", - data, - }); - await mutateAll(/.*private\/accounts.*/); - return res; - }; - - const deleteBankAccount = async ( - h_wire: string, - ): Promise<HttpResponseOk<void>> => { - // delete MOCKED_ACCOUNTS[h_wire] - // return Promise.resolve({ ok: true, data: undefined }); - const res = await request<void>(`/private/accounts/${h_wire}`, { - method: "DELETE", - }); - await mutateAll(/.*private\/accounts.*/); - return res; - }; - - return { - createBankAccount, - updateBankAccount, - deleteBankAccount, - }; -} - -export interface BankAccountAPI { - createBankAccount: ( - data: MerchantBackend.BankAccounts.AccountAddDetails, - ) => Promise<HttpResponseOk<void>>; - updateBankAccount: ( - id: string, - data: MerchantBackend.BankAccounts.AccountPatchDetails, - ) => Promise<HttpResponseOk<void>>; - deleteBankAccount: (id: string) => Promise<HttpResponseOk<void>>; -} - -export interface InstanceBankAccountFilter { -} - -export function useInstanceBankAccounts( - args?: InstanceBankAccountFilter, - updatePosition?: (id: string) => void, -): HttpResponsePaginated< - MerchantBackend.BankAccounts.AccountsSummaryResponse, - MerchantBackend.ErrorDetail -> { - // return { - // ok: true, - // loadMore() { }, - // loadMorePrev() { }, - // data: { - // accounts: Object.values(MOCKED_ACCOUNTS).map(e => ({ - // ...e, - // active: true, - // })) - // } - // } - const { fetcher } = useBackendInstanceRequest(); - - const [pageAfter, setPageAfter] = useState(1); - - const totalAfter = pageAfter * PAGE_SIZE; - const { - data: afterData, - error: afterError, - isValidating: loadingAfter, - } = useSWR< - HttpResponseOk<MerchantBackend.BankAccounts.AccountsSummaryResponse>, - RequestError<MerchantBackend.ErrorDetail> - >([`/private/accounts`], fetcher); - - const [lastAfter, setLastAfter] = useState< - HttpResponse< - MerchantBackend.BankAccounts.AccountsSummaryResponse, - MerchantBackend.ErrorDetail - > - >({ loading: true }); - useEffect(() => { - if (afterData) setLastAfter(afterData); - }, [afterData /*, beforeData*/]); - - if (afterError) return afterError.cause; - - // if the query returns less that we ask, then we have reach the end or beginning - const isReachingEnd = - afterData && afterData.data.accounts.length < totalAfter; - const isReachingStart = false; - - const pagination = { - isReachingEnd, - isReachingStart, - loadMore: () => { - if (!afterData || isReachingEnd) return; - if (afterData.data.accounts.length < MAX_RESULT_SIZE) { - setPageAfter(pageAfter + 1); - } else { - const from = `${afterData.data.accounts[afterData.data.accounts.length - 1] - .h_wire - }`; - if (from && updatePosition) updatePosition(from); - } - }, - loadMorePrev: () => { - }, - }; - - const accounts = !afterData ? [] : (afterData || lastAfter).data.accounts; - if (loadingAfter /* || loadingBefore */) - return { loading: true, data: { accounts } }; - if (/*beforeData &&*/ afterData) { - return { ok: true, data: { accounts }, ...pagination }; - } - return { loading: true }; -} - -export function useBankAccountDetails( - h_wire: string, -): HttpResponse< - MerchantBackend.BankAccounts.BankAccountEntry, - MerchantBackend.ErrorDetail -> { - // return { - // ok: true, - // data: { - // ...MOCKED_ACCOUNTS[h_wire], - // active: true, - // } - // } - const { fetcher } = useBackendInstanceRequest(); - - const { data, error, isValidating } = useSWR< - HttpResponseOk<MerchantBackend.BankAccounts.BankAccountEntry>, - RequestError<MerchantBackend.ErrorDetail> - >([`/private/accounts/${h_wire}`], fetcher, { - refreshInterval: 0, - refreshWhenHidden: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshWhenOffline: false, - }); - - if (isValidating) return { loading: true, data: data?.data }; - if (data) { - return data; - } - if (error) return error.cause; - return { loading: true }; -} diff --git a/packages/auditor-backoffice-ui/src/hooks/critical.ts b/packages/auditor-backoffice-ui/src/hooks/critical.ts new file mode 100644 index 000000000..8283fefbb --- /dev/null +++ b/packages/auditor-backoffice-ui/src/hooks/critical.ts @@ -0,0 +1,67 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 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. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +import { + HttpResponse, + HttpResponseOk, + RequestError, +} from "@gnu-taler/web-util/browser"; +import { AuditorBackend } from "../declaration.js"; +import { useBackendRequest } from "./backend.js"; + +// FIX default import https://github.com/microsoft/TypeScript/issues/49189 +import _useSWR, { SWRHook } from "swr"; + +const useSWR = _useSWR as unknown as SWRHook; + +type YesOrNo = "yes" | "no"; + +export interface HelperDashboardFilter { + finance?: YesOrNo; + security?: YesOrNo; + operating?: YesOrNo; + detail?: YesOrNo; +} + +export function getCriticalData( + args?: HelperDashboardFilter, + updateFilter?: (d: Date) => void, +): HttpResponse<any, AuditorBackend.ErrorDetail> { + const { multiFetcher } = useBackendRequest(); + const endpoints = [ + "monitoring/fee-time-inconsistency", + "monitoring/emergency", + "monitoring/emergency-by-count", + "monitoring/reserve-balance-insufficient-inconsistency", + ]; + + const { data: list, error: listError } = useSWR< + HttpResponseOk<any>[], + RequestError<AuditorBackend.ErrorDetail> + >(endpoints, multiFetcher, { + refreshInterval: 60, + refreshWhenHidden: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshWhenOffline: false, + }); + + if (listError) return listError.cause; + + if (list) { + return { ok: true, data: [list] }; + } + return { loading: true }; +} diff --git a/packages/auditor-backoffice-ui/src/hooks/deposit_confirmations.ts b/packages/auditor-backoffice-ui/src/hooks/deposit_confirmations.ts deleted file mode 100644 index e4ec9a2f2..000000000 --- a/packages/auditor-backoffice-ui/src/hooks/deposit_confirmations.ts +++ /dev/null @@ -1,161 +0,0 @@ -/*
- This file is part of GNU Taler
- (C) 2021-2024 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.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-import {
- HttpResponse,
- HttpResponseOk,
- RequestError,
-} from "@gnu-taler/web-util/browser";
-import { AuditorBackend, MerchantBackend, WithId } from "../declaration.js";
-import { useBackendInstanceRequest, useMatchMutate } from "./backend.js";
-
-// FIX default import https://github.com/microsoft/TypeScript/issues/49189
-import _useSWR, { SWRHook, useSWRConfig } from "swr";
-const useSWR = _useSWR as unknown as SWRHook;
-
-export interface DepositConfirmationAPI {
- getDepositConfirmation: (
- id: string,
- ) => Promise<void>;
- createDepositConfirmation: (
- data: MerchantBackend.Products.ProductAddDetail,
- ) => Promise<void>;
- updateDepositConfirmation: (
- id: string,
- data: MerchantBackend.Products.ProductPatchDetail,
- ) => Promise<void>;
- deleteDepositConfirmation: (id: string) => Promise<void>;
-}
-
-export function useDepositConfirmationAPI(): DepositConfirmationAPI {
- const mutateAll = useMatchMutate();
- const { mutate } = useSWRConfig();
-
- const { request } = useBackendInstanceRequest();
-
- const createDepositConfirmation = async (
- data: MerchantBackend.Products.ProductAddDetail,
- ): Promise<void> => {
- const res = await request(`/private/products`, {
- method: "POST",
- data,
- });
-
- return await mutateAll(/.*\/private\/products.*/);
- };
-
- const updateDepositConfirmation = async (
- productId: string,
- data: MerchantBackend.Products.ProductPatchDetail,
- ): Promise<void> => {
- const r = await request(`/private/products/${productId}`, {
- method: "PATCH",
- data,
- });
-
- return await mutateAll(/.*\/private\/products.*/);
- };
-
- const deleteDepositConfirmation = async (productId: string): Promise<void> => {
- await request(`/private/products/${productId}`, {
- method: "DELETE",
- });
- await mutate([`/private/products`]);
- };
-
- const getDepositConfirmation = async (
- serialId: string,
- ): Promise<void> => {
- await request(`/deposit-confirmation/${serialId}`, {
- method: "GET",
- });
-
- return
- };
-
- return {createDepositConfirmation, updateDepositConfirmation, deleteDepositConfirmation, getDepositConfirmation};
-}
-
-export function useDepositConfirmation(): HttpResponse<
- (AuditorBackend.DepositConfirmation.DepositConfirmationDetail & WithId)[],
- AuditorBackend.ErrorDetail
-> {
- const { fetcher, multiFetcher } = useBackendInstanceRequest();
-
- const { data: list, error: listError } = useSWR<
- HttpResponseOk<AuditorBackend.DepositConfirmation.DepositConfirmationList>,
- RequestError<AuditorBackend.ErrorDetail>
- >([`/deposit-confirmation`], fetcher, {
- refreshInterval: 0,
- refreshWhenHidden: false,
- revalidateOnFocus: false,
- revalidateOnReconnect: false,
- refreshWhenOffline: false,
- });
-
- const paths = (list?.data.depositConfirmations || []).map(
- (p) => `/deposit-confirmation/${p.serial_id}`,
- );
- const { data: depositConfirmations, error: depositConfirmationError } = useSWR<
- HttpResponseOk<AuditorBackend.DepositConfirmation.DepositConfirmationDetail>[],
- RequestError<AuditorBackend.ErrorDetail>
- >([paths], multiFetcher, {
- refreshInterval: 0,
- refreshWhenHidden: false,
- revalidateOnFocus: false,
- revalidateOnReconnect: false,
- refreshWhenOffline: false,
- });
-
- if (listError) return listError.cause;
- if (depositConfirmationError) return depositConfirmationError.cause;
-
- if (depositConfirmations) {
- const dataWithId = depositConfirmations.map((d) => {
- //take the id from the queried url
- return {
- ...d.data,
- id: d.info?.url.replace(/.*\/deposit-confirmation\//, "") || "",
- };
- });
- return { ok: true, data: dataWithId };
- }
- return { loading: true };
-}
-
-export function useDepositConfirmationDetails(
- serialId: string,
-): HttpResponse<
- AuditorBackend.DepositConfirmation.DepositConfirmationDetail,
- AuditorBackend.ErrorDetail
-> {
- const { fetcher } = useBackendInstanceRequest();
-
- const { data, error, isValidating } = useSWR<
- HttpResponseOk<AuditorBackend.DepositConfirmation.DepositConfirmationDetail>,
- RequestError<AuditorBackend.ErrorDetail>
- >([`/deposit-confirmation/${serialId}`], fetcher, {
- refreshInterval: 0,
- refreshWhenHidden: false,
- revalidateOnFocus: false,
- revalidateOnReconnect: false,
- refreshWhenOffline: false,
- });
-
- if (isValidating) return { loading: true, data: data?.data };
- if (data) return data;
- if (error) return error.cause;
- return { loading: true };
-}
diff --git a/packages/auditor-backoffice-ui/src/hooks/entity.ts b/packages/auditor-backoffice-ui/src/hooks/entity.ts new file mode 100644 index 000000000..3cfdd8616 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/hooks/entity.ts @@ -0,0 +1,83 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 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. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * Imports. + */ +import { + HttpResponse, + HttpResponseOk, + RequestError, +} from "@gnu-taler/web-util/browser"; +import { AuditorBackend } from "../declaration.js"; +import { useBackendRequest, useMatchMutate } from "./backend.js"; + +// FIX default import https://github.com/microsoft/TypeScript/issues/49189 +import _useSWR, { SWRHook } from "swr"; +import { useEntityContext } from "../context/entity.js"; + +const useSWR = _useSWR as unknown as SWRHook; + +interface Props { + endpoint: string; + entity: any; +} + +export function getEntityList({ + endpoint, + entity, +}: Props): HttpResponse<any, AuditorBackend.ErrorDetail> { + const { fetcher } = useBackendRequest(); + + const { data: list, error: listError } = useSWR< + HttpResponseOk<typeof entity>, + RequestError<AuditorBackend.ErrorDetail> + >([`monitoring/` + endpoint], fetcher, { + refreshInterval: 0, + refreshWhenHidden: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshWhenOffline: false, + }); + + if (listError) return listError.cause; + + if (list?.data != null) { + return { ok: true, data: [list?.data] }; + } + return { loading: true }; +} +export interface EntityAPI { + updateEntity: (id: string) => Promise<void>; +} + +export function useEntityAPI(): EntityAPI { + const mutateAll = useMatchMutate(); + const { request } = useBackendRequest(); + const { endpoint } = useEntityContext(); + const data = { suppressed: true }; + + const updateEntity = async (id: string): Promise<void> => { + const r = await request(`monitoring/${endpoint}/${id}`, { + method: "PATCH", + data, + }); + + return await mutateAll(/.*\/monitoring.*/); + }; + + return { updateEntity }; +} diff --git a/packages/auditor-backoffice-ui/src/hooks/finance.ts b/packages/auditor-backoffice-ui/src/hooks/finance.ts new file mode 100644 index 000000000..a0d035735 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/hooks/finance.ts @@ -0,0 +1,67 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 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. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * Imports. + */ +import { + HttpResponse, + HttpResponseOk, + RequestError, +} from "@gnu-taler/web-util/browser"; +import { AuditorBackend } from "../declaration.js"; +import { useBackendRequest } from "./backend.js"; + +// FIX default import https://github.com/microsoft/TypeScript/issues/49189 +import _useSWR, { SWRHook } from "swr"; + +const useSWR = _useSWR as unknown as SWRHook; + +export function getKeyFiguresData(): HttpResponse< + any, + AuditorBackend.ErrorDetail +> { + const { multiFetcher } = useBackendRequest(); + const endpoints = [ + "monitoring/misattribution-in-inconsistency", + "monitoring/coin-inconsistency", + "monitoring/reserve-in-inconsistency", + "monitoring/bad-sig-losses", + "monitoring/balances", + "monitoring/amount-arithmetic-inconsistency", + "monitoring/wire-format-inconsistency", + "monitoring/wire-out-inconsistency", + "monitoring/reserve-balance-summary-wrong-inconsistency", + ]; + + const { data: list, error: listError } = useSWR< + HttpResponseOk<any>[], + RequestError<AuditorBackend.ErrorDetail> + >(endpoints, multiFetcher, { + refreshInterval: 60, + refreshWhenHidden: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshWhenOffline: false, + }); + + if (listError) return listError.cause; + + if (list) { + return { ok: true, data: [list] }; + } + return { loading: true }; +} diff --git a/packages/auditor-backoffice-ui/src/hooks/index.ts b/packages/auditor-backoffice-ui/src/hooks/index.ts index 61afbc94a..b844c49cf 100644 --- a/packages/auditor-backoffice-ui/src/hooks/index.ts +++ b/packages/auditor-backoffice-ui/src/hooks/index.ts @@ -14,112 +14,41 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { buildCodecForObject, codecForMap, codecForString, codecForTimestamp } from "@gnu-taler/taler-util"; -import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser"; -import { StateUpdater, useEffect, useState } from "preact/hooks"; -import { LoginToken } from "../declaration.js"; +import { canonicalizeBaseUrl } from "@gnu-taler/taler-util"; +import { StateUpdater, useState } from "preact/hooks"; import { ValueOrFunction } from "../utils/types.js"; -import { useMatchMutate } from "./backend.js"; -const calculateRootPath = () => { - const rootPath = - typeof window !== undefined - ? window.location.origin + window.location.pathname - : "/"; - - /** - * By default, merchant backend serves the html content - * from the /webui root. This should cover most of the - * cases and the rootPath will be the merchant backend - * URL where the instances are - */ - return rootPath.replace("/webui/", ""); -}; - -const loginTokenCodec = buildCodecForObject<LoginToken>() - .property("token", codecForString()) - .property("expiration", codecForTimestamp) - .build("loginToken") -const TOKENS_KEY = buildStorageKey("auditor-token", codecForMap(loginTokenCodec)); +export function useBackendURL(url?: string): [string, StateUpdater<string>] { + const canonUrl = canonicalizeBaseUrl(url ?? calculateRootPath()); - -export function useBackendURL( - url?: string, -): [string, StateUpdater<string>] { - const [value, setter] = useSimpleLocalStorage( - "auditor-base-url", - url || calculateRootPath(), - ); + const [value, setter] = useSimpleLocalStorage("auditor-base-url", canonUrl); const checkedSetter = (v: ValueOrFunction<string>) => { - return setter((p) => (v instanceof Function ? v(p ?? "") : v).replace(/\/$/, "")); + // FIXME: Explain?! + return setter((p) => + (v instanceof Function ? v(p ?? "") : v).replace(/\/$/, ""), + ); }; return [value!, checkedSetter]; } -export function useBackendDefaultToken( -): [LoginToken | undefined, ((d: LoginToken | undefined) => void)] { - const { update: setToken, value: tokenMap, reset } = useLocalStorage(TOKENS_KEY, {}) - - const tokenOfDefaultInstance = tokenMap["default"] - const clearCache = useMatchMutate() - useEffect(() => { - clearCache() - }, [tokenOfDefaultInstance]) - - function updateToken( - value: (LoginToken | undefined) - ): void { - if (value === undefined) { - reset() - } else { - const res = { ...tokenMap, "default": value } - setToken(res) - } - } - return [tokenMap["default"], updateToken]; -} - -export function useBackendInstanceToken( - id: string, -): [LoginToken | undefined, ((d: LoginToken | undefined) => void)] { - const { update: setToken, value: tokenMap, reset } = useLocalStorage(TOKENS_KEY, {}) - const [defaultToken, defaultSetToken] = useBackendDefaultToken(); - - // instance named 'default' use the default token - if (id === "default") { - return [defaultToken, defaultSetToken]; - } - function updateToken( - value: (LoginToken | undefined) - ): void { - if (value === undefined) { - reset() - } else { - const res = { ...tokenMap, [id]: value } - setToken(res) - } - } - - return [tokenMap[id], updateToken]; -} +function calculateRootPath() { + const rootPath = + typeof window !== undefined + ? window.location.origin + window.location.pathname + : "/"; -export function useLang(initial?: string): [string, StateUpdater<string>] { - const browserLang = - typeof window !== "undefined" - ? navigator.language || (navigator as any).userLanguage - : undefined; - const defaultLang = (browserLang || initial || "en").substring(0, 2); - return useSimpleLocalStorage("lang-preference", defaultLang) as [string, StateUpdater<string>]; + /* + * By default, auditor backend serves the html content + * from the /webui root. This should cover most of the + * cases and the rootPath will be the auditor backend + * URL where the instances are + */ + return rootPath.replace("/spa/", ""); } -export function useSimpleLocalStorage( +function useSimpleLocalStorage( key: string, initialValue?: string, ): [string | undefined, StateUpdater<string | undefined>] { diff --git a/packages/auditor-backoffice-ui/src/hooks/instance.test.ts b/packages/auditor-backoffice-ui/src/hooks/instance.test.ts deleted file mode 100644 index ee1576764..000000000 --- a/packages/auditor-backoffice-ui/src/hooks/instance.test.ts +++ /dev/null @@ -1,741 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import * as tests from "@gnu-taler/web-util/testing"; -import { expect } from "chai"; -import { AccessToken, MerchantBackend } from "../declaration.js"; -import { - useAdminAPI, - useBackendInstances, - useInstanceAPI, - useInstanceDetails, - useManagementAPI, -} from "./instance.js"; -import { ApiMockEnvironment } from "./testing.js"; -import { - API_CREATE_INSTANCE, - API_DELETE_INSTANCE, - API_GET_CURRENT_INSTANCE, - API_LIST_INSTANCES, - API_NEW_LOGIN, - API_UPDATE_CURRENT_INSTANCE, - API_UPDATE_CURRENT_INSTANCE_AUTH, - API_UPDATE_INSTANCE_BY_ID, -} from "./urls.js"; - -describe("instance api interaction with details", () => { - it("should evict cache when updating an instance", async () => { - const env = new ApiMockEnvironment(); - - env.addRequestExpectation(API_GET_CURRENT_INSTANCE, { - response: { - name: "instance_name", - } as MerchantBackend.Instances.QueryInstancesResponse, - }); - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const api = useInstanceAPI(); - const query = useInstanceDetails(); - return { query, api }; - }, - {}, - [ - ({ query, api }) => { - expect(query.loading).true; - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - name: "instance_name", - }); - env.addRequestExpectation(API_UPDATE_CURRENT_INSTANCE, { - request: { - name: "other_name", - } as MerchantBackend.Instances.InstanceReconfigurationMessage, - }); - env.addRequestExpectation(API_GET_CURRENT_INSTANCE, { - response: { - name: "other_name", - } as MerchantBackend.Instances.QueryInstancesResponse, - }); - api.updateInstance({ - name: "other_name", - } as MerchantBackend.Instances.InstanceReconfigurationMessage); - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - name: "other_name", - }); - }, - ], - env.buildTestingContext(), - ); - - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - }); - - it("should evict cache when setting the instance's token", async () => { - const env = new ApiMockEnvironment(); - - env.addRequestExpectation(API_GET_CURRENT_INSTANCE, { - response: { - name: "instance_name", - auth: { - method: "token", - // token: "not-secret", - }, - } as MerchantBackend.Instances.QueryInstancesResponse, - }); - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const api = useInstanceAPI(); - const query = useInstanceDetails(); - return { query, api }; - }, - {}, - [ - ({ query, api }) => { - expect(query.loading).true; - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - name: "instance_name", - auth: { - method: "token", - }, - }); - env.addRequestExpectation(API_UPDATE_CURRENT_INSTANCE_AUTH, { - request: { - method: "token", - token: "secret", - } as MerchantBackend.Instances.InstanceAuthConfigurationMessage, - }); - env.addRequestExpectation(API_NEW_LOGIN, { - auth: "secret", - request: { - scope: "write", - duration: { - "d_us": "forever", - }, - refreshable: true, - }, - }); - env.addRequestExpectation(API_GET_CURRENT_INSTANCE, { - response: { - name: "instance_name", - auth: { - method: "token", - // token: "secret", - }, - } as MerchantBackend.Instances.QueryInstancesResponse, - }); - api.setNewAccessToken(undefined, "secret" as AccessToken); - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - name: "instance_name", - auth: { - method: "token", - // token: "secret", - }, - }); - }, - ], - env.buildTestingContext(), - ); - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - }); - - it("should evict cache when clearing the instance's token", async () => { - const env = new ApiMockEnvironment(); - - env.addRequestExpectation(API_GET_CURRENT_INSTANCE, { - response: { - name: "instance_name", - auth: { - method: "token", - // token: "not-secret", - }, - } as MerchantBackend.Instances.QueryInstancesResponse, - }); - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const api = useInstanceAPI(); - const query = useInstanceDetails(); - return { query, api }; - }, - {}, - [ - ({ query, api }) => { - expect(query.loading).true; - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - name: "instance_name", - auth: { - method: "token", - // token: "not-secret", - }, - }); - env.addRequestExpectation(API_UPDATE_CURRENT_INSTANCE_AUTH, { - request: { - method: "external", - } as MerchantBackend.Instances.InstanceAuthConfigurationMessage, - }); - env.addRequestExpectation(API_GET_CURRENT_INSTANCE, { - response: { - name: "instance_name", - auth: { - method: "external", - }, - } as MerchantBackend.Instances.QueryInstancesResponse, - }); - - api.clearAccessToken(undefined); - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - name: "instance_name", - auth: { - method: "external", - }, - }); - }, - ], - env.buildTestingContext(), - ); - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - // const { result, waitForNextUpdate } = renderHook( - // () => { - // const api = useInstanceAPI(); - // const query = useInstanceDetails(); - - // return { query, api }; - // }, - // { wrapper: TestingContext } - // ); - - // expect(result.current).not.undefined; - // if (!result.current) { - // return; - // } - // expect(result.current.query.loading).true; - - // await waitForNextUpdate({ timeout: 1 }); - - // expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - - // expect(result.current.query.loading).false; - - // expect(result.current?.query.ok).true; - // if (!result.current?.query.ok) return; - - // expect(result.current.query.data).equals({ - // name: 'instance_name', - // auth: { - // method: 'token', - // token: 'not-secret', - // } - // }); - - // act(async () => { - // await result.current?.api.clearToken(); - // }); - - // expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - - // expect(result.current.query.loading).false; - - // await waitForNextUpdate({ timeout: 1 }); - - // expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - - // expect(result.current.query.loading).false; - // expect(result.current.query.ok).true; - - // expect(result.current.query.data).equals({ - // name: 'instance_name', - // auth: { - // method: 'external', - // } - // }); - }); -}); - -describe("instance admin api interaction with listing", () => { - it("should evict cache when creating a new instance", async () => { - const env = new ApiMockEnvironment(); - - env.addRequestExpectation(API_LIST_INSTANCES, { - response: { - instances: [ - { - name: "instance_name", - } as MerchantBackend.Instances.Instance, - ], - }, - }); - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const api = useAdminAPI(); - const query = useBackendInstances(); - return { query, api }; - }, - {}, - [ - ({ query, api }) => { - expect(query.loading).true; - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - instances: [ - { - name: "instance_name", - }, - ], - }); - - env.addRequestExpectation(API_CREATE_INSTANCE, { - request: { - name: "other_name", - } as MerchantBackend.Instances.InstanceConfigurationMessage, - }); - env.addRequestExpectation(API_LIST_INSTANCES, { - response: { - instances: [ - { - name: "instance_name", - } as MerchantBackend.Instances.Instance, - { - name: "other_name", - } as MerchantBackend.Instances.Instance, - ], - }, - }); - - api.createInstance({ - name: "other_name", - } as MerchantBackend.Instances.InstanceConfigurationMessage); - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - instances: [ - { - name: "instance_name", - }, - { - name: "other_name", - }, - ], - }); - }, - ], - env.buildTestingContext(), - ); - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - }); - - it("should evict cache when deleting an instance", async () => { - const env = new ApiMockEnvironment(); - - env.addRequestExpectation(API_LIST_INSTANCES, { - response: { - instances: [ - { - id: "default", - name: "instance_name", - } as MerchantBackend.Instances.Instance, - { - id: "the_id", - name: "second_instance", - } as MerchantBackend.Instances.Instance, - ], - }, - }); - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const api = useAdminAPI(); - const query = useBackendInstances(); - return { query, api }; - }, - {}, - [ - ({ query, api }) => { - expect(query.loading).true; - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - instances: [ - { - id: "default", - name: "instance_name", - }, - { - id: "the_id", - name: "second_instance", - }, - ], - }); - - env.addRequestExpectation(API_DELETE_INSTANCE("the_id"), {}); - env.addRequestExpectation(API_LIST_INSTANCES, { - response: { - instances: [ - { - id: "default", - name: "instance_name", - } as MerchantBackend.Instances.Instance, - ], - }, - }); - - api.deleteInstance("the_id"); - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - instances: [ - { - id: "default", - name: "instance_name", - }, - ], - }); - }, - ], - env.buildTestingContext(), - ); - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - - // const { result, waitForNextUpdate } = renderHook( - // () => { - // const api = useAdminAPI(); - // const query = useBackendInstances(); - - // return { query, api }; - // }, - // { wrapper: TestingContext } - // ); - - // expect(result.current).not.undefined; - // if (!result.current) { - // return; - // } - // expect(result.current.query.loading).true; - - // await waitForNextUpdate({ timeout: 1 }); - - // expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - - // expect(result.current.query.loading).false; - - // expect(result.current?.query.ok).true; - // if (!result.current?.query.ok) return; - - // expect(result.current.query.data).equals({ - // instances: [{ - // id: 'default', - // name: 'instance_name' - // }, { - // id: 'the_id', - // name: 'second_instance' - // }] - // }); - - // env.addRequestExpectation(API_DELETE_INSTANCE('the_id'), {}); - - // act(async () => { - // await result.current?.api.deleteInstance('the_id'); - // }); - - // expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - - // env.addRequestExpectation(API_LIST_INSTANCES, { - // response: { - // instances: [{ - // id: 'default', - // name: 'instance_name' - // } as MerchantBackend.Instances.Instance] - // }, - // }); - - // expect(result.current.query.loading).false; - - // await waitForNextUpdate({ timeout: 1 }); - - // expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - - // expect(result.current.query.loading).false; - // expect(result.current.query.ok).true; - - // expect(result.current.query.data).equals({ - // instances: [{ - // id: 'default', - // name: 'instance_name' - // }] - // }); - }); - - it("should evict cache when deleting (purge) an instance", async () => { - const env = new ApiMockEnvironment(); - - env.addRequestExpectation(API_LIST_INSTANCES, { - response: { - instances: [ - { - id: "default", - name: "instance_name", - } as MerchantBackend.Instances.Instance, - { - id: "the_id", - name: "second_instance", - } as MerchantBackend.Instances.Instance, - ], - }, - }); - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const api = useAdminAPI(); - const query = useBackendInstances(); - return { query, api }; - }, - {}, - [ - ({ query, api }) => { - expect(query.loading).true; - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - instances: [ - { - id: "default", - name: "instance_name", - }, - { - id: "the_id", - name: "second_instance", - }, - ], - }); - - env.addRequestExpectation(API_DELETE_INSTANCE("the_id"), { - qparam: { - purge: "YES", - }, - }); - env.addRequestExpectation(API_LIST_INSTANCES, { - response: { - instances: [ - { - id: "default", - name: "instance_name", - } as MerchantBackend.Instances.Instance, - ], - }, - }); - - api.purgeInstance("the_id"); - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - instances: [ - { - id: "default", - name: "instance_name", - }, - ], - }); - }, - ], - env.buildTestingContext(), - ); - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - }); -}); - -describe("instance management api interaction with listing", () => { - it("should evict cache when updating an instance", async () => { - const env = new ApiMockEnvironment(); - - env.addRequestExpectation(API_LIST_INSTANCES, { - response: { - instances: [ - { - id: "managed", - name: "instance_name", - } as MerchantBackend.Instances.Instance, - ], - }, - }); - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const api = useManagementAPI("managed"); - const query = useBackendInstances(); - return { query, api }; - }, - {}, - [ - ({ query, api }) => { - expect(query.loading).true; - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - instances: [ - { - id: "managed", - name: "instance_name", - }, - ], - }); - - env.addRequestExpectation(API_UPDATE_INSTANCE_BY_ID("managed"), { - request: { - name: "other_name", - } as MerchantBackend.Instances.InstanceReconfigurationMessage, - }); - env.addRequestExpectation(API_LIST_INSTANCES, { - response: { - instances: [ - { - id: "managed", - name: "other_name", - } as MerchantBackend.Instances.Instance, - ], - }, - }); - - api.updateInstance({ - name: "other_name", - } as MerchantBackend.Instances.InstanceConfigurationMessage); - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - instances: [ - { - id: "managed", - name: "other_name", - }, - ], - }); - }, - ], - env.buildTestingContext(), - ); - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - }); -}); diff --git a/packages/auditor-backoffice-ui/src/hooks/instance.ts b/packages/auditor-backoffice-ui/src/hooks/instance.ts deleted file mode 100644 index 0677191db..000000000 --- a/packages/auditor-backoffice-ui/src/hooks/instance.ts +++ /dev/null @@ -1,313 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ -import { - HttpResponse, - HttpResponseOk, - RequestError, -} from "@gnu-taler/web-util/browser"; -import { useBackendContext } from "../context/backend.js"; -import { AccessToken, MerchantBackend } from "../declaration.js"; -import { - useBackendBaseRequest, - useBackendInstanceRequest, - useCredentialsChecker, - useMatchMutate, -} from "./backend.js"; - -// FIX default import https://github.com/microsoft/TypeScript/issues/49189 -import _useSWR, { SWRHook, useSWRConfig } from "swr"; -const useSWR = _useSWR as unknown as SWRHook; - -interface InstanceAPI { - updateInstance: ( - data: MerchantBackend.Instances.InstanceReconfigurationMessage, - ) => Promise<void>; - deleteInstance: () => Promise<void>; - clearAccessToken: (currentToken: AccessToken | undefined) => Promise<void>; - setNewAccessToken: (currentToken: AccessToken | undefined, token: AccessToken) => Promise<void>; -} - -export function useAdminAPI(): AdminAPI { - const { request } = useBackendBaseRequest(); - const mutateAll = useMatchMutate(); - - const createInstance = async ( - instance: MerchantBackend.Instances.InstanceConfigurationMessage, - ): Promise<void> => { - await request(`/management/instances`, { - method: "POST", - data: instance, - }); - - mutateAll(/\/management\/instances/); - }; - - const deleteInstance = async (id: string): Promise<void> => { - await request(`/management/instances/${id}`, { - method: "DELETE", - }); - - mutateAll(/\/management\/instances/); - }; - - const purgeInstance = async (id: string): Promise<void> => { - await request(`/management/instances/${id}`, { - method: "DELETE", - params: { - purge: "YES", - }, - }); - - mutateAll(/\/management\/instances/); - }; - - return { createInstance, deleteInstance, purgeInstance }; -} - -export interface AdminAPI { - createInstance: ( - data: MerchantBackend.Instances.InstanceConfigurationMessage, - ) => Promise<void>; - deleteInstance: (id: string) => Promise<void>; - purgeInstance: (id: string) => Promise<void>; -} - -export function useManagementAPI(instanceId: string): InstanceAPI { - const mutateAll = useMatchMutate(); - const { url: backendURL } = useBackendContext() - const { updateToken } = useBackendContext(); - const { request } = useBackendBaseRequest(); - const { requestNewLoginToken } = useCredentialsChecker() - - const updateInstance = async ( - instance: MerchantBackend.Instances.InstanceReconfigurationMessage, - ): Promise<void> => { - await request(`/management/instances/${instanceId}`, { - method: "PATCH", - data: instance, - }); - - mutateAll(/\/management\/instances/); - }; - - const deleteInstance = async (): Promise<void> => { - await request(`/management/instances/${instanceId}`, { - method: "DELETE", - }); - - mutateAll(/\/management\/instances/); - }; - - const clearAccessToken = async (currentToken: AccessToken | undefined): Promise<void> => { - await request(`/management/instances/${instanceId}/auth`, { - method: "POST", - token: currentToken, - data: { method: "external" }, - }); - - mutateAll(/\/management\/instances/); - }; - - const setNewAccessToken = async (currentToken: AccessToken | undefined, newToken: AccessToken): Promise<void> => { - await request(`/management/instances/${instanceId}/auth`, { - method: "POST", - token: currentToken, - data: { method: "token", token: newToken }, - }); - - const resp = await requestNewLoginToken(backendURL, newToken) - if (resp.valid) { - const { token, expiration } = resp - updateToken({ token, expiration }); - } else { - updateToken(undefined) - } - - mutateAll(/\/management\/instances/); - }; - - return { updateInstance, deleteInstance, setNewAccessToken, clearAccessToken }; -} - -export function useInstanceAPI(): InstanceAPI { - const { mutate } = useSWRConfig(); - const { url: backendURL, updateToken } = useBackendContext() - - const { - token: adminToken, - } = useBackendContext(); - const { request } = useBackendInstanceRequest(); - const { requestNewLoginToken } = useCredentialsChecker() - - const updateInstance = async ( - instance: MerchantBackend.Instances.InstanceReconfigurationMessage, - ): Promise<void> => { - await request(`/private/`, { - method: "PATCH", - data: instance, - }); - - if (adminToken) mutate(["/private/instances", adminToken, backendURL], null); - mutate([`/private/`], null); - }; - - const deleteInstance = async (): Promise<void> => { - await request(`/private/`, { - method: "DELETE", - // token: adminToken, - }); - - if (adminToken) mutate(["/private/instances", adminToken, backendURL], null); - mutate([`/private/`], null); - }; - - const clearAccessToken = async (currentToken: AccessToken | undefined): Promise<void> => { - await request(`/private/auth`, { - method: "POST", - token: currentToken, - data: { method: "external" }, - }); - - mutate([`/private/`], null); - }; - - const setNewAccessToken = async (currentToken: AccessToken | undefined, newToken: AccessToken): Promise<void> => { - await request(`/private/auth`, { - method: "POST", - token: currentToken, - data: { method: "token", token: newToken }, - }); - - const resp = await requestNewLoginToken(backendURL, newToken) - if (resp.valid) { - const { token, expiration } = resp - updateToken({ token, expiration }); - } else { - updateToken(undefined) - } - - mutate([`/private/`], null); - }; - - return { updateInstance, deleteInstance, setNewAccessToken, clearAccessToken }; -} - -export function useInstanceDetails(): HttpResponse< - MerchantBackend.Instances.QueryInstancesResponse, - MerchantBackend.ErrorDetail -> { - const { fetcher } = useBackendInstanceRequest(); - - const { data, error, isValidating } = useSWR< - HttpResponseOk<MerchantBackend.Instances.QueryInstancesResponse>, - RequestError<MerchantBackend.ErrorDetail> - >([`/private/`], fetcher, { - refreshInterval: 0, - refreshWhenHidden: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshWhenOffline: false, - revalidateIfStale: false, - errorRetryCount: 0, - errorRetryInterval: 1, - shouldRetryOnError: false, - }); - - if (isValidating) return { loading: true, data: data?.data }; - if (data) return data; - if (error) return error.cause; - return { loading: true }; -} - -type KYCStatus = - | { type: "ok" } - | { type: "redirect"; status: MerchantBackend.KYC.AccountKycRedirects }; - -export function useInstanceKYCDetails(): HttpResponse< - KYCStatus, - MerchantBackend.ErrorDetail -> { - const { fetcher } = useBackendInstanceRequest(); - - const { data, error } = useSWR< - HttpResponseOk<MerchantBackend.KYC.AccountKycRedirects>, - RequestError<MerchantBackend.ErrorDetail> - >([`/private/kyc`], fetcher, { - refreshInterval: 60 * 1000, - refreshWhenHidden: false, - revalidateOnFocus: false, - revalidateIfStale: false, - revalidateOnMount: false, - revalidateOnReconnect: false, - refreshWhenOffline: false, - errorRetryCount: 0, - errorRetryInterval: 1, - shouldRetryOnError: false, - }); - - if (data) { - if (data.info?.status === 202) - return { ok: true, data: { type: "redirect", status: data.data } }; - return { ok: true, data: { type: "ok" } }; - } - if (error) return error.cause; - return { loading: true }; -} - -export function useManagedInstanceDetails( - instanceId: string, -): HttpResponse< - MerchantBackend.Instances.QueryInstancesResponse, - MerchantBackend.ErrorDetail -> { - const { request } = useBackendBaseRequest(); - - const { data, error, isValidating } = useSWR< - HttpResponseOk<MerchantBackend.Instances.QueryInstancesResponse>, - RequestError<MerchantBackend.ErrorDetail> - >([`/management/instances/${instanceId}`], request, { - refreshInterval: 0, - refreshWhenHidden: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshWhenOffline: false, - errorRetryCount: 0, - errorRetryInterval: 1, - shouldRetryOnError: false, - }); - - if (isValidating) return { loading: true, data: data?.data }; - if (data) return data; - if (error) return error.cause; - return { loading: true }; -} - -export function useBackendInstances(): HttpResponse< - MerchantBackend.Instances.InstancesResponse, - MerchantBackend.ErrorDetail -> { - const { request } = useBackendBaseRequest(); - - const { data, error, isValidating } = useSWR< - HttpResponseOk<MerchantBackend.Instances.InstancesResponse>, - RequestError<MerchantBackend.ErrorDetail> - >(["/management/instances"], request); - - if (isValidating) return { loading: true, data: data?.data }; - if (data) return data; - if (error) return error.cause; - return { loading: true }; -} diff --git a/packages/auditor-backoffice-ui/src/hooks/listener.ts b/packages/auditor-backoffice-ui/src/hooks/listener.ts deleted file mode 100644 index d101f7bb8..000000000 --- a/packages/auditor-backoffice-ui/src/hooks/listener.ts +++ /dev/null @@ -1,85 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { useState } from "preact/hooks"; - -/** - * This component is used when a component wants one child to have a trigger for - * an action (a button) and other child have the action implemented (like - * gathering information with a form). The difference with other approaches is - * that in this case the parent component is not holding the state. - * - * It will return a subscriber and activator. - * - * The activator may be undefined, if it is undefined it is indicating that the - * subscriber is not ready to be called. - * - * The subscriber will receive a function (the listener) that will be call when the - * activator runs. The listener must return the collected information. - * - * As a result, when the activator is triggered by a child component, the - * @action function is called receives the information from the listener defined by other - * child component - * - * @param action from <T> to <R> - * @returns activator and subscriber, undefined activator means that there is not subscriber - */ - -export function useListener<T, R = any>( - action: (r: T) => Promise<R>, -): [undefined | (() => Promise<R>), (listener?: () => T) => void] { - type RunnerHandler = { toBeRan?: () => Promise<R> }; - const [state, setState] = useState<RunnerHandler>({}); - - /** - * subscriber will receive a method that will be call when the activator runs - * - * @param listener function to be run when the activator runs - */ - const subscriber = (listener?: () => T) => { - if (listener) { - setState({ - toBeRan: () => { - const whatWeGetFromTheListener = listener(); - return action(whatWeGetFromTheListener); - }, - }); - } else { - setState({ - toBeRan: undefined, - }); - } - }; - - /** - * activator will call runner if there is someone subscribed - */ - const activator = state.toBeRan - ? async () => { - if (state.toBeRan) { - return state.toBeRan(); - } - return Promise.reject(); - } - : undefined; - - return [activator, subscriber]; -} diff --git a/packages/auditor-backoffice-ui/src/hooks/notifications.ts b/packages/auditor-backoffice-ui/src/hooks/notifications.ts deleted file mode 100644 index 133ddd80b..000000000 --- a/packages/auditor-backoffice-ui/src/hooks/notifications.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { useState } from "preact/hooks"; -import { Notification } from "../utils/types.js"; - -interface Result { - notifications: Notification[]; - pushNotification: (n: Notification) => void; - removeNotification: (n: Notification) => void; -} - -type NotificationWithDate = Notification & { since: Date }; - -export function useNotifications( - initial: Notification[] = [], - timeout = 3000, -): Result { - const [notifications, setNotifications] = useState<NotificationWithDate[]>( - initial.map((i) => ({ ...i, since: new Date() })), - ); - - const pushNotification = (n: Notification): void => { - const entry = { ...n, since: new Date() }; - setNotifications((ns) => [...ns, entry]); - if (n.type !== "ERROR") - setTimeout(() => { - setNotifications((ns) => ns.filter((x) => x.since !== entry.since)); - }, timeout); - }; - - const removeNotification = (notif: Notification) => { - setNotifications((ns: NotificationWithDate[]) => - ns.filter((n) => n !== notif), - ); - }; - return { notifications, pushNotification, removeNotification }; -} diff --git a/packages/auditor-backoffice-ui/src/hooks/operational.ts b/packages/auditor-backoffice-ui/src/hooks/operational.ts new file mode 100644 index 000000000..c40a1423c --- /dev/null +++ b/packages/auditor-backoffice-ui/src/hooks/operational.ts @@ -0,0 +1,80 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 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. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +import { + HttpResponse, + HttpResponseOk, + RequestError, +} from "@gnu-taler/web-util/browser"; +import { AuditorBackend } from "../declaration.js"; +import { useBackendRequest } from "./backend.js"; + +// FIX default import https://github.com/microsoft/TypeScript/issues/49189 +import _useSWR, { SWRHook } from "swr"; + +const useSWR = _useSWR as unknown as SWRHook; + +type YesOrNo = "yes" | "no"; + +export interface HelperDashboardFilter { + finance?: YesOrNo; + security?: YesOrNo; + operating?: YesOrNo; + detail?: YesOrNo; +} + +export function getOperationData( + args?: HelperDashboardFilter, + updateFilter?: (d: Date) => void, +): HttpResponse<any, AuditorBackend.ErrorDetail> { + const { multiFetcher } = useBackendRequest(); + const endpoints = [ + "monitoring/row-inconsistency", + "monitoring/purse-not-closed-inconsistencies", + "monitoring/reserve-not-closed-inconsistency", + "monitoring/denominations-without-sigs", + "monitoring/deposit-confirmation", + "monitoring/denomination-key-validity-withdraw-inconsistency", + "monitoring/refreshes-hanging", + //TODO fix endpoint + // "monitoring/closure-lags", + // "monitoring/row-minor-inconsistencies", + // "monitoring/historic-denomination-revenue", + // "monitoring/denomination-pending", + "monitoring/historic-reserve-summary", + ]; + + const { data: list, error: listError } = useSWR< + HttpResponseOk<any>[], + RequestError<AuditorBackend.ErrorDetail> + >(endpoints, multiFetcher, { + refreshInterval: 60, + refreshWhenHidden: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshWhenOffline: false, + }); + + if (listError) return listError.cause; + + if (list) { + return { ok: true, data: [list] }; + } + return { loading: true }; +} + +export interface EntityAPI { + updateEntity: (id: string) => Promise<void>; +} diff --git a/packages/auditor-backoffice-ui/src/hooks/order.test.ts b/packages/auditor-backoffice-ui/src/hooks/order.test.ts deleted file mode 100644 index c243309a8..000000000 --- a/packages/auditor-backoffice-ui/src/hooks/order.test.ts +++ /dev/null @@ -1,587 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import * as tests from "@gnu-taler/web-util/testing"; -import { expect } from "chai"; -import { MerchantBackend } from "../declaration.js"; -import { useInstanceOrders, useOrderAPI, useOrderDetails } from "./order.js"; -import { ApiMockEnvironment } from "./testing.js"; -import { - API_CREATE_ORDER, - API_DELETE_ORDER, - API_FORGET_ORDER_BY_ID, - API_GET_ORDER_BY_ID, - API_LIST_ORDERS, - API_REFUND_ORDER_BY_ID, -} from "./urls.js"; - -describe("order api interaction with listing", () => { - it("should evict cache when creating an order", async () => { - const env = new ApiMockEnvironment(); - - env.addRequestExpectation(API_LIST_ORDERS, { - qparam: { delta: -20, paid: "yes" }, - response: { - orders: [{ order_id: "1" }, { order_id: "2" } as MerchantBackend.Orders.OrderHistoryEntry], - }, - }); - - const newDate = (d: Date) => { - //console.log("new date", d); - }; - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const query = useInstanceOrders({ paid: "yes" }, newDate); - const api = useOrderAPI(); - return { query, api }; - }, - {}, - [ - ({ query, api }) => { - expect(query.loading).true; - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - orders: [{ order_id: "1" }, { order_id: "2" }], - }); - - env.addRequestExpectation(API_CREATE_ORDER, { - request: { - order: { amount: "ARS:12", summary: "pay me" }, - }, - response: { order_id: "3" }, - }); - - env.addRequestExpectation(API_LIST_ORDERS, { - qparam: { delta: -20, paid: "yes" }, - response: { - orders: [{ order_id: "1" }, { order_id: "2" } as any, { order_id: "3" } as any], - }, - }); - - api.createOrder({ - order: { amount: "ARS:12", summary: "pay me" }, - } as any); - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - orders: [{ order_id: "1" }, { order_id: "2" }, { order_id: "3" }], - }); - }, - ], - env.buildTestingContext(), - ); - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - }); - - it("should evict cache when doing a refund", async () => { - const env = new ApiMockEnvironment(); - - env.addRequestExpectation(API_LIST_ORDERS, { - qparam: { delta: -20, paid: "yes" }, - response: { orders: [{ - order_id: "1", - amount: "EUR:12", - refundable: true, - } as MerchantBackend.Orders.OrderHistoryEntry] }, - }); - - const newDate = (d: Date) => { - //console.log("new date", d); - }; - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const query = useInstanceOrders({ paid: "yes" }, newDate); - const api = useOrderAPI(); - return { query, api }; - }, - {}, - [ - ({ query, api }) => { - expect(query.loading).true; - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - orders: [ - { - order_id: "1", - amount: "EUR:12", - refundable: true, - }, - ], - }); - env.addRequestExpectation(API_REFUND_ORDER_BY_ID("1"), { - request: { - reason: "double pay", - refund: "EUR:1", - }, - }); - - env.addRequestExpectation(API_LIST_ORDERS, { - qparam: { delta: -20, paid: "yes" }, - response: { orders: [ - { order_id: "1", amount: "EUR:12", refundable: false } as any, - ] }, - }); - - api.refundOrder("1", { - reason: "double pay", - refund: "EUR:1", - }); - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - orders: [ - { - order_id: "1", - amount: "EUR:12", - refundable: false, - }, - ], - }); - }, - ], - env.buildTestingContext(), - ); - - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - }); - - it("should evict cache when deleting an order", async () => { - const env = new ApiMockEnvironment(); - - env.addRequestExpectation(API_LIST_ORDERS, { - qparam: { delta: -20, paid: "yes" }, - response: { - orders: [{ order_id: "1" }, { order_id: "2" } as MerchantBackend.Orders.OrderHistoryEntry], - }, - }); - - const newDate = (d: Date) => { - //console.log("new date", d); - }; - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const query = useInstanceOrders({ paid: "yes" }, newDate); - const api = useOrderAPI(); - return { query, api }; - }, - {}, - [ - ({ query, api }) => { - expect(query.loading).true; - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - orders: [{ order_id: "1" }, { order_id: "2" }], - }); - - env.addRequestExpectation(API_DELETE_ORDER("1"), {}); - - env.addRequestExpectation(API_LIST_ORDERS, { - qparam: { delta: -20, paid: "yes" }, - response: { - orders: [{ order_id: "2" } as any], - }, - }); - - api.deleteOrder("1"); - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - orders: [{ order_id: "2" }], - }); - }, - ], - env.buildTestingContext(), - ); - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - }); -}); - -describe("order api interaction with details", () => { - it("should evict cache when doing a refund", async () => { - const env = new ApiMockEnvironment(); - - env.addRequestExpectation(API_GET_ORDER_BY_ID("1"), { - // qparam: { delta: 0, paid: "yes" }, - response: { - summary: "description", - refund_amount: "EUR:0", - } as unknown as MerchantBackend.Orders.CheckPaymentPaidResponse, - }); - - const newDate = (d: Date) => { - //console.log("new date", d); - }; - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const query = useOrderDetails("1"); - const api = useOrderAPI(); - return { query, api }; - }, - {}, - [ - ({ query, api }) => { - expect(query.loading).true; - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - summary: "description", - refund_amount: "EUR:0", - }); - env.addRequestExpectation(API_REFUND_ORDER_BY_ID("1"), { - request: { - reason: "double pay", - refund: "EUR:1", - }, - }); - - env.addRequestExpectation(API_GET_ORDER_BY_ID("1"), { - response: { - summary: "description", - refund_amount: "EUR:1", - } as unknown as MerchantBackend.Orders.CheckPaymentPaidResponse, - }); - - api.refundOrder("1", { - reason: "double pay", - refund: "EUR:1", - }); - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - summary: "description", - refund_amount: "EUR:1", - }); - }, - ], - env.buildTestingContext(), - ); - - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - }); - - it("should evict cache when doing a forget", async () => { - const env = new ApiMockEnvironment(); - - env.addRequestExpectation(API_GET_ORDER_BY_ID("1"), { - // qparam: { delta: 0, paid: "yes" }, - response: { - summary: "description", - refund_amount: "EUR:0", - } as unknown as MerchantBackend.Orders.CheckPaymentPaidResponse, - }); - - const newDate = (d: Date) => { - //console.log("new date", d); - }; - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const query = useOrderDetails("1"); - const api = useOrderAPI(); - return { query, api }; - }, - {}, - [ - ({ query, api }) => { - expect(query.loading).true; - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - summary: "description", - refund_amount: "EUR:0", - }); - env.addRequestExpectation(API_FORGET_ORDER_BY_ID("1"), { - request: { - fields: ["$.summary"], - }, - }); - - env.addRequestExpectation(API_GET_ORDER_BY_ID("1"), { - response: { - summary: undefined, - } as unknown as MerchantBackend.Orders.CheckPaymentPaidResponse, - }); - - api.forgetOrder("1", { - fields: ["$.summary"], - }); - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - summary: undefined, - }); - }, - ], - env.buildTestingContext(), - ); - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - }); -}); - -describe("order listing pagination", () => { - it("should not load more if has reach the end", async () => { - const env = new ApiMockEnvironment(); - env.addRequestExpectation(API_LIST_ORDERS, { - qparam: { delta: 20, wired: "yes", date_s: 12 }, - response: { - orders: [{ order_id: "1" } as any], - }, - }); - - env.addRequestExpectation(API_LIST_ORDERS, { - qparam: { delta: -20, wired: "yes", date_s: 13 }, - response: { - orders: [{ order_id: "2" } as any], - }, - }); - - const newDate = (d: Date) => { - //console.log("new date", d); - }; - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const date = new Date(12000); - const query = useInstanceOrders({ wired: "yes", date }, newDate); - const api = useOrderAPI(); - return { query, api }; - }, - {}, - [ - ({ query, api }) => { - expect(query.loading).true; - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - orders: [{ order_id: "1" }, { order_id: "2" }], - }); - expect(query.isReachingEnd).true; - expect(query.isReachingStart).true; - - // should not trigger new state update or query - query.loadMore(); - query.loadMorePrev(); - }, - ], - env.buildTestingContext(), - ); - - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - }); - - it("should load more if result brings more that PAGE_SIZE", async () => { - const env = new ApiMockEnvironment(); - - const ordersFrom0to20 = Array.from({ length: 20 }).map((e, i) => ({ - order_id: String(i), - })); - const ordersFrom20to40 = Array.from({ length: 20 }).map((e, i) => ({ - order_id: String(i + 20), - })); - const ordersFrom20to0 = [...ordersFrom0to20].reverse(); - - env.addRequestExpectation(API_LIST_ORDERS, { - qparam: { delta: 20, wired: "yes", date_s: 12 }, - response: { - orders: ordersFrom0to20, - }, - }); - - env.addRequestExpectation(API_LIST_ORDERS, { - qparam: { delta: -20, wired: "yes", date_s: 13 }, - response: { - orders: ordersFrom20to40, - }, - }); - - const newDate = (d: Date) => { - //console.log("new date", d); - }; - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const date = new Date(12000); - const query = useInstanceOrders({ wired: "yes", date }, newDate); - const api = useOrderAPI(); - return { query, api }; - }, - {}, - [ - ({ query, api }) => { - expect(query.loading).true; - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - orders: [...ordersFrom20to0, ...ordersFrom20to40], - }); - expect(query.isReachingEnd).false; - expect(query.isReachingStart).false; - - env.addRequestExpectation(API_LIST_ORDERS, { - qparam: { delta: -40, wired: "yes", date_s: 13 }, - response: { - orders: [...ordersFrom20to40, { order_id: "41" }], - }, - }); - - query.loadMore(); - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).true; - }, - ({ query, api }) => { - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - orders: [ - ...ordersFrom20to0, - ...ordersFrom20to40, - { order_id: "41" }, - ], - }); - - env.addRequestExpectation(API_LIST_ORDERS, { - qparam: { delta: 40, wired: "yes", date_s: 12 }, - response: { - orders: [...ordersFrom0to20, { order_id: "-1" }], - }, - }); - - query.loadMorePrev(); - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).true; - }, - ({ query, api }) => { - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - orders: [ - { order_id: "-1" }, - ...ordersFrom20to0, - ...ordersFrom20to40, - { order_id: "41" }, - ], - }); - }, - ], - env.buildTestingContext(), - ); - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - }); -}); diff --git a/packages/auditor-backoffice-ui/src/hooks/order.ts b/packages/auditor-backoffice-ui/src/hooks/order.ts deleted file mode 100644 index e7a893f2c..000000000 --- a/packages/auditor-backoffice-ui/src/hooks/order.ts +++ /dev/null @@ -1,289 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ -import { - HttpResponse, - HttpResponseOk, - HttpResponsePaginated, - RequestError, -} from "@gnu-taler/web-util/browser"; -import { useEffect, useState } from "preact/hooks"; -import { MerchantBackend } from "../declaration.js"; -import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js"; -import { useBackendInstanceRequest, useMatchMutate } from "./backend.js"; - -// FIX default import https://github.com/microsoft/TypeScript/issues/49189 -import _useSWR, { SWRHook } from "swr"; -const useSWR = _useSWR as unknown as SWRHook; - -export interface OrderAPI { - //FIXME: add OutOfStockResponse on 410 - createOrder: ( - data: MerchantBackend.Orders.PostOrderRequest, - ) => Promise<HttpResponseOk<MerchantBackend.Orders.PostOrderResponse>>; - forgetOrder: ( - id: string, - data: MerchantBackend.Orders.ForgetRequest, - ) => Promise<HttpResponseOk<void>>; - refundOrder: ( - id: string, - data: MerchantBackend.Orders.RefundRequest, - ) => Promise<HttpResponseOk<MerchantBackend.Orders.MerchantRefundResponse>>; - deleteOrder: (id: string) => Promise<HttpResponseOk<void>>; - getPaymentURL: (id: string) => Promise<HttpResponseOk<string>>; -} - -type YesOrNo = "yes" | "no"; - -export function useOrderAPI(): OrderAPI { - const mutateAll = useMatchMutate(); - const { request } = useBackendInstanceRequest(); - - const createOrder = async ( - data: MerchantBackend.Orders.PostOrderRequest, - ): Promise<HttpResponseOk<MerchantBackend.Orders.PostOrderResponse>> => { - const res = await request<MerchantBackend.Orders.PostOrderResponse>( - `/private/orders`, - { - method: "POST", - data, - }, - ); - await mutateAll(/.*private\/orders.*/); - // mutate('') - return res; - }; - const refundOrder = async ( - orderId: string, - data: MerchantBackend.Orders.RefundRequest, - ): Promise<HttpResponseOk<MerchantBackend.Orders.MerchantRefundResponse>> => { - mutateAll(/@"\/private\/orders"@/); - const res = request<MerchantBackend.Orders.MerchantRefundResponse>( - `/private/orders/${orderId}/refund`, - { - method: "POST", - data, - }, - ); - - // order list returns refundable information, so we must evict everything - await mutateAll(/.*private\/orders.*/); - return res; - }; - - const forgetOrder = async ( - orderId: string, - data: MerchantBackend.Orders.ForgetRequest, - ): Promise<HttpResponseOk<void>> => { - mutateAll(/@"\/private\/orders"@/); - const res = request<void>(`/private/orders/${orderId}/forget`, { - method: "PATCH", - data, - }); - // we may be forgetting some fields that are pare of the listing, so we must evict everything - await mutateAll(/.*private\/orders.*/); - return res; - }; - const deleteOrder = async ( - orderId: string, - ): Promise<HttpResponseOk<void>> => { - mutateAll(/@"\/private\/orders"@/); - const res = request<void>(`/private/orders/${orderId}`, { - method: "DELETE", - }); - await mutateAll(/.*private\/orders.*/); - return res; - }; - - const getPaymentURL = async ( - orderId: string, - ): Promise<HttpResponseOk<string>> => { - return request<MerchantBackend.Orders.MerchantOrderStatusResponse>( - `/private/orders/${orderId}`, - { - method: "GET", - }, - ).then((res) => { - const url = - res.data.order_status === "unpaid" - ? res.data.taler_pay_uri - : res.data.contract_terms.fulfillment_url; - const response: HttpResponseOk<string> = res as any; - response.data = url || ""; - return response; - }); - }; - - return { createOrder, forgetOrder, deleteOrder, refundOrder, getPaymentURL }; -} - -export function useOrderDetails( - oderId: string, -): HttpResponse< - MerchantBackend.Orders.MerchantOrderStatusResponse, - MerchantBackend.ErrorDetail -> { - const { fetcher } = useBackendInstanceRequest(); - - const { data, error, isValidating } = useSWR< - HttpResponseOk<MerchantBackend.Orders.MerchantOrderStatusResponse>, - RequestError<MerchantBackend.ErrorDetail> - >([`/private/orders/${oderId}`], fetcher, { - refreshInterval: 0, - refreshWhenHidden: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshWhenOffline: false, - }); - - if (isValidating) return { loading: true, data: data?.data }; - if (data) return data; - if (error) return error.cause; - return { loading: true }; -} - -export interface InstanceOrderFilter { - paid?: YesOrNo; - refunded?: YesOrNo; - wired?: YesOrNo; - date?: Date; -} - -export function useInstanceOrders( - args?: InstanceOrderFilter, - updateFilter?: (d: Date) => void, -): HttpResponsePaginated< - MerchantBackend.Orders.OrderHistory, - MerchantBackend.ErrorDetail -> { - const { orderFetcher } = useBackendInstanceRequest(); - - const [pageBefore, setPageBefore] = useState(1); - const [pageAfter, setPageAfter] = useState(1); - - const totalAfter = pageAfter * PAGE_SIZE; - const totalBefore = args?.date ? pageBefore * PAGE_SIZE : 0; - - /** - * FIXME: this can be cleaned up a little - * - * the logic of double query should be inside the orderFetch so from the hook perspective and cache - * is just one query and one error status - */ - const { - data: beforeData, - error: beforeError, - isValidating: loadingBefore, - } = useSWR< - HttpResponseOk<MerchantBackend.Orders.OrderHistory>, - RequestError<MerchantBackend.ErrorDetail> - >( - [ - `/private/orders`, - args?.paid, - args?.refunded, - args?.wired, - args?.date, - totalBefore, - ], - orderFetcher, - ); - const { - data: afterData, - error: afterError, - isValidating: loadingAfter, - } = useSWR< - HttpResponseOk<MerchantBackend.Orders.OrderHistory>, - RequestError<MerchantBackend.ErrorDetail> - >( - [ - `/private/orders`, - args?.paid, - args?.refunded, - args?.wired, - args?.date, - -totalAfter, - ], - orderFetcher, - ); - - //this will save last result - const [lastBefore, setLastBefore] = useState< - HttpResponse< - MerchantBackend.Orders.OrderHistory, - MerchantBackend.ErrorDetail - > - >({ loading: true }); - const [lastAfter, setLastAfter] = useState< - HttpResponse< - MerchantBackend.Orders.OrderHistory, - MerchantBackend.ErrorDetail - > - >({ loading: true }); - useEffect(() => { - if (afterData) setLastAfter(afterData); - if (beforeData) setLastBefore(beforeData); - }, [afterData, beforeData]); - - if (beforeError) return beforeError.cause; - if (afterError) return afterError.cause; - - // if the query returns less that we ask, then we have reach the end or beginning - const isReachingEnd = afterData && afterData.data.orders.length < totalAfter; - const isReachingStart = - args?.date === undefined || - (beforeData && beforeData.data.orders.length < totalBefore); - - const pagination = { - isReachingEnd, - isReachingStart, - loadMore: () => { - if (!afterData || isReachingEnd) return; - if (afterData.data.orders.length < MAX_RESULT_SIZE) { - setPageAfter(pageAfter + 1); - } else { - const from = - afterData.data.orders[afterData.data.orders.length - 1].timestamp.t_s; - if (from && from !== "never" && updateFilter) - updateFilter(new Date(from * 1000)); - } - }, - loadMorePrev: () => { - if (!beforeData || isReachingStart) return; - if (beforeData.data.orders.length < MAX_RESULT_SIZE) { - setPageBefore(pageBefore + 1); - } else if (beforeData) { - const from = - beforeData.data.orders[beforeData.data.orders.length - 1].timestamp - .t_s; - if (from && from !== "never" && updateFilter) - updateFilter(new Date(from * 1000)); - } - }, - }; - - const orders = - !beforeData || !afterData - ? [] - : (beforeData || lastBefore).data.orders - .slice() - .reverse() - .concat((afterData || lastAfter).data.orders); - if (loadingAfter || loadingBefore) return { loading: true, data: { orders } }; - if (beforeData && afterData) { - return { ok: true, data: { orders }, ...pagination }; - } - return { loading: true }; -} diff --git a/packages/auditor-backoffice-ui/src/hooks/otp.ts b/packages/auditor-backoffice-ui/src/hooks/otp.ts deleted file mode 100644 index b045e365a..000000000 --- a/packages/auditor-backoffice-ui/src/hooks/otp.ts +++ /dev/null @@ -1,223 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ -import { - HttpResponse, - HttpResponseOk, - HttpResponsePaginated, - RequestError, -} from "@gnu-taler/web-util/browser"; -import { useEffect, useState } from "preact/hooks"; -import { MerchantBackend } from "../declaration.js"; -import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js"; -import { useBackendInstanceRequest, useMatchMutate } from "./backend.js"; - -// FIX default import https://github.com/microsoft/TypeScript/issues/49189 -import _useSWR, { SWRHook } from "swr"; -const useSWR = _useSWR as unknown as SWRHook; - -const MOCKED_DEVICES: Record<string, MerchantBackend.OTP.OtpDeviceAddDetails> = { - "1": { - otp_device_description: "first device", - otp_algorithm: 1, - otp_device_id: "1", - otp_key: "123", - }, - "2": { - otp_device_description: "second device", - otp_algorithm: 0, - otp_device_id: "2", - otp_key: "456", - } -} - -export function useOtpDeviceAPI(): OtpDeviceAPI { - const mutateAll = useMatchMutate(); - const { request } = useBackendInstanceRequest(); - - const createOtpDevice = async ( - data: MerchantBackend.OTP.OtpDeviceAddDetails, - ): Promise<HttpResponseOk<void>> => { - // MOCKED_DEVICES[data.otp_device_id] = data - // return Promise.resolve({ ok: true, data: undefined }); - const res = await request<void>(`/private/otp-devices`, { - method: "POST", - data, - }); - await mutateAll(/.*private\/otp-devices.*/); - return res; - }; - - const updateOtpDevice = async ( - deviceId: string, - data: MerchantBackend.OTP.OtpDevicePatchDetails, - ): Promise<HttpResponseOk<void>> => { - // MOCKED_DEVICES[deviceId].otp_algorithm = data.otp_algorithm - // MOCKED_DEVICES[deviceId].otp_ctr = data.otp_ctr - // MOCKED_DEVICES[deviceId].otp_device_description = data.otp_device_description - // MOCKED_DEVICES[deviceId].otp_key = data.otp_key - // return Promise.resolve({ ok: true, data: undefined }); - const res = await request<void>(`/private/otp-devices/${deviceId}`, { - method: "PATCH", - data, - }); - await mutateAll(/.*private\/otp-devices.*/); - return res; - }; - - const deleteOtpDevice = async ( - deviceId: string, - ): Promise<HttpResponseOk<void>> => { - // delete MOCKED_DEVICES[deviceId] - // return Promise.resolve({ ok: true, data: undefined }); - const res = await request<void>(`/private/otp-devices/${deviceId}`, { - method: "DELETE", - }); - await mutateAll(/.*private\/otp-devices.*/); - return res; - }; - - return { - createOtpDevice, - updateOtpDevice, - deleteOtpDevice, - }; -} - -export interface OtpDeviceAPI { - createOtpDevice: ( - data: MerchantBackend.OTP.OtpDeviceAddDetails, - ) => Promise<HttpResponseOk<void>>; - updateOtpDevice: ( - id: string, - data: MerchantBackend.OTP.OtpDevicePatchDetails, - ) => Promise<HttpResponseOk<void>>; - deleteOtpDevice: (id: string) => Promise<HttpResponseOk<void>>; -} - -export interface InstanceOtpDeviceFilter { -} - -export function useInstanceOtpDevices( - args?: InstanceOtpDeviceFilter, - updatePosition?: (id: string) => void, -): HttpResponsePaginated< - MerchantBackend.OTP.OtpDeviceSummaryResponse, - MerchantBackend.ErrorDetail -> { - // return { - // ok: true, - // loadMore: () => { }, - // loadMorePrev: () => { }, - // data: { - // otp_devices: Object.values(MOCKED_DEVICES).map(d => ({ - // device_description: d.otp_device_description, - // otp_device_id: d.otp_device_id - // })) - // } - // } - - const { fetcher } = useBackendInstanceRequest(); - - const [pageAfter, setPageAfter] = useState(1); - - const totalAfter = pageAfter * PAGE_SIZE; - const { - data: afterData, - error: afterError, - isValidating: loadingAfter, - } = useSWR< - HttpResponseOk<MerchantBackend.OTP.OtpDeviceSummaryResponse>, - RequestError<MerchantBackend.ErrorDetail> - >([`/private/otp-devices`], fetcher); - - const [lastAfter, setLastAfter] = useState< - HttpResponse< - MerchantBackend.OTP.OtpDeviceSummaryResponse, - MerchantBackend.ErrorDetail - > - >({ loading: true }); - useEffect(() => { - if (afterData) setLastAfter(afterData); - }, [afterData /*, beforeData*/]); - - if (afterError) return afterError.cause; - - // if the query returns less that we ask, then we have reach the end or beginning - const isReachingEnd = - afterData && afterData.data.otp_devices.length < totalAfter; - const isReachingStart = true; - - const pagination = { - isReachingEnd, - isReachingStart, - loadMore: () => { - if (!afterData || isReachingEnd) return; - if (afterData.data.otp_devices.length < MAX_RESULT_SIZE) { - setPageAfter(pageAfter + 1); - } else { - const from = `${afterData.data.otp_devices[afterData.data.otp_devices.length - 1] - .otp_device_id - }`; - if (from && updatePosition) updatePosition(from); - } - }, - loadMorePrev: () => { - }, - }; - - const otp_devices = !afterData ? [] : (afterData || lastAfter).data.otp_devices; - if (loadingAfter /* || loadingBefore */) - return { loading: true, data: { otp_devices } }; - if (/*beforeData &&*/ afterData) { - return { ok: true, data: { otp_devices }, ...pagination }; - } - return { loading: true }; -} - -export function useOtpDeviceDetails( - deviceId: string, -): HttpResponse< - MerchantBackend.OTP.OtpDeviceDetails, - MerchantBackend.ErrorDetail -> { - // return { - // ok: true, - // data: { - // device_description: MOCKED_DEVICES[deviceId].otp_device_description, - // otp_algorithm: MOCKED_DEVICES[deviceId].otp_algorithm, - // otp_ctr: MOCKED_DEVICES[deviceId].otp_ctr - // } - // } - const { fetcher } = useBackendInstanceRequest(); - - const { data, error, isValidating } = useSWR< - HttpResponseOk<MerchantBackend.OTP.OtpDeviceDetails>, - RequestError<MerchantBackend.ErrorDetail> - >([`/private/otp-devices/${deviceId}`], fetcher, { - refreshInterval: 0, - refreshWhenHidden: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshWhenOffline: false, - }); - - if (isValidating) return { loading: true, data: data?.data }; - if (data) { - return data; - } - if (error) return error.cause; - return { loading: true }; -} diff --git a/packages/auditor-backoffice-ui/src/hooks/product.test.ts b/packages/auditor-backoffice-ui/src/hooks/product.test.ts deleted file mode 100644 index 7cac10e25..000000000 --- a/packages/auditor-backoffice-ui/src/hooks/product.test.ts +++ /dev/null @@ -1,362 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import * as tests from "@gnu-taler/web-util/testing"; -import { expect } from "chai"; -import { MerchantBackend } from "../declaration.js"; -import { - useInstanceProducts, - useProductAPI, - useProductDetails, -} from "./product.js"; -import { ApiMockEnvironment } from "./testing.js"; -import { - API_CREATE_PRODUCT, - API_DELETE_PRODUCT, - API_GET_PRODUCT_BY_ID, - API_LIST_PRODUCTS, - API_UPDATE_PRODUCT_BY_ID, -} from "./urls.js"; - -describe("product api interaction with listing", () => { - it("should evict cache when creating a product", async () => { - const env = new ApiMockEnvironment(); - - env.addRequestExpectation(API_LIST_PRODUCTS, { - response: { - products: [{ product_id: "1234" }], - }, - }); - env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { - response: { price: "ARS:12" } as MerchantBackend.Products.ProductDetail, - }); - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const query = useInstanceProducts(); - const api = useProductAPI(); - return { query, api }; - }, - {}, - [ - ({ query, api }) => { - expect(query.loading).true; - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).true; - }, - ({ query, api }) => { - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals([{ id: "1234", price: "ARS:12" }]); - - env.addRequestExpectation(API_CREATE_PRODUCT, { - request: { - price: "ARS:23", - } as MerchantBackend.Products.ProductAddDetail, - }); - - env.addRequestExpectation(API_LIST_PRODUCTS, { - response: { - products: [{ product_id: "1234" }, { product_id: "2345" }], - }, - }); - env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { - response: { - price: "ARS:12", - } as MerchantBackend.Products.ProductDetail, - }); - env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { - response: { - price: "ARS:12", - } as MerchantBackend.Products.ProductDetail, - }); - env.addRequestExpectation(API_GET_PRODUCT_BY_ID("2345"), { - response: { - price: "ARS:23", - } as MerchantBackend.Products.ProductDetail, - }); - - api.createProduct({ - price: "ARS:23", - } as any); - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).true; - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals([ - { - id: "1234", - price: "ARS:12", - }, - { - id: "2345", - price: "ARS:23", - }, - ]); - }, - ], - env.buildTestingContext(), - ); - - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - }); - - it("should evict cache when updating a product", async () => { - const env = new ApiMockEnvironment(); - - env.addRequestExpectation(API_LIST_PRODUCTS, { - response: { - products: [{ product_id: "1234" }], - }, - }); - env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { - response: { price: "ARS:12" } as MerchantBackend.Products.ProductDetail, - }); - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const query = useInstanceProducts(); - const api = useProductAPI(); - return { query, api }; - }, - {}, - [ - ({ query, api }) => { - expect(query.loading).true; - }, - ({ query, api }) => { - expect(query.loading).true; - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals([{ id: "1234", price: "ARS:12" }]); - - env.addRequestExpectation(API_UPDATE_PRODUCT_BY_ID("1234"), { - request: { - price: "ARS:13", - } as MerchantBackend.Products.ProductPatchDetail, - }); - - env.addRequestExpectation(API_LIST_PRODUCTS, { - response: { - products: [{ product_id: "1234" }], - }, - }); - env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { - response: { - price: "ARS:13", - } as MerchantBackend.Products.ProductDetail, - }); - - api.updateProduct("1234", { - price: "ARS:13", - } as any); - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals([ - { - id: "1234", - price: "ARS:13", - }, - ]); - }, - ], - env.buildTestingContext(), - ); - - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - }); - - it("should evict cache when deleting a product", async () => { - const env = new ApiMockEnvironment(); - - env.addRequestExpectation(API_LIST_PRODUCTS, { - response: { - products: [{ product_id: "1234" }, { product_id: "2345" }], - }, - }); - env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { - response: { price: "ARS:12" } as MerchantBackend.Products.ProductDetail, - }); - env.addRequestExpectation(API_GET_PRODUCT_BY_ID("2345"), { - response: { price: "ARS:23" } as MerchantBackend.Products.ProductDetail, - }); - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const query = useInstanceProducts(); - const api = useProductAPI(); - return { query, api }; - }, - {}, - [ - ({ query, api }) => { - expect(query.loading).true; - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).true; - }, - ({ query, api }) => { - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals([ - { id: "1234", price: "ARS:12" }, - { id: "2345", price: "ARS:23" }, - ]); - - env.addRequestExpectation(API_DELETE_PRODUCT("2345"), {}); - - env.addRequestExpectation(API_LIST_PRODUCTS, { - response: { - products: [{ product_id: "1234" }], - }, - }); - - env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { - response: { - price: "ARS:12", - } as MerchantBackend.Products.ProductDetail, - }); - api.deleteProduct("2345"); - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).true; - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals([{ id: "1234", price: "ARS:12" }]); - }, - ], - env.buildTestingContext(), - ); - - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - }); -}); - -describe("product api interaction with details", () => { - it("should evict cache when updating a product", async () => { - const env = new ApiMockEnvironment(); - - env.addRequestExpectation(API_GET_PRODUCT_BY_ID("12"), { - response: { - description: "this is a description", - } as MerchantBackend.Products.ProductDetail, - }); - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const query = useProductDetails("12"); - const api = useProductAPI(); - return { query, api }; - }, - {}, - [ - ({ query, api }) => { - expect(query.loading).true; - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - description: "this is a description", - }); - - env.addRequestExpectation(API_UPDATE_PRODUCT_BY_ID("12"), { - request: { - description: "other description", - } as MerchantBackend.Products.ProductPatchDetail, - }); - - env.addRequestExpectation(API_GET_PRODUCT_BY_ID("12"), { - response: { - description: "other description", - } as MerchantBackend.Products.ProductDetail, - }); - - api.updateProduct("12", { - description: "other description", - } as any); - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - description: "other description", - }); - }, - ], - env.buildTestingContext(), - ); - - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - }); -}); diff --git a/packages/auditor-backoffice-ui/src/hooks/product.ts b/packages/auditor-backoffice-ui/src/hooks/product.ts deleted file mode 100644 index 8ca8d2724..000000000 --- a/packages/auditor-backoffice-ui/src/hooks/product.ts +++ /dev/null @@ -1,177 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ -import { - HttpResponse, - HttpResponseOk, - RequestError, -} from "@gnu-taler/web-util/browser"; -import { MerchantBackend, WithId } from "../declaration.js"; -import { useBackendInstanceRequest, useMatchMutate } from "./backend.js"; - -// FIX default import https://github.com/microsoft/TypeScript/issues/49189 -import _useSWR, { SWRHook, useSWRConfig } from "swr"; -const useSWR = _useSWR as unknown as SWRHook; - -export interface ProductAPI { - getProduct: ( - id: string, - ) => Promise<void>; - createProduct: ( - data: MerchantBackend.Products.ProductAddDetail, - ) => Promise<void>; - updateProduct: ( - id: string, - data: MerchantBackend.Products.ProductPatchDetail, - ) => Promise<void>; - deleteProduct: (id: string) => Promise<void>; - lockProduct: ( - id: string, - data: MerchantBackend.Products.LockRequest, - ) => Promise<void>; -} - -export function useProductAPI(): ProductAPI { - const mutateAll = useMatchMutate(); - const { mutate } = useSWRConfig(); - - const { request } = useBackendInstanceRequest(); - - const createProduct = async ( - data: MerchantBackend.Products.ProductAddDetail, - ): Promise<void> => { - const res = await request(`/private/products`, { - method: "POST", - data, - }); - - return await mutateAll(/.*\/private\/products.*/); - }; - - const updateProduct = async ( - productId: string, - data: MerchantBackend.Products.ProductPatchDetail, - ): Promise<void> => { - const r = await request(`/private/products/${productId}`, { - method: "PATCH", - data, - }); - - return await mutateAll(/.*\/private\/products.*/); - }; - - const deleteProduct = async (productId: string): Promise<void> => { - await request(`/private/products/${productId}`, { - method: "DELETE", - }); - await mutate([`/private/products`]); - }; - - const lockProduct = async ( - productId: string, - data: MerchantBackend.Products.LockRequest, - ): Promise<void> => { - await request(`/private/products/${productId}/lock`, { - method: "POST", - data, - }); - - return await mutateAll(/.*"\/private\/products.*/); - }; - - const getProduct = async ( - productId: string, - ): Promise<void> => { - await request(`/private/products/${productId}`, { - method: "GET", - }); - - return - }; - - return { createProduct, updateProduct, deleteProduct, lockProduct, getProduct }; -} - -export function useInstanceProducts(): HttpResponse< - (MerchantBackend.Products.ProductDetail & WithId)[], - MerchantBackend.ErrorDetail -> { - const { fetcher, multiFetcher } = useBackendInstanceRequest(); - - const { data: list, error: listError } = useSWR< - HttpResponseOk<MerchantBackend.Products.InventorySummaryResponse>, - RequestError<MerchantBackend.ErrorDetail> - >([`/private/products`], fetcher, { - refreshInterval: 0, - refreshWhenHidden: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshWhenOffline: false, - }); - - const paths = (list?.data.products || []).map( - (p) => `/private/products/${p.product_id}`, - ); - const { data: products, error: productError } = useSWR< - HttpResponseOk<MerchantBackend.Products.ProductDetail>[], - RequestError<MerchantBackend.ErrorDetail> - >([paths], multiFetcher, { - refreshInterval: 0, - refreshWhenHidden: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshWhenOffline: false, - }); - - if (listError) return listError.cause; - if (productError) return productError.cause; - - if (products) { - const dataWithId = products.map((d) => { - //take the id from the queried url - return { - ...d.data, - id: d.info?.url.replace(/.*\/private\/products\//, "") || "", - }; - }); - return { ok: true, data: dataWithId }; - } - return { loading: true }; -} - -export function useProductDetails( - productId: string, -): HttpResponse< - MerchantBackend.Products.ProductDetail, - MerchantBackend.ErrorDetail -> { - const { fetcher } = useBackendInstanceRequest(); - - const { data, error, isValidating } = useSWR< - HttpResponseOk<MerchantBackend.Products.ProductDetail>, - RequestError<MerchantBackend.ErrorDetail> - >([`/private/products/${productId}`], fetcher, { - refreshInterval: 0, - refreshWhenHidden: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshWhenOffline: false, - }); - - if (isValidating) return { loading: true, data: data?.data }; - if (data) return data; - if (error) return error.cause; - return { loading: true }; -} diff --git a/packages/auditor-backoffice-ui/src/hooks/reserve.test.ts b/packages/auditor-backoffice-ui/src/hooks/reserve.test.ts deleted file mode 100644 index b3eecd754..000000000 --- a/packages/auditor-backoffice-ui/src/hooks/reserve.test.ts +++ /dev/null @@ -1,448 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { expect } from "chai"; -import { MerchantBackend } from "../declaration.js"; -import { - useInstanceReserves, - useReserveDetails, - useReservesAPI, - useRewardDetails, -} from "./reserves.js"; -import { ApiMockEnvironment } from "./testing.js"; -import { - API_AUTHORIZE_REWARD, - API_AUTHORIZE_REWARD_FOR_RESERVE, - API_CREATE_RESERVE, - API_DELETE_RESERVE, - API_GET_RESERVE_BY_ID, - API_GET_REWARD_BY_ID, - API_LIST_RESERVES, -} from "./urls.js"; -import * as tests from "@gnu-taler/web-util/testing"; - -describe("reserve api interaction with listing", () => { - it("should evict cache when creating a reserve", async () => { - const env = new ApiMockEnvironment(); - - env.addRequestExpectation(API_LIST_RESERVES, { - response: { - reserves: [ - { - reserve_pub: "11", - } as MerchantBackend.Rewards.ReserveStatusEntry, - ], - }, - }); - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const api = useReservesAPI(); - const query = useInstanceReserves(); - return { query, api }; - }, - {}, - [ - ({ query, api }) => { - expect(query.loading).true; - }, - ({ query, api }) => { - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - reserves: [{ reserve_pub: "11" }], - }); - - env.addRequestExpectation(API_CREATE_RESERVE, { - request: { - initial_balance: "ARS:3333", - exchange_url: "http://url", - wire_method: "iban", - }, - response: { - reserve_pub: "22", - accounts: [], - }, - }); - - env.addRequestExpectation(API_LIST_RESERVES, { - response: { - reserves: [ - { - reserve_pub: "11", - } as MerchantBackend.Rewards.ReserveStatusEntry, - { - reserve_pub: "22", - } as MerchantBackend.Rewards.ReserveStatusEntry, - ], - }, - }); - - api.createReserve({ - initial_balance: "ARS:3333", - exchange_url: "http://url", - wire_method: "iban", - }); - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - - expect(query.data).deep.equals({ - reserves: [ - { - reserve_pub: "11", - } as MerchantBackend.Rewards.ReserveStatusEntry, - { - reserve_pub: "22", - } as MerchantBackend.Rewards.ReserveStatusEntry, - ], - }); - }, - ], - env.buildTestingContext(), - ); - - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - }); - - it("should evict cache when deleting a reserve", async () => { - const env = new ApiMockEnvironment(); - - env.addRequestExpectation(API_LIST_RESERVES, { - response: { - reserves: [ - { - reserve_pub: "11", - } as MerchantBackend.Rewards.ReserveStatusEntry, - { - reserve_pub: "22", - } as MerchantBackend.Rewards.ReserveStatusEntry, - { - reserve_pub: "33", - } as MerchantBackend.Rewards.ReserveStatusEntry, - ], - }, - }); - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const api = useReservesAPI(); - const query = useInstanceReserves(); - return { query, api }; - }, - {}, - [ - ({ query, api }) => { - expect(query.loading).true; - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - reserves: [ - { reserve_pub: "11" }, - { reserve_pub: "22" }, - { reserve_pub: "33" }, - ], - }); - - env.addRequestExpectation(API_DELETE_RESERVE("11"), {}); - env.addRequestExpectation(API_LIST_RESERVES, { - response: { - reserves: [ - { - reserve_pub: "22", - } as MerchantBackend.Rewards.ReserveStatusEntry, - { - reserve_pub: "33", - } as MerchantBackend.Rewards.ReserveStatusEntry, - ], - }, - }); - - api.deleteReserve("11"); - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - reserves: [{ reserve_pub: "22" }, { reserve_pub: "33" }], - }); - }, - ], - env.buildTestingContext(), - ); - - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - }); -}); - -describe("reserve api interaction with details", () => { - it("should evict cache when adding a reward for a specific reserve", async () => { - const env = new ApiMockEnvironment(); - - env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), { - response: { - accounts: [{ payto_uri: "payto://here" }], - rewards: [{ reason: "why?", reward_id: "id1", total_amount: "USD:10" }], - } as MerchantBackend.Rewards.ReserveDetail, - qparam: { - rewards: "yes", - }, - }); - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const api = useReservesAPI(); - const query = useReserveDetails("11"); - return { query, api }; - }, - {}, - [ - ({ query, api }) => { - expect(query.loading).true; - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - accounts: [{ payto_uri: "payto://here" }], - rewards: [{ reason: "why?", reward_id: "id1", total_amount: "USD:10" }], - }); - - env.addRequestExpectation(API_AUTHORIZE_REWARD_FOR_RESERVE("11"), { - request: { - amount: "USD:12", - justification: "not", - next_url: "http://taler.net", - }, - response: { - reward_id: "id2", - taler_reward_uri: "uri", - reward_expiration: { t_s: 1 }, - reward_status_url: "url", - }, - }); - - env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), { - response: { - accounts: [{ payto_uri: "payto://here" }], - rewards: [ - { reason: "why?", reward_id: "id1", total_amount: "USD:10" }, - { reason: "not", reward_id: "id2", total_amount: "USD:12" }, - ], - } as MerchantBackend.Rewards.ReserveDetail, - qparam: { - rewards: "yes", - }, - }); - - api.authorizeRewardReserve("11", { - amount: "USD:12", - justification: "not", - next_url: "http://taler.net", - }); - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).false; - - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - - expect(query.data).deep.equals({ - accounts: [{ payto_uri: "payto://here" }], - rewards: [ - { reason: "why?", reward_id: "id1", total_amount: "USD:10" }, - { reason: "not", reward_id: "id2", total_amount: "USD:12" }, - ], - }); - }, - ], - env.buildTestingContext(), - ); - - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - }); - - it("should evict cache when adding a reward for a random reserve", async () => { - const env = new ApiMockEnvironment(); - - env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), { - response: { - accounts: [{ payto_uri: "payto://here" }], - rewards: [{ reason: "why?", reward_id: "id1", total_amount: "USD:10" }], - } as MerchantBackend.Rewards.ReserveDetail, - qparam: { - rewards: "yes", - }, - }); - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const api = useReservesAPI(); - const query = useReserveDetails("11"); - return { query, api }; - }, - {}, - [ - ({ query, api }) => { - expect(query.loading).true; - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - accounts: [{ payto_uri: "payto://here" }], - rewards: [{ reason: "why?", reward_id: "id1", total_amount: "USD:10" }], - }); - - env.addRequestExpectation(API_AUTHORIZE_REWARD, { - request: { - amount: "USD:12", - justification: "not", - next_url: "http://taler.net", - }, - response: { - reward_id: "id2", - taler_reward_uri: "uri", - reward_expiration: { t_s: 1 }, - reward_status_url: "url", - }, - }); - - env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), { - response: { - accounts: [{ payto_uri: "payto://here" }], - rewards: [ - { reason: "why?", reward_id: "id1", total_amount: "USD:10" }, - { reason: "not", reward_id: "id2", total_amount: "USD:12" }, - ], - } as MerchantBackend.Rewards.ReserveDetail, - qparam: { - rewards: "yes", - }, - }); - - api.authorizeReward({ - amount: "USD:12", - justification: "not", - next_url: "http://taler.net", - }); - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - - expect(query.data).deep.equals({ - accounts: [{ payto_uri: "payto://here" }], - rewards: [ - { reason: "why?", reward_id: "id1", total_amount: "USD:10" }, - { reason: "not", reward_id: "id2", total_amount: "USD:12" }, - ], - }); - }, - ], - env.buildTestingContext(), - ); - - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - }); -}); - -describe("reserve api interaction with reward details", () => { - it("should list rewards", async () => { - const env = new ApiMockEnvironment(); - - env.addRequestExpectation(API_GET_REWARD_BY_ID("11"), { - response: { - total_picked_up: "USD:12", - reason: "not", - } as MerchantBackend.Rewards.RewardDetails, - qparam: { - pickups: "yes", - }, - }); - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const query = useRewardDetails("11"); - return { query }; - }, - {}, - [ - ({ query }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).true; - }, - ({ query }) => { - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - total_picked_up: "USD:12", - reason: "not", - }); - }, - ], - env.buildTestingContext(), - ); - - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - }); -}); diff --git a/packages/auditor-backoffice-ui/src/hooks/reserves.ts b/packages/auditor-backoffice-ui/src/hooks/reserves.ts deleted file mode 100644 index b719bfbe6..000000000 --- a/packages/auditor-backoffice-ui/src/hooks/reserves.ts +++ /dev/null @@ -1,181 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ -import { - HttpResponse, - HttpResponseOk, - RequestError, -} from "@gnu-taler/web-util/browser"; -import { MerchantBackend } from "../declaration.js"; -import { useBackendInstanceRequest, useMatchMutate } from "./backend.js"; - -// FIX default import https://github.com/microsoft/TypeScript/issues/49189 -import _useSWR, { SWRHook, useSWRConfig } from "swr"; -const useSWR = _useSWR as unknown as SWRHook; - -export function useReservesAPI(): ReserveMutateAPI { - const mutateAll = useMatchMutate(); - const { mutate } = useSWRConfig(); - const { request } = useBackendInstanceRequest(); - - const createReserve = async ( - data: MerchantBackend.Rewards.ReserveCreateRequest, - ): Promise< - HttpResponseOk<MerchantBackend.Rewards.ReserveCreateConfirmation> - > => { - const res = await request<MerchantBackend.Rewards.ReserveCreateConfirmation>( - `/private/reserves`, - { - method: "POST", - data, - }, - ); - - //evict reserve list query - await mutateAll(/.*private\/reserves.*/); - - return res; - }; - - const authorizeRewardReserve = async ( - pub: string, - data: MerchantBackend.Rewards.RewardCreateRequest, - ): Promise<HttpResponseOk<MerchantBackend.Rewards.RewardCreateConfirmation>> => { - const res = await request<MerchantBackend.Rewards.RewardCreateConfirmation>( - `/private/reserves/${pub}/authorize-reward`, - { - method: "POST", - data, - }, - ); - - //evict reserve details query - await mutate([`/private/reserves/${pub}`]); - - return res; - }; - - const authorizeReward = async ( - data: MerchantBackend.Rewards.RewardCreateRequest, - ): Promise<HttpResponseOk<MerchantBackend.Rewards.RewardCreateConfirmation>> => { - const res = await request<MerchantBackend.Rewards.RewardCreateConfirmation>( - `/private/rewards`, - { - method: "POST", - data, - }, - ); - - //evict all details query - await mutateAll(/.*private\/reserves\/.*/); - - return res; - }; - - const deleteReserve = async ( - pub: string, - ): Promise<HttpResponse<void, MerchantBackend.ErrorDetail>> => { - const res = await request<void>(`/private/reserves/${pub}`, { - method: "DELETE", - }); - - //evict reserve list query - await mutateAll(/.*private\/reserves.*/); - - return res; - }; - - return { createReserve, authorizeReward, authorizeRewardReserve, deleteReserve }; -} - -export interface ReserveMutateAPI { - createReserve: ( - data: MerchantBackend.Rewards.ReserveCreateRequest, - ) => Promise<HttpResponseOk<MerchantBackend.Rewards.ReserveCreateConfirmation>>; - authorizeRewardReserve: ( - id: string, - data: MerchantBackend.Rewards.RewardCreateRequest, - ) => Promise<HttpResponseOk<MerchantBackend.Rewards.RewardCreateConfirmation>>; - authorizeReward: ( - data: MerchantBackend.Rewards.RewardCreateRequest, - ) => Promise<HttpResponseOk<MerchantBackend.Rewards.RewardCreateConfirmation>>; - deleteReserve: ( - id: string, - ) => Promise<HttpResponse<void, MerchantBackend.ErrorDetail>>; -} - -export function useInstanceReserves(): HttpResponse< - MerchantBackend.Rewards.RewardReserveStatus, - MerchantBackend.ErrorDetail -> { - const { fetcher } = useBackendInstanceRequest(); - - const { data, error, isValidating } = useSWR< - HttpResponseOk<MerchantBackend.Rewards.RewardReserveStatus>, - RequestError<MerchantBackend.ErrorDetail> - >([`/private/reserves`], fetcher); - - if (isValidating) return { loading: true, data: data?.data }; - if (data) return data; - if (error) return error.cause; - return { loading: true }; -} - -export function useReserveDetails( - reserveId: string, -): HttpResponse< - MerchantBackend.Rewards.ReserveDetail, - MerchantBackend.ErrorDetail -> { - const { reserveDetailFetcher } = useBackendInstanceRequest(); - - const { data, error, isValidating } = useSWR< - HttpResponseOk<MerchantBackend.Rewards.ReserveDetail>, - RequestError<MerchantBackend.ErrorDetail> - >([`/private/reserves/${reserveId}`], reserveDetailFetcher, { - refreshInterval: 0, - refreshWhenHidden: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshWhenOffline: false, - }); - - if (isValidating) return { loading: true, data: data?.data }; - if (data) return data; - if (error) return error.cause; - return { loading: true }; -} - -export function useRewardDetails( - rewardId: string, -): HttpResponse<MerchantBackend.Rewards.RewardDetails, MerchantBackend.ErrorDetail> { - const { rewardsDetailFetcher } = useBackendInstanceRequest(); - - const { data, error, isValidating } = useSWR< - HttpResponseOk<MerchantBackend.Rewards.RewardDetails>, - RequestError<MerchantBackend.ErrorDetail> - >([`/private/rewards/${rewardId}`], rewardsDetailFetcher, { - refreshInterval: 0, - refreshWhenHidden: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshWhenOffline: false, - }); - - if (isValidating) return { loading: true, data: data?.data }; - if (data) return data; - if (error) return error.cause; - return { loading: true }; -} diff --git a/packages/auditor-backoffice-ui/src/hooks/templates.ts b/packages/auditor-backoffice-ui/src/hooks/templates.ts deleted file mode 100644 index ee8728cc8..000000000 --- a/packages/auditor-backoffice-ui/src/hooks/templates.ts +++ /dev/null @@ -1,266 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ -import { - HttpResponse, - HttpResponseOk, - HttpResponsePaginated, - RequestError, -} from "@gnu-taler/web-util/browser"; -import { useEffect, useState } from "preact/hooks"; -import { MerchantBackend } from "../declaration.js"; -import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js"; -import { useBackendInstanceRequest, useMatchMutate } from "./backend.js"; - -// FIX default import https://github.com/microsoft/TypeScript/issues/49189 -import _useSWR, { SWRHook } from "swr"; -const useSWR = _useSWR as unknown as SWRHook; - -export function useTemplateAPI(): TemplateAPI { - const mutateAll = useMatchMutate(); - const { request } = useBackendInstanceRequest(); - - const createTemplate = async ( - data: MerchantBackend.Template.TemplateAddDetails, - ): Promise<HttpResponseOk<void>> => { - const res = await request<void>(`/private/templates`, { - method: "POST", - data, - }); - await mutateAll(/.*private\/templates.*/); - return res; - }; - - const updateTemplate = async ( - templateId: string, - data: MerchantBackend.Template.TemplatePatchDetails, - ): Promise<HttpResponseOk<void>> => { - const res = await request<void>(`/private/templates/${templateId}`, { - method: "PATCH", - data, - }); - await mutateAll(/.*private\/templates.*/); - return res; - }; - - const deleteTemplate = async ( - templateId: string, - ): Promise<HttpResponseOk<void>> => { - const res = await request<void>(`/private/templates/${templateId}`, { - method: "DELETE", - }); - await mutateAll(/.*private\/templates.*/); - return res; - }; - - const createOrderFromTemplate = async ( - templateId: string, - data: MerchantBackend.Template.UsingTemplateDetails, - ): Promise< - HttpResponseOk<MerchantBackend.Template.UsingTemplateResponse> - > => { - const res = await request<MerchantBackend.Template.UsingTemplateResponse>( - `/templates/${templateId}`, - { - method: "POST", - data, - }, - ); - await mutateAll(/.*private\/templates.*/); - return res; - }; - - const testTemplateExist = async ( - templateId: string, - ): Promise<HttpResponseOk<void>> => { - const res = await request<void>(`/private/templates/${templateId}`, { method: "GET", }); - return res; - }; - - - return { - createTemplate, - updateTemplate, - deleteTemplate, - testTemplateExist, - createOrderFromTemplate, - }; -} - -export interface TemplateAPI { - createTemplate: ( - data: MerchantBackend.Template.TemplateAddDetails, - ) => Promise<HttpResponseOk<void>>; - updateTemplate: ( - id: string, - data: MerchantBackend.Template.TemplatePatchDetails, - ) => Promise<HttpResponseOk<void>>; - testTemplateExist: ( - id: string - ) => Promise<HttpResponseOk<void>>; - deleteTemplate: (id: string) => Promise<HttpResponseOk<void>>; - createOrderFromTemplate: ( - id: string, - data: MerchantBackend.Template.UsingTemplateDetails, - ) => Promise<HttpResponseOk<MerchantBackend.Template.UsingTemplateResponse>>; -} - -export interface InstanceTemplateFilter { - //FIXME: add filter to the template list - position?: string; -} - -export function useInstanceTemplates( - args?: InstanceTemplateFilter, - updatePosition?: (id: string) => void, -): HttpResponsePaginated< - MerchantBackend.Template.TemplateSummaryResponse, - MerchantBackend.ErrorDetail -> { - const { templateFetcher } = useBackendInstanceRequest(); - - const [pageBefore, setPageBefore] = useState(1); - const [pageAfter, setPageAfter] = useState(1); - - const totalAfter = pageAfter * PAGE_SIZE; - const totalBefore = args?.position ? pageBefore * PAGE_SIZE : 0; - - /** - * FIXME: this can be cleaned up a little - * - * the logic of double query should be inside the orderFetch so from the hook perspective and cache - * is just one query and one error status - */ - const { - data: beforeData, - error: beforeError, - isValidating: loadingBefore, - } = useSWR< - HttpResponseOk<MerchantBackend.Template.TemplateSummaryResponse>, - RequestError<MerchantBackend.ErrorDetail>>( - [ - `/private/templates`, - args?.position, - totalBefore, - ], - templateFetcher, - ); - const { - data: afterData, - error: afterError, - isValidating: loadingAfter, - } = useSWR< - HttpResponseOk<MerchantBackend.Template.TemplateSummaryResponse>, - RequestError<MerchantBackend.ErrorDetail> - >([`/private/templates`, args?.position, -totalAfter], templateFetcher); - - //this will save last result - const [lastBefore, setLastBefore] = useState< - HttpResponse< - MerchantBackend.Template.TemplateSummaryResponse, - MerchantBackend.ErrorDetail - > - >({ loading: true }); - - const [lastAfter, setLastAfter] = useState< - HttpResponse< - MerchantBackend.Template.TemplateSummaryResponse, - MerchantBackend.ErrorDetail - > - >({ loading: true }); - useEffect(() => { - if (afterData) setLastAfter(afterData); - if (beforeData) setLastBefore(beforeData); - }, [afterData, beforeData]); - - if (beforeError) return beforeError.cause; - if (afterError) return afterError.cause; - - // if the query returns less that we ask, then we have reach the end or beginning - const isReachingEnd = - afterData && afterData.data.templates.length < totalAfter; - const isReachingStart = args?.position === undefined - || - (beforeData && beforeData.data.templates.length < totalBefore); - - const pagination = { - isReachingEnd, - isReachingStart, - loadMore: () => { - if (!afterData || isReachingEnd) return; - if (afterData.data.templates.length < MAX_RESULT_SIZE) { - setPageAfter(pageAfter + 1); - } else { - const from = `${afterData.data.templates[afterData.data.templates.length - 1] - .template_id - }`; - if (from && updatePosition) updatePosition(from); - } - }, - loadMorePrev: () => { - if (!beforeData || isReachingStart) return; - if (beforeData.data.templates.length < MAX_RESULT_SIZE) { - setPageBefore(pageBefore + 1); - } else if (beforeData) { - const from = `${beforeData.data.templates[beforeData.data.templates.length - 1] - .template_id - }`; - if (from && updatePosition) updatePosition(from); - } - }, - }; - - // const templates = !afterData ? [] : (afterData || lastAfter).data.templates; - const templates = - !beforeData || !afterData - ? [] - : (beforeData || lastBefore).data.templates - .slice() - .reverse() - .concat((afterData || lastAfter).data.templates); - if (loadingAfter || loadingBefore) - return { loading: true, data: { templates } }; - if (beforeData && afterData) { - return { ok: true, data: { templates }, ...pagination }; - } - return { loading: true }; -} - -export function useTemplateDetails( - templateId: string, -): HttpResponse< - MerchantBackend.Template.TemplateDetails, - MerchantBackend.ErrorDetail -> { - const { templateFetcher } = useBackendInstanceRequest(); - - const { data, error, isValidating } = useSWR< - HttpResponseOk<MerchantBackend.Template.TemplateDetails>, - RequestError<MerchantBackend.ErrorDetail> - >([`/private/templates/${templateId}`], templateFetcher, { - refreshInterval: 0, - refreshWhenHidden: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshWhenOffline: false, - }); - - if (isValidating) return { loading: true, data: data?.data }; - if (data) { - return data; - } - if (error) return error.cause; - return { loading: true }; -} diff --git a/packages/auditor-backoffice-ui/src/hooks/testing.tsx b/packages/auditor-backoffice-ui/src/hooks/testing.tsx deleted file mode 100644 index 7955f832a..000000000 --- a/packages/auditor-backoffice-ui/src/hooks/testing.tsx +++ /dev/null @@ -1,180 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { MockEnvironment } from "@gnu-taler/web-util/testing"; -import { ComponentChildren, FunctionalComponent, h, VNode } from "preact"; -import { HttpRequestLibrary, HttpRequestOptions, HttpResponse } from "@gnu-taler/taler-util/http"; -import { SWRConfig } from "swr"; -import { ApiContextProvider } from "@gnu-taler/web-util/browser"; -import { BackendContextProvider } from "../context/backend.js"; -import { InstanceContextProvider } from "../context/instance.js"; -import { HttpResponseOk, RequestOptions } from "@gnu-taler/web-util/browser"; -import { TalerBankIntegrationHttpClient, TalerCoreBankHttpClient, TalerRevenueHttpClient, TalerWireGatewayHttpClient } from "@gnu-taler/taler-util"; - -export class ApiMockEnvironment extends MockEnvironment { - constructor(debug = false) { - super(debug); - } - - mockApiIfNeeded(): void { - null; // do nothing - } - - public buildTestingContext(): FunctionalComponent<{ - children: ComponentChildren; - }> { - const __SAVE_REQUEST_AND_GET_MOCKED_RESPONSE = - this.saveRequestAndGetMockedResponse.bind(this); - - return function TestingContext({ - children, - }: { - children: ComponentChildren; - }): VNode { - - async function request<T>( - base: string, - path: string, - options: RequestOptions = {}, - ): Promise<HttpResponseOk<T>> { - const _url = new URL(`${base}${path}`); - // Object.entries(options.params ?? {}).forEach(([key, value]) => { - // _url.searchParams.set(key, String(value)); - // }); - - const mocked = __SAVE_REQUEST_AND_GET_MOCKED_RESPONSE( - { - method: options.method ?? "GET", - url: _url.href, - }, - { - qparam: options.params, - auth: options.token, - request: options.data, - }, - ); - const status = mocked.expectedQuery?.query.code ?? 200; - const requestPayload = mocked.expectedQuery?.params?.request; - const responsePayload = mocked.expectedQuery?.params?.response; - - return { - ok: true, - data: responsePayload as T, - loading: false, - clientError: false, - serverError: false, - info: { - hasToken: !!options.token, - status, - url: _url.href, - payload: options.data, - options: {}, - }, - }; - } - const SC: any = SWRConfig; - - const mockHttpClient = new class implements HttpRequestLibrary { - async fetch(url: string, options?: HttpRequestOptions | undefined): Promise<HttpResponse> { - const _url = new URL(url); - const mocked = __SAVE_REQUEST_AND_GET_MOCKED_RESPONSE( - { - method: options?.method ?? "GET", - url: _url.href, - }, - { - qparam: _url.searchParams, - auth: options as any, - request: options?.body as any, - }, - ); - const status = mocked.expectedQuery?.query.code ?? 200; - const requestPayload = mocked.expectedQuery?.params?.request; - const responsePayload = mocked.expectedQuery?.params?.response; - - // FIXME: complete this implementation to mock any query - const resp: HttpResponse = { - requestUrl: _url.href, - status: status, - headers: {} as any, - requestMethod: options?.method ?? "GET", - json: async () => responsePayload, - text: async () => responsePayload as any as string, - bytes: async () => responsePayload as ArrayBuffer, - }; - return resp - } - get(url: string, opt?: HttpRequestOptions): Promise<HttpResponse> { - return this.fetch(url, { - method: "GET", - ...opt, - }); - } - - postJson( - url: string, - body: any, - opt?: HttpRequestOptions, - ): Promise<HttpResponse> { - return this.fetch(url, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(body), - ...opt, - }); - } - - } - const bankCore = new TalerCoreBankHttpClient("http://localhost", mockHttpClient) - const bankIntegration = new TalerBankIntegrationHttpClient(bankCore.getIntegrationAPI().href, mockHttpClient) - const bankRevenue = new TalerRevenueHttpClient(bankCore.getRevenueAPI("a").href, mockHttpClient) - const bankWire = new TalerWireGatewayHttpClient(bankCore.getWireGatewayAPI("b").href, "b", mockHttpClient) - - return ( - <BackendContextProvider defaultUrl="http://backend"> - <InstanceContextProvider - value={{ - token: undefined, - id: "default", - admin: true, - changeToken: () => null, - }} - > - <ApiContextProvider value={{ request, bankCore, bankIntegration, bankRevenue, bankWire }}> - <SC - value={{ - loadingTimeout: 0, - dedupingInterval: 0, - shouldRetryOnError: false, - errorRetryInterval: 0, - errorRetryCount: 0, - provider: () => new Map(), - }} - > - {children} - </SC> - </ApiContextProvider> - </InstanceContextProvider> - </BackendContextProvider> - ); - }; - } -} diff --git a/packages/auditor-backoffice-ui/src/hooks/transfer.test.ts b/packages/auditor-backoffice-ui/src/hooks/transfer.test.ts deleted file mode 100644 index a7187af27..000000000 --- a/packages/auditor-backoffice-ui/src/hooks/transfer.test.ts +++ /dev/null @@ -1,254 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import * as tests from "@gnu-taler/web-util/testing"; -import { expect } from "chai"; -import { MerchantBackend } from "../declaration.js"; -import { API_INFORM_TRANSFERS, API_LIST_TRANSFERS } from "./urls.js"; -import { ApiMockEnvironment } from "./testing.js"; -import { useInstanceTransfers, useTransferAPI } from "./transfer.js"; - -describe("transfer api interaction with listing", () => { - it("should evict cache when informing a transfer", async () => { - const env = new ApiMockEnvironment(); - - env.addRequestExpectation(API_LIST_TRANSFERS, { - qparam: { limit: -20 }, - response: { - transfers: [{ wtid: "2" } as MerchantBackend.Transfers.TransferDetails], - }, - }); - - const moveCursor = (d: string) => { - console.log("new position", d); - }; - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const query = useInstanceTransfers({}, moveCursor); - const api = useTransferAPI(); - return { query, api }; - }, - {}, - [ - ({ query, api }) => { - expect(query.loading).true; - }, - - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - transfers: [{ wtid: "2" }], - }); - - env.addRequestExpectation(API_INFORM_TRANSFERS, { - request: { - wtid: "3", - credit_amount: "EUR:1", - exchange_url: "exchange.url", - payto_uri: "payto://", - }, - response: { total: "" } as any, - }); - - env.addRequestExpectation(API_LIST_TRANSFERS, { - qparam: { limit: -20 }, - response: { - transfers: [{ wtid: "3" } as any, { wtid: "2" } as any], - }, - }); - - api.informTransfer({ - wtid: "3", - credit_amount: "EUR:1", - exchange_url: "exchange.url", - payto_uri: "payto://", - }); - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - - expect(query.data).deep.equals({ - transfers: [{ wtid: "3" }, { wtid: "2" }], - }); - }, - ], - env.buildTestingContext(), - ); - - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - }); -}); - -describe("transfer listing pagination", () => { - it("should not load more if has reach the end", async () => { - const env = new ApiMockEnvironment(); - - env.addRequestExpectation(API_LIST_TRANSFERS, { - qparam: { limit: -20, payto_uri: "payto://" }, - response: { - transfers: [{ wtid: "2" }, { wtid: "1" } as any], - }, - }); - - const moveCursor = (d: string) => { - console.log("new position", d); - }; - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - return useInstanceTransfers({ payto_uri: "payto://" }, moveCursor); - }, - {}, - [ - (query) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).true; - }, - (query) => { - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - transfers: [{ wtid: "2" }, { wtid: "1" }], - }); - expect(query.isReachingEnd).true; - expect(query.isReachingStart).true; - - //check that this button won't trigger more updates since - //has reach end and start - query.loadMore(); - query.loadMorePrev(); - }, - ], - env.buildTestingContext(), - ); - - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - expect(hookBehavior).deep.eq({ result: "ok" }); - }); - - it("should load more if result brings more that PAGE_SIZE", async () => { - const env = new ApiMockEnvironment(); - - const transfersFrom0to20 = Array.from({ length: 20 }).map((e, i) => ({ - wtid: String(i), - })); - const transfersFrom20to40 = Array.from({ length: 20 }).map((e, i) => ({ - wtid: String(i + 20), - })); - const transfersFrom20to0 = [...transfersFrom0to20].reverse(); - - env.addRequestExpectation(API_LIST_TRANSFERS, { - qparam: { limit: 20, payto_uri: "payto://", offset: "1" }, - response: { - transfers: transfersFrom0to20, - }, - }); - - env.addRequestExpectation(API_LIST_TRANSFERS, { - qparam: { limit: -20, payto_uri: "payto://", offset: "1" }, - response: { - transfers: transfersFrom20to40, - }, - }); - - const moveCursor = (d: string) => { - console.log("new position", d); - }; - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - return useInstanceTransfers( - { payto_uri: "payto://", position: "1" }, - moveCursor, - ); - }, - {}, - [ - (result) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(result.loading).true; - }, - (result) => { - expect(result.loading).undefined; - expect(result.ok).true; - if (!result.ok) return; - expect(result.data).deep.equals({ - transfers: [...transfersFrom20to0, ...transfersFrom20to40], - }); - expect(result.isReachingEnd).false; - expect(result.isReachingStart).false; - - //query more - env.addRequestExpectation(API_LIST_TRANSFERS, { - qparam: { limit: -40, payto_uri: "payto://", offset: "1" }, - response: { - transfers: [...transfersFrom20to40, { wtid: "41" }], - }, - }); - result.loadMore(); - }, - (result) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(result.loading).true; - }, - (result) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(result.loading).undefined; - expect(result.ok).true; - if (!result.ok) return; - expect(result.data).deep.equals({ - transfers: [ - ...transfersFrom20to0, - ...transfersFrom20to40, - { wtid: "41" }, - ], - }); - expect(result.isReachingEnd).true; - expect(result.isReachingStart).false; - }, - ], - env.buildTestingContext(), - ); - - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - expect(hookBehavior).deep.eq({ result: "ok" }); - }); -}); diff --git a/packages/auditor-backoffice-ui/src/hooks/transfer.ts b/packages/auditor-backoffice-ui/src/hooks/transfer.ts deleted file mode 100644 index 27c3bdc75..000000000 --- a/packages/auditor-backoffice-ui/src/hooks/transfer.ts +++ /dev/null @@ -1,188 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ -import { - HttpResponse, - HttpResponseOk, - HttpResponsePaginated, - RequestError, -} from "@gnu-taler/web-util/browser"; -import { useEffect, useState } from "preact/hooks"; -import { MerchantBackend } from "../declaration.js"; -import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js"; -import { useBackendInstanceRequest, useMatchMutate } from "./backend.js"; - -// FIX default import https://github.com/microsoft/TypeScript/issues/49189 -import _useSWR, { SWRHook } from "swr"; -const useSWR = _useSWR as unknown as SWRHook; - -export function useTransferAPI(): TransferAPI { - const mutateAll = useMatchMutate(); - const { request } = useBackendInstanceRequest(); - - const informTransfer = async ( - data: MerchantBackend.Transfers.TransferInformation, - ): Promise<HttpResponseOk<{}>> => { - const res = await request<{}>(`/private/transfers`, { - method: "POST", - data, - }); - - await mutateAll(/.*private\/transfers.*/); - return res; - }; - - return { informTransfer }; -} - -export interface TransferAPI { - informTransfer: ( - data: MerchantBackend.Transfers.TransferInformation, - ) => Promise<HttpResponseOk<{}>>; -} - -export interface InstanceTransferFilter { - payto_uri?: string; - verified?: "yes" | "no"; - position?: string; -} - -export function useInstanceTransfers( - args?: InstanceTransferFilter, - updatePosition?: (id: string) => void, -): HttpResponsePaginated< - MerchantBackend.Transfers.TransferList, - MerchantBackend.ErrorDetail -> { - const { transferFetcher } = useBackendInstanceRequest(); - - const [pageBefore, setPageBefore] = useState(1); - const [pageAfter, setPageAfter] = useState(1); - - const totalAfter = pageAfter * PAGE_SIZE; - const totalBefore = args?.position !== undefined ? pageBefore * PAGE_SIZE : 0; - - /** - * FIXME: this can be cleaned up a little - * - * the logic of double query should be inside the orderFetch so from the hook perspective and cache - * is just one query and one error status - */ - const { - data: beforeData, - error: beforeError, - isValidating: loadingBefore, - } = useSWR< - HttpResponseOk<MerchantBackend.Transfers.TransferList>, - RequestError<MerchantBackend.ErrorDetail> - >( - [ - `/private/transfers`, - args?.payto_uri, - args?.verified, - args?.position, - totalBefore, - ], - transferFetcher, - ); - const { - data: afterData, - error: afterError, - isValidating: loadingAfter, - } = useSWR< - HttpResponseOk<MerchantBackend.Transfers.TransferList>, - RequestError<MerchantBackend.ErrorDetail> - >( - [ - `/private/transfers`, - args?.payto_uri, - args?.verified, - args?.position, - -totalAfter, - ], - transferFetcher, - ); - - //this will save last result - const [lastBefore, setLastBefore] = useState< - HttpResponse< - MerchantBackend.Transfers.TransferList, - MerchantBackend.ErrorDetail - > - >({ loading: true }); - const [lastAfter, setLastAfter] = useState< - HttpResponse< - MerchantBackend.Transfers.TransferList, - MerchantBackend.ErrorDetail - > - >({ loading: true }); - useEffect(() => { - if (afterData) setLastAfter(afterData); - if (beforeData) setLastBefore(beforeData); - }, [afterData, beforeData]); - - if (beforeError) return beforeError.cause; - if (afterError) return afterError.cause; - - // if the query returns less that we ask, then we have reach the end or beginning - const isReachingEnd = - afterData && afterData.data.transfers.length < totalAfter; - const isReachingStart = - args?.position === undefined || - (beforeData && beforeData.data.transfers.length < totalBefore); - - const pagination = { - isReachingEnd, - isReachingStart, - loadMore: () => { - if (!afterData || isReachingEnd) return; - if (afterData.data.transfers.length < MAX_RESULT_SIZE) { - setPageAfter(pageAfter + 1); - } else { - const from = `${ - afterData.data.transfers[afterData.data.transfers.length - 1] - .transfer_serial_id - }`; - if (from && updatePosition) updatePosition(from); - } - }, - loadMorePrev: () => { - if (!beforeData || isReachingStart) return; - if (beforeData.data.transfers.length < MAX_RESULT_SIZE) { - setPageBefore(pageBefore + 1); - } else if (beforeData) { - const from = `${ - beforeData.data.transfers[beforeData.data.transfers.length - 1] - .transfer_serial_id - }`; - if (from && updatePosition) updatePosition(from); - } - }, - }; - - const transfers = - !beforeData || !afterData - ? [] - : (beforeData || lastBefore).data.transfers - .slice() - .reverse() - .concat((afterData || lastAfter).data.transfers); - if (loadingAfter || loadingBefore) - return { loading: true, data: { transfers } }; - if (beforeData && afterData) { - return { ok: true, data: { transfers }, ...pagination }; - } - return { loading: true }; -} diff --git a/packages/auditor-backoffice-ui/src/hooks/urls.ts b/packages/auditor-backoffice-ui/src/hooks/urls.ts deleted file mode 100644 index b6485259f..000000000 --- a/packages/auditor-backoffice-ui/src/hooks/urls.ts +++ /dev/null @@ -1,303 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ -import { Query } from "@gnu-taler/web-util/testing"; -import { MerchantBackend } from "../declaration.js"; - -//////////////////// -// ORDER -//////////////////// - -export const API_CREATE_ORDER: Query< - MerchantBackend.Orders.PostOrderRequest, - MerchantBackend.Orders.PostOrderResponse -> = { - method: "POST", - url: "http://backend/instances/default/private/orders", -}; - -export const API_GET_ORDER_BY_ID = ( - id: string, -): Query<unknown, MerchantBackend.Orders.MerchantOrderStatusResponse> => ({ - method: "GET", - url: `http://backend/instances/default/private/orders/${id}`, -}); - -export const API_LIST_ORDERS: Query< - unknown, - MerchantBackend.Orders.OrderHistory -> = { - method: "GET", - url: "http://backend/instances/default/private/orders", -}; - -export const API_REFUND_ORDER_BY_ID = ( - id: string, -): Query< - MerchantBackend.Orders.RefundRequest, - MerchantBackend.Orders.MerchantRefundResponse -> => ({ - method: "POST", - url: `http://backend/instances/default/private/orders/${id}/refund`, -}); - -export const API_FORGET_ORDER_BY_ID = ( - id: string, -): Query<MerchantBackend.Orders.ForgetRequest, unknown> => ({ - method: "PATCH", - url: `http://backend/instances/default/private/orders/${id}/forget`, -}); - -export const API_DELETE_ORDER = ( - id: string, -): Query<MerchantBackend.Orders.ForgetRequest, unknown> => ({ - method: "DELETE", - url: `http://backend/instances/default/private/orders/${id}`, -}); - -//////////////////// -// TRANSFER -//////////////////// - -export const API_LIST_TRANSFERS: Query< - unknown, - MerchantBackend.Transfers.TransferList -> = { - method: "GET", - url: "http://backend/instances/default/private/transfers", -}; - -export const API_INFORM_TRANSFERS: Query< - MerchantBackend.Transfers.TransferInformation, - {} -> = { - method: "POST", - url: "http://backend/instances/default/private/transfers", -}; - -//////////////////// -// PRODUCT -//////////////////// - -export const API_CREATE_PRODUCT: Query< - MerchantBackend.Products.ProductAddDetail, - unknown -> = { - method: "POST", - url: "http://backend/instances/default/private/products", -}; - -export const API_LIST_PRODUCTS: Query< - unknown, - MerchantBackend.Products.InventorySummaryResponse -> = { - method: "GET", - url: "http://backend/instances/default/private/products", -}; - -export const API_GET_PRODUCT_BY_ID = ( - id: string, -): Query<unknown, MerchantBackend.Products.ProductDetail> => ({ - method: "GET", - url: `http://backend/instances/default/private/products/${id}`, -}); - -export const API_UPDATE_PRODUCT_BY_ID = ( - id: string, -): Query< - MerchantBackend.Products.ProductPatchDetail, - MerchantBackend.Products.InventorySummaryResponse -> => ({ - method: "PATCH", - url: `http://backend/instances/default/private/products/${id}`, -}); - -export const API_DELETE_PRODUCT = (id: string): Query<unknown, unknown> => ({ - method: "DELETE", - url: `http://backend/instances/default/private/products/${id}`, -}); - -//////////////////// -// RESERVES -//////////////////// - -export const API_CREATE_RESERVE: Query< - MerchantBackend.Rewards.ReserveCreateRequest, - MerchantBackend.Rewards.ReserveCreateConfirmation -> = { - method: "POST", - url: "http://backend/instances/default/private/reserves", -}; -export const API_LIST_RESERVES: Query< - unknown, - MerchantBackend.Rewards.RewardReserveStatus -> = { - method: "GET", - url: "http://backend/instances/default/private/reserves", -}; - -export const API_GET_RESERVE_BY_ID = ( - pub: string, -): Query<unknown, MerchantBackend.Rewards.ReserveDetail> => ({ - method: "GET", - url: `http://backend/instances/default/private/reserves/${pub}`, -}); - -export const API_GET_REWARD_BY_ID = ( - pub: string, -): Query<unknown, MerchantBackend.Rewards.RewardDetails> => ({ - method: "GET", - url: `http://backend/instances/default/private/rewards/${pub}`, -}); - -export const API_AUTHORIZE_REWARD_FOR_RESERVE = ( - pub: string, -): Query< - MerchantBackend.Rewards.RewardCreateRequest, - MerchantBackend.Rewards.RewardCreateConfirmation -> => ({ - method: "POST", - url: `http://backend/instances/default/private/reserves/${pub}/authorize-reward`, -}); - -export const API_AUTHORIZE_REWARD: Query< - MerchantBackend.Rewards.RewardCreateRequest, - MerchantBackend.Rewards.RewardCreateConfirmation -> = { - method: "POST", - url: `http://backend/instances/default/private/rewards`, -}; - -export const API_DELETE_RESERVE = (id: string): Query<unknown, unknown> => ({ - method: "DELETE", - url: `http://backend/instances/default/private/reserves/${id}`, -}); - -//////////////////// -// INSTANCE ADMIN -//////////////////// - -export const API_CREATE_INSTANCE: Query< - MerchantBackend.Instances.InstanceConfigurationMessage, - unknown -> = { - method: "POST", - url: "http://backend/management/instances", -}; - -export const API_GET_INSTANCE_BY_ID = ( - id: string, -): Query<unknown, MerchantBackend.Instances.QueryInstancesResponse> => ({ - method: "GET", - url: `http://backend/management/instances/${id}`, -}); - -export const API_GET_INSTANCE_KYC_BY_ID = ( - id: string, -): Query<unknown, MerchantBackend.KYC.AccountKycRedirects> => ({ - method: "GET", - url: `http://backend/management/instances/${id}/kyc`, -}); - -export const API_LIST_INSTANCES: Query< - unknown, - MerchantBackend.Instances.InstancesResponse -> = { - method: "GET", - url: "http://backend/management/instances", -}; - -export const API_UPDATE_INSTANCE_BY_ID = ( - id: string, -): Query< - MerchantBackend.Instances.InstanceReconfigurationMessage, - unknown -> => ({ - method: "PATCH", - url: `http://backend/management/instances/${id}`, -}); - -export const API_UPDATE_INSTANCE_AUTH_BY_ID = ( - id: string, -): Query< - MerchantBackend.Instances.InstanceAuthConfigurationMessage, - unknown -> => ({ - method: "POST", - url: `http://backend/management/instances/${id}/auth`, -}); - -export const API_DELETE_INSTANCE = (id: string): Query<unknown, unknown> => ({ - method: "DELETE", - url: `http://backend/management/instances/${id}`, -}); - -//////////////////// -// AUTH -//////////////////// - -export const API_NEW_LOGIN: Query< - MerchantBackend.Instances.LoginTokenRequest, - unknown -> = ({ - method: "POST", - url: `http://backend/private/token`, -}); - -//////////////////// -// INSTANCE -//////////////////// - -export const API_GET_CURRENT_INSTANCE: Query< - unknown, - MerchantBackend.Instances.QueryInstancesResponse -> = { - method: "GET", - url: `http://backend/instances/default/private/`, -}; - -export const API_GET_CURRENT_INSTANCE_KYC: Query< - unknown, - MerchantBackend.KYC.AccountKycRedirects -> = { - method: "GET", - url: `http://backend/instances/default/private/kyc`, -}; - -export const API_UPDATE_CURRENT_INSTANCE: Query< - MerchantBackend.Instances.InstanceReconfigurationMessage, - unknown -> = { - method: "PATCH", - url: `http://backend/instances/default/private/`, -}; - -export const API_UPDATE_CURRENT_INSTANCE_AUTH: Query< - MerchantBackend.Instances.InstanceAuthConfigurationMessage, - unknown -> = { - method: "POST", - url: `http://backend/instances/default/private/auth`, -}; - -export const API_DELETE_CURRENT_INSTANCE: Query<unknown, unknown> = { - method: "DELETE", - url: `http://backend/instances/default/private`, -}; diff --git a/packages/auditor-backoffice-ui/src/hooks/webhooks.ts b/packages/auditor-backoffice-ui/src/hooks/webhooks.ts deleted file mode 100644 index ad6bf96e2..000000000 --- a/packages/auditor-backoffice-ui/src/hooks/webhooks.ts +++ /dev/null @@ -1,178 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ -import { - HttpResponse, - HttpResponseOk, - HttpResponsePaginated, - RequestError, -} from "@gnu-taler/web-util/browser"; -import { useEffect, useState } from "preact/hooks"; -import { MerchantBackend } from "../declaration.js"; -import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js"; -import { useBackendInstanceRequest, useMatchMutate } from "./backend.js"; - -// FIX default import https://github.com/microsoft/TypeScript/issues/49189 -import _useSWR, { SWRHook } from "swr"; -const useSWR = _useSWR as unknown as SWRHook; - -export function useWebhookAPI(): WebhookAPI { - const mutateAll = useMatchMutate(); - const { request } = useBackendInstanceRequest(); - - const createWebhook = async ( - data: MerchantBackend.Webhooks.WebhookAddDetails, - ): Promise<HttpResponseOk<void>> => { - const res = await request<void>(`/private/webhooks`, { - method: "POST", - data, - }); - await mutateAll(/.*private\/webhooks.*/); - return res; - }; - - const updateWebhook = async ( - webhookId: string, - data: MerchantBackend.Webhooks.WebhookPatchDetails, - ): Promise<HttpResponseOk<void>> => { - const res = await request<void>(`/private/webhooks/${webhookId}`, { - method: "PATCH", - data, - }); - await mutateAll(/.*private\/webhooks.*/); - return res; - }; - - const deleteWebhook = async ( - webhookId: string, - ): Promise<HttpResponseOk<void>> => { - const res = await request<void>(`/private/webhooks/${webhookId}`, { - method: "DELETE", - }); - await mutateAll(/.*private\/webhooks.*/); - return res; - }; - - return { createWebhook, updateWebhook, deleteWebhook }; -} - -export interface WebhookAPI { - createWebhook: ( - data: MerchantBackend.Webhooks.WebhookAddDetails, - ) => Promise<HttpResponseOk<void>>; - updateWebhook: ( - id: string, - data: MerchantBackend.Webhooks.WebhookPatchDetails, - ) => Promise<HttpResponseOk<void>>; - deleteWebhook: (id: string) => Promise<HttpResponseOk<void>>; -} - -export interface InstanceWebhookFilter { - //FIXME: add filter to the webhook list - position?: string; -} - -export function useInstanceWebhooks( - args?: InstanceWebhookFilter, - updatePosition?: (id: string) => void, -): HttpResponsePaginated< - MerchantBackend.Webhooks.WebhookSummaryResponse, - MerchantBackend.ErrorDetail -> { - const { webhookFetcher } = useBackendInstanceRequest(); - - const [pageBefore, setPageBefore] = useState(1); - const [pageAfter, setPageAfter] = useState(1); - - const totalAfter = pageAfter * PAGE_SIZE; - const totalBefore = args?.position ? pageBefore * PAGE_SIZE : 0; - - const { - data: afterData, - error: afterError, - isValidating: loadingAfter, - } = useSWR< - HttpResponseOk<MerchantBackend.Webhooks.WebhookSummaryResponse>, - RequestError<MerchantBackend.ErrorDetail> - >([`/private/webhooks`, args?.position, -totalAfter], webhookFetcher); - - const [lastAfter, setLastAfter] = useState< - HttpResponse< - MerchantBackend.Webhooks.WebhookSummaryResponse, - MerchantBackend.ErrorDetail - > - >({ loading: true }); - useEffect(() => { - if (afterData) setLastAfter(afterData); - }, [afterData]); - - if (afterError) return afterError.cause; - - const isReachingEnd = - afterData && afterData.data.webhooks.length < totalAfter; - const isReachingStart = true; - - const pagination = { - isReachingEnd, - isReachingStart, - loadMore: () => { - if (!afterData || isReachingEnd) return; - if (afterData.data.webhooks.length < MAX_RESULT_SIZE) { - setPageAfter(pageAfter + 1); - } else { - const from = `${ - afterData.data.webhooks[afterData.data.webhooks.length - 1].webhook_id - }`; - if (from && updatePosition) updatePosition(from); - } - }, - loadMorePrev: () => { - return; - }, - }; - - const webhooks = !afterData ? [] : (afterData || lastAfter).data.webhooks; - - if (loadingAfter) return { loading: true, data: { webhooks } }; - if (afterData) { - return { ok: true, data: { webhooks }, ...pagination }; - } - return { loading: true }; -} - -export function useWebhookDetails( - webhookId: string, -): HttpResponse< - MerchantBackend.Webhooks.WebhookDetails, - MerchantBackend.ErrorDetail -> { - const { webhookFetcher } = useBackendInstanceRequest(); - - const { data, error, isValidating } = useSWR< - HttpResponseOk<MerchantBackend.Webhooks.WebhookDetails>, - RequestError<MerchantBackend.ErrorDetail> - >([`/private/webhooks/${webhookId}`], webhookFetcher, { - refreshInterval: 0, - refreshWhenHidden: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshWhenOffline: false, - }); - - if (isValidating) return { loading: true, data: data?.data }; - if (data) return data; - if (error) return error.cause; - return { loading: true }; -} |