diff options
Diffstat (limited to 'packages/merchant-backoffice-ui/src/hooks')
16 files changed, 3050 insertions, 655 deletions
diff --git a/packages/merchant-backoffice-ui/src/hooks/async.ts b/packages/merchant-backoffice-ui/src/hooks/async.ts index 6c116e628..f22badc88 100644 --- a/packages/merchant-backoffice-ui/src/hooks/async.ts +++ b/packages/merchant-backoffice-ui/src/hooks/async.ts @@ -19,7 +19,6 @@   * @author Sebastian Javier Marchano (sebasjm)   */  import { useState } from "preact/hooks"; -import { cancelPendingRequest } from "./backend.js";  export interface Options {    slowTolerance: number; @@ -62,8 +61,7 @@ export function useAsync<T>(      clearTimeout(handler);    }; -  function cancel() { -    cancelPendingRequest(); +  function cancel(): void {      setLoading(false);      setSlow(false);    } diff --git a/packages/merchant-backoffice-ui/src/hooks/backend.ts b/packages/merchant-backoffice-ui/src/hooks/backend.ts index cbfac35de..a0639a4a0 100644 --- a/packages/merchant-backoffice-ui/src/hooks/backend.ts +++ b/packages/merchant-backoffice-ui/src/hooks/backend.ts @@ -20,15 +20,16 @@   */  import { useSWRConfig } from "swr"; -import axios, { AxiosError, AxiosResponse } from "axios";  import { MerchantBackend } from "../declaration.js";  import { useBackendContext } from "../context/backend.js"; -import { useEffect, useState } from "preact/hooks"; -import { DEFAULT_REQUEST_TIMEOUT } from "../utils/constants.js"; +import { useCallback, useEffect, useState } from "preact/hooks"; +import { useInstanceContext } from "../context/instance.js";  import { -  axiosHandler, -  removeAxiosCancelToken, -} from "../utils/switchableAxios.js"; +  HttpResponse, +  HttpResponseOk, +  RequestOptions, +} from "../utils/request.js"; +import { useApiContext } from "../context/api.js";  export function useMatchMutate(): (    re: RegExp, @@ -44,9 +45,7 @@ export function useMatchMutate(): (    return function matchRegexMutate(re: RegExp, value?: unknown) {      const allKeys = Array.from(cache.keys()); -    // console.log(allKeys)      const keys = allKeys.filter((key) => re.test(key)); -    // console.log(allKeys.length, keys.length)      const mutations = keys.map((key) => {        // console.log(key)        mutate(key, value, true); @@ -55,268 +54,234 @@ export function useMatchMutate(): (    };  } -export type HttpResponse<T> = -  | HttpResponseOk<T> -  | HttpResponseLoading<T> -  | HttpError; -export type HttpResponsePaginated<T> = -  | HttpResponseOkPaginated<T> -  | HttpResponseLoading<T> -  | HttpError; - -export interface RequestInfo { -  url: string; -  hasToken: boolean; -  params: unknown; -  data: unknown; -  status: number; -} - -interface HttpResponseLoading<T> { -  ok?: false; -  loading: true; -  clientError?: false; -  serverError?: false; - -  data?: T; -} -export interface HttpResponseOk<T> { -  ok: true; -  loading?: false; -  clientError?: false; -  serverError?: false; - -  data: T; -  info?: RequestInfo; -} - -export type HttpResponseOkPaginated<T> = HttpResponseOk<T> & WithPagination; - -export interface WithPagination { -  loadMore: () => void; -  loadMorePrev: () => void; -  isReachingEnd?: boolean; -  isReachingStart?: boolean; -} - -export type HttpError = -  | HttpResponseClientError -  | HttpResponseServerError -  | HttpResponseUnexpectedError; -export interface SwrError { -  info: unknown; -  status: number; -  message: string; -} -export interface HttpResponseServerError { -  ok?: false; -  loading?: false; -  clientError?: false; -  serverError: true; - -  error?: MerchantBackend.ErrorDetail; -  status: number; -  message: string; -  info?: RequestInfo; -} -interface HttpResponseClientError { -  ok?: false; -  loading?: false; -  clientError: true; -  serverError?: false; - -  info?: RequestInfo; -  isUnauthorized: boolean; -  isNotfound: boolean; -  status: number; -  error?: MerchantBackend.ErrorDetail; -  message: string; -} - -interface HttpResponseUnexpectedError { -  ok?: false; -  loading?: false; -  clientError?: false; -  serverError?: false; - -  info?: RequestInfo; -  status?: number; -  error: unknown; -  message: string; -} - -type Methods = "get" | "post" | "patch" | "delete" | "put"; - -interface RequestOptions { -  method?: Methods; -  token?: string; -  data?: unknown; -  params?: unknown; -} - -function buildRequestOk<T>( -  res: AxiosResponse<T>, -  url: string, -  hasToken: boolean, -): HttpResponseOk<T> { -  return { -    ok: true, -    data: res.data, -    info: { -      params: res.config.params, -      data: res.config.data, -      url, -      hasToken, -      status: res.status, -    }, -  }; -} - -// function buildResponse<T>(data?: T, error?: MerchantBackend.ErrorDetail, isValidating?: boolean): HttpResponse<T> { -//   if (isValidating) return {loading: true} -//   if (error) return buildRequestFailed() -// } - -function buildRequestFailed( -  ex: AxiosError<MerchantBackend.ErrorDetail>, -  url: string, -  hasToken: boolean, -): -  | HttpResponseClientError -  | HttpResponseServerError -  | HttpResponseUnexpectedError { -  const status = ex.response?.status; - -  const info: RequestInfo = { -    data: ex.request?.data, -    params: ex.request?.params, -    url, -    hasToken, -    status: status || 0, -  }; - -  if (status && status >= 400 && status < 500) { -    const error: HttpResponseClientError = { -      clientError: true, -      isNotfound: status === 404, -      isUnauthorized: status === 401, -      status, -      info, -      message: ex.response?.data?.hint || ex.message, -      error: ex.response?.data, -    }; -    return error; -  } -  if (status && status >= 500 && status < 600) { -    const error: HttpResponseServerError = { -      serverError: true, -      status, -      info, -      message: -        `${ex.response?.data?.hint} (code ${ex.response?.data?.code})` || -        ex.message, -      error: ex.response?.data, -    }; -    return error; -  } - -  const error: HttpResponseUnexpectedError = { -    info, -    status, -    error: ex, -    message: ex.message, -  }; - -  return error; -} - -const CancelToken = axios.CancelToken; -let source = CancelToken.source(); - -export function cancelPendingRequest(): void { -  source.cancel("canceled by the user"); -  source = CancelToken.source(); -} - -export function isAxiosError<T>( -  error: AxiosError | any, -): error is AxiosError<T> { -  return error && error.isAxiosError; -} - -export async function request<T>( -  url: string, -  options: RequestOptions = {}, -): Promise<HttpResponseOk<T>> { -  const headers = options.token -    ? { Authorization: `Bearer ${options.token}` } -    : undefined; - -  try { -    const res = await axiosHandler({ -      url, -      responseType: "json", -      headers, -      cancelToken: !removeAxiosCancelToken ? source.token : undefined, -      method: options.method || "get", -      data: options.data, -      params: options.params, -      timeout: DEFAULT_REQUEST_TIMEOUT * 1000, -    }); -    return buildRequestOk<T>(res, url, !!options.token); -  } catch (e) { -    if (isAxiosError<MerchantBackend.ErrorDetail>(e)) { -      const error = buildRequestFailed(e, url, !!options.token); -      throw error; -    } -    throw e; -  } -} - -export function multiFetcher<T>( -  urls: string[], -  token: string, -  backend: string, -): Promise<HttpResponseOk<T>[]> { -  return Promise.all(urls.map((url) => fetcher<T>(url, token, backend))); -} - -export function fetcher<T>( -  url: string, -  token: string, -  backend: string, -): Promise<HttpResponseOk<T>> { -  return request<T>(`${backend}${url}`, { token }); -} -  export function useBackendInstancesTestForAdmin(): HttpResponse<MerchantBackend.Instances.InstancesResponse> { -  const { url, token } = useBackendContext(); +  const { request } = useBackendBaseRequest();    type Type = MerchantBackend.Instances.InstancesResponse;    const [result, setResult] = useState<HttpResponse<Type>>({ loading: true });    useEffect(() => { -    request<Type>(`${url}/management/instances`, { token }) +    request<Type>(`/management/instances`)        .then((data) => setResult(data))        .catch((error) => setResult(error)); -  }, [url, token]); +  }, [request]);    return result;  }  export function useBackendConfig(): HttpResponse<MerchantBackend.VersionResponse> { -  const { url, token } = useBackendContext(); +  const { request } = useBackendBaseRequest();    type Type = MerchantBackend.VersionResponse;    const [result, setResult] = useState<HttpResponse<Type>>({ loading: true });    useEffect(() => { -    request<Type>(`${url}/config`, { token }) +    request<Type>(`/config`)        .then((data) => setResult(data))        .catch((error) => setResult(error)); -  }, [url, token]); +  }, [request]);    return result;  } + +interface useBackendInstanceRequestType { +  request: <T>( +    path: string, +    options?: RequestOptions, +  ) => Promise<HttpResponseOk<T>>; +  fetcher: <T>(path: string) => Promise<HttpResponseOk<T>>; +  reserveDetailFetcher: <T>(path: string) => Promise<HttpResponseOk<T>>; +  tipsDetailFetcher: <T>(path: string) => Promise<HttpResponseOk<T>>; +  multiFetcher: <T>(url: string[]) => Promise<HttpResponseOk<T>[]>; +  orderFetcher: <T>( +    path: string, +    paid?: YesOrNo, +    refunded?: YesOrNo, +    wired?: YesOrNo, +    searchDate?: Date, +    delta?: number, +  ) => Promise<HttpResponseOk<T>>; +  transferFetcher: <T>( +    path: string, +    payto_uri?: string, +    verified?: string, +    position?: string, +    delta?: number, +  ) => Promise<HttpResponseOk<T>>; +  templateFetcher: <T>( +    path: string, +    position?: string, +    delta?: number, +  ) => Promise<HttpResponseOk<T>>; +} +interface useBackendBaseRequestType { +  request: <T>( +    path: string, +    options?: RequestOptions, +  ) => Promise<HttpResponseOk<T>>; +} + +type YesOrNo = "yes" | "no"; + +/** + * + * @param root the request is intended to the base URL and no the instance URL + * @returns request handler to + */ +export function useBackendBaseRequest(): useBackendBaseRequestType { +  const { url: backend, token } = useBackendContext(); +  const { request: requestHandler } = useApiContext(); + +  const request = useCallback( +    function requestImpl<T>( +      path: string, +      options: RequestOptions = {}, +    ): Promise<HttpResponseOk<T>> { +      return requestHandler<T>(backend, path, { token, ...options }); +    }, +    [backend, token], +  ); + +  return { request }; +} + +export function useBackendInstanceRequest(): useBackendInstanceRequestType { +  const { url: baseUrl, token: baseToken } = useBackendContext(); +  const { token: instanceToken, id, admin } = useInstanceContext(); +  const { request: requestHandler } = useApiContext(); + +  const { backend, token } = !admin +    ? { backend: baseUrl, token: baseToken } +    : { backend: `${baseUrl}/instances/${id}`, token: instanceToken }; + +  const request = useCallback( +    function requestImpl<T>( +      path: string, +      options: RequestOptions = {}, +    ): Promise<HttpResponseOk<T>> { +      return requestHandler<T>(backend, path, { token, ...options }); +    }, +    [backend, token], +  ); + +  const multiFetcher = useCallback( +    function multiFetcherImpl<T>( +      paths: string[], +    ): Promise<HttpResponseOk<T>[]> { +      return Promise.all( +        paths.map((path) => requestHandler<T>(backend, path, { token })), +      ); +    }, +    [backend, token], +  ); + +  const fetcher = useCallback( +    function fetcherImpl<T>(path: string): Promise<HttpResponseOk<T>> { +      return requestHandler<T>(backend, path, { token }); +    }, +    [backend, token], +  ); + +  const orderFetcher = useCallback( +    function orderFetcherImpl<T>( +      path: string, +      paid?: YesOrNo, +      refunded?: YesOrNo, +      wired?: YesOrNo, +      searchDate?: Date, +      delta?: number, +    ): Promise<HttpResponseOk<T>> { +      const date_ms = +        delta && delta < 0 && searchDate +          ? searchDate.getTime() + 1 +          : searchDate?.getTime(); +      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_ms !== undefined) params.date_ms = date_ms; +      return requestHandler<T>(backend, path, { params, token }); +    }, +    [backend, token], +  ); + +  const reserveDetailFetcher = useCallback( +    function reserveDetailFetcherImpl<T>( +      path: string, +    ): Promise<HttpResponseOk<T>> { +      return requestHandler<T>(backend, path, { +        params: { +          tips: "yes", +        }, +        token, +      }); +    }, +    [backend, token], +  ); + +  const tipsDetailFetcher = useCallback( +    function tipsDetailFetcherImpl<T>( +      path: string, +    ): Promise<HttpResponseOk<T>> { +      return requestHandler<T>(backend, path, { +        params: { +          pickups: "yes", +        }, +        token, +      }); +    }, +    [backend, token], +  ); + +  const transferFetcher = useCallback( +    function transferFetcherImpl<T>( +      path: string, +      payto_uri?: string, +      verified?: string, +      position?: string, +      delta?: number, +    ): Promise<HttpResponseOk<T>> { +      const params: any = {}; +      if (payto_uri !== undefined) params.payto_uri = payto_uri; +      if (verified !== undefined) params.verified = verified; +      if (delta !== undefined) { +        params.limit = delta; +      } +      if (position !== undefined) params.offset = position; + +      return requestHandler<T>(backend, path, { params, token }); +    }, +    [backend, token], +  ); + +  const templateFetcher = useCallback( +    function templateFetcherImpl<T>( +      path: string, +      position?: string, +      delta?: number, +    ): Promise<HttpResponseOk<T>> { +      const params: any = {}; +      if (delta !== undefined) { +        params.limit = delta; +      } +      if (position !== undefined) params.offset = position; + +      return requestHandler<T>(backend, path, { params, token }); +    }, +    [backend, token], +  ); + +  return { +    request, +    fetcher, +    multiFetcher, +    orderFetcher, +    reserveDetailFetcher, +    tipsDetailFetcher, +    transferFetcher, +    templateFetcher, +  }; +} diff --git a/packages/merchant-backoffice-ui/src/hooks/index.ts b/packages/merchant-backoffice-ui/src/hooks/index.ts index 0581d9938..bb210c9ba 100644 --- a/packages/merchant-backoffice-ui/src/hooks/index.ts +++ b/packages/merchant-backoffice-ui/src/hooks/index.ts @@ -59,6 +59,7 @@ export function useBackendDefaultToken(  export function useBackendInstanceToken(    id: string,  ): [string | undefined, StateUpdater<string | undefined>] { +  const [random, setRandom] = useState(0);    const [token, setToken] = useLocalStorage(`backend-token-${id}`);    const [defaultToken, defaultSetToken] = useBackendDefaultToken(); @@ -66,8 +67,20 @@ export function useBackendInstanceToken(    if (id === "default") {      return [defaultToken, defaultSetToken];    } +  function updateToken( +    value: +      | (string | undefined) +      | ((s: string | undefined) => string | undefined), +  ): void { +    setToken((p) => { +      const toStore = value instanceof Function ? value(p) : value; +      // setToken(value) +      setRandom(new Date().getTime()); +      return toStore; +    }); +  } -  return [token, setToken]; +  return [token, updateToken];  }  export function useLang(initial?: string): [string, StateUpdater<string>] { diff --git a/packages/merchant-backoffice-ui/src/hooks/instance.test.ts b/packages/merchant-backoffice-ui/src/hooks/instance.test.ts new file mode 100644 index 000000000..c7aa63e20 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/hooks/instance.test.ts @@ -0,0 +1,660 @@ +/* + 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 { tests } from "@gnu-taler/web-util/lib/index.browser"; +import { expect } from "chai"; +import { 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_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', +              token: 'not-secret', +            } +          }); +          env.addRequestExpectation(API_UPDATE_CURRENT_INSTANCE_AUTH, { +            request: { +              method: 'token', +              token: 'secret' +            } as MerchantBackend.Instances.InstanceAuthConfigurationMessage, +          }); +          env.addRequestExpectation(API_GET_CURRENT_INSTANCE, { +            response: { +              name: 'instance_name', +              auth: { +                method: 'token', +                token: 'secret', +              } +            } as MerchantBackend.Instances.QueryInstancesResponse, +          }); +          api.setNewToken('secret') +        }, +        ({ 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.clearToken(); +        }, +        ({ 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/merchant-backoffice-ui/src/hooks/instance.ts b/packages/merchant-backoffice-ui/src/hooks/instance.ts index ab59487de..3c05472d0 100644 --- a/packages/merchant-backoffice-ui/src/hooks/instance.ts +++ b/packages/merchant-backoffice-ui/src/hooks/instance.ts @@ -15,14 +15,11 @@   */  import useSWR, { useSWRConfig } from "swr";  import { useBackendContext } from "../context/backend.js"; -import { useInstanceContext } from "../context/instance.js";  import { MerchantBackend } from "../declaration.js"; +import { HttpError, HttpResponse, HttpResponseOk } from "../utils/request.js";  import { -  fetcher, -  HttpError, -  HttpResponse, -  HttpResponseOk, -  request, +  useBackendBaseRequest, +  useBackendInstanceRequest,    useMatchMutate,  } from "./backend.js"; @@ -36,15 +33,14 @@ interface InstanceAPI {  }  export function useAdminAPI(): AdminAPI { -  const { url, token } = useBackendContext(); +  const { request } = useBackendBaseRequest();    const mutateAll = useMatchMutate();    const createInstance = async (      instance: MerchantBackend.Instances.InstanceConfigurationMessage,    ): Promise<void> => { -    await request(`${url}/management/instances`, { -      method: "post", -      token, +    await request(`/management/instances`, { +      method: "POST",        data: instance,      }); @@ -52,18 +48,16 @@ export function useAdminAPI(): AdminAPI {    };    const deleteInstance = async (id: string): Promise<void> => { -    await request(`${url}/management/instances/${id}`, { -      method: "delete", -      token, +    await request(`/management/instances/${id}`, { +      method: "DELETE",      });      mutateAll(/\/management\/instances/);    };    const purgeInstance = async (id: string): Promise<void> => { -    await request(`${url}/management/instances/${id}`, { -      method: "delete", -      token, +    await request(`/management/instances/${id}`, { +      method: "DELETE",        params: {          purge: "YES",        }, @@ -85,14 +79,14 @@ export interface AdminAPI {  export function useManagementAPI(instanceId: string): InstanceAPI {    const mutateAll = useMatchMutate(); -  const { url, token, updateLoginStatus } = useBackendContext(); +  const { updateToken } = useBackendContext(); +  const { request } = useBackendBaseRequest();    const updateInstance = async (      instance: MerchantBackend.Instances.InstanceReconfigurationMessage,    ): Promise<void> => { -    await request(`${url}/management/instances/${instanceId}`, { -      method: "patch", -      token, +    await request(`/management/instances/${instanceId}`, { +      method: "PATCH",        data: instance,      }); @@ -100,18 +94,16 @@ export function useManagementAPI(instanceId: string): InstanceAPI {    };    const deleteInstance = async (): Promise<void> => { -    await request(`${url}/management/instances/${instanceId}`, { -      method: "delete", -      token, +    await request(`/management/instances/${instanceId}`, { +      method: "DELETE",      });      mutateAll(/\/management\/instances/);    };    const clearToken = async (): Promise<void> => { -    await request(`${url}/management/instances/${instanceId}/auth`, { -      method: "post", -      token, +    await request(`/management/instances/${instanceId}/auth`, { +      method: "POST",        data: { method: "external" },      }); @@ -119,13 +111,12 @@ export function useManagementAPI(instanceId: string): InstanceAPI {    };    const setNewToken = async (newToken: string): Promise<void> => { -    await request(`${url}/management/instances/${instanceId}/auth`, { -      method: "post", -      token, +    await request(`/management/instances/${instanceId}/auth`, { +      method: "POST",        data: { method: "token", token: newToken },      }); -    updateLoginStatus(url, newToken); +    updateToken(newToken);      mutateAll(/\/management\/instances/);    }; @@ -139,71 +130,59 @@ export function useInstanceAPI(): InstanceAPI {      token: adminToken,      updateLoginStatus,    } = useBackendContext(); -  const { token: instanceToken, id, admin } = useInstanceContext(); - -  const { url, token } = !admin -    ? { url: baseUrl, token: adminToken } -    : { url: `${baseUrl}/instances/${id}`, token: instanceToken }; +  const { request } = useBackendInstanceRequest();    const updateInstance = async (      instance: MerchantBackend.Instances.InstanceReconfigurationMessage,    ): Promise<void> => { -    await request(`${url}/private/`, { -      method: "patch", -      token, +    await request(`/private/`, { +      method: "PATCH",        data: instance,      });      if (adminToken) mutate(["/private/instances", adminToken, baseUrl], null); -    mutate([`/private/`, token, url], null); +    mutate([`/private/`], null);    };    const deleteInstance = async (): Promise<void> => { -    await request(`${url}/private/`, { -      method: "delete", -      token: adminToken, +    await request(`/private/`, { +      method: "DELETE", +      // token: adminToken,      });      if (adminToken) mutate(["/private/instances", adminToken, baseUrl], null); -    mutate([`/private/`, token, url], null); +    mutate([`/private/`], null);    };    const clearToken = async (): Promise<void> => { -    await request(`${url}/private/auth`, { -      method: "post", -      token, +    await request(`/private/auth`, { +      method: "POST",        data: { method: "external" },      }); -    mutate([`/private/`, token, url], null); +    mutate([`/private/`], null);    };    const setNewToken = async (newToken: string): Promise<void> => { -    await request(`${url}/private/auth`, { -      method: "post", -      token, +    await request(`/private/auth`, { +      method: "POST",        data: { method: "token", token: newToken },      });      updateLoginStatus(baseUrl, newToken); -    mutate([`/private/`, token, url], null); +    mutate([`/private/`], null);    };    return { updateInstance, deleteInstance, setNewToken, clearToken };  }  export function useInstanceDetails(): HttpResponse<MerchantBackend.Instances.QueryInstancesResponse> { -  const { url: baseUrl, token: baseToken } = useBackendContext(); -  const { token: instanceToken, id, admin } = useInstanceContext(); - -  const { url, token } = !admin -    ? { url: baseUrl, token: baseToken } -    : { url: `${baseUrl}/instances/${id}`, token: instanceToken }; +  const { fetcher } = useBackendInstanceRequest();    const { data, error, isValidating } = useSWR<      HttpResponseOk<MerchantBackend.Instances.QueryInstancesResponse>,      HttpError -  >([`/private/`, token, url], fetcher, { +  >([`/private/`], fetcher, {      refreshInterval: 0,      refreshWhenHidden: false,      revalidateOnFocus: false, @@ -225,17 +204,12 @@ type KYCStatus =    | { type: "redirect"; status: MerchantBackend.Instances.AccountKycRedirects };  export function useInstanceKYCDetails(): HttpResponse<KYCStatus> { -  const { url: baseUrl, token: baseToken } = useBackendContext(); -  const { token: instanceToken, id, admin } = useInstanceContext(); - -  const { url, token } = !admin -    ? { url: baseUrl, token: baseToken } -    : { url: `${baseUrl}/instances/${id}`, token: instanceToken }; +  const { fetcher } = useBackendInstanceRequest();    const { data, error } = useSWR<      HttpResponseOk<MerchantBackend.Instances.AccountKycRedirects>,      HttpError -  >([`/private/kyc`, token, url], fetcher, { +  >([`/private/kyc`], fetcher, {      refreshInterval: 5000,      refreshWhenHidden: false,      revalidateOnFocus: false, @@ -258,12 +232,12 @@ export function useInstanceKYCDetails(): HttpResponse<KYCStatus> {  export function useManagedInstanceDetails(    instanceId: string,  ): HttpResponse<MerchantBackend.Instances.QueryInstancesResponse> { -  const { url, token } = useBackendContext(); +  const { request } = useBackendBaseRequest();    const { data, error, isValidating } = useSWR<      HttpResponseOk<MerchantBackend.Instances.QueryInstancesResponse>,      HttpError -  >([`/management/instances/${instanceId}`, token, url], fetcher, { +  >([`/management/instances/${instanceId}`], request, {      refreshInterval: 0,      refreshWhenHidden: false,      revalidateOnFocus: false, @@ -281,13 +255,12 @@ export function useManagedInstanceDetails(  }  export function useBackendInstances(): HttpResponse<MerchantBackend.Instances.InstancesResponse> { -  const { url } = useBackendContext(); -  const { token } = useInstanceContext(); +  const { request } = useBackendBaseRequest();    const { data, error, isValidating } = useSWR<      HttpResponseOk<MerchantBackend.Instances.InstancesResponse>,      HttpError -  >(["/management/instances", token, url], fetcher); +  >(["/management/instances"], request);    if (isValidating) return { loading: true, data: data?.data };    if (data) return data; diff --git a/packages/merchant-backoffice-ui/src/hooks/order.test.ts b/packages/merchant-backoffice-ui/src/hooks/order.test.ts new file mode 100644 index 000000000..be4d1d804 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/hooks/order.test.ts @@ -0,0 +1,579 @@ +/* + 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 { tests } from "@gnu-taler/web-util/lib/index.browser"; +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: 0, paid: "yes" }, +      response: { +        orders: [{ order_id: "1" } as MerchantBackend.Orders.OrderHistoryEntry], +      }, +    }); + +    env.addRequestExpectation(API_LIST_ORDERS, { +      qparam: { delta: -20, paid: "yes" }, +      response: { +        orders: [{ 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: 0, paid: "yes" }, +            response: { +              orders: [{ order_id: "1" } as any], +            }, +          }); + +          env.addRequestExpectation(API_LIST_ORDERS, { +            qparam: { delta: -20, paid: "yes" }, +            response: { +              orders: [{ 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: 0, paid: "yes" }, +      response: { +        orders: [{ order_id: "1", amount: 'EUR:12', refundable: true } as MerchantBackend.Orders.OrderHistoryEntry], +      }, +    }); + +    env.addRequestExpectation(API_LIST_ORDERS, { +      qparam: { delta: -20, paid: "yes" }, +      response: { orders: [], }, +    }); + + +    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: 0, paid: "yes" }, +            response: { +              orders: [{ order_id: "1", amount: 'EUR:12', refundable: false } as any], +            }, +          }); + +          env.addRequestExpectation(API_LIST_ORDERS, { +            qparam: { delta: -20, paid: "yes" }, +            response: { orders: [], }, +          }); + +          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: 0, paid: "yes" }, +      response: { +        orders: [{ order_id: "1" } as MerchantBackend.Orders.OrderHistoryEntry], +      }, +    }); + +    env.addRequestExpectation(API_LIST_ORDERS, { +      qparam: { delta: -20, paid: "yes" }, +      response: { +        orders: [{ 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: 0, paid: "yes" }, +            response: { +              orders: [], +            }, +          }); + +          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_ms: 12 }, +      response: { +        orders: [{ order_id: "1" } as any], +      }, +    }); + +    env.addRequestExpectation(API_LIST_ORDERS, { +      qparam: { delta: -20, wired: "yes", date_ms: 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(12); +        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_ms: 12 }, +      response: { +        orders: ordersFrom0to20, +      }, +    }); + +    env.addRequestExpectation(API_LIST_ORDERS, { +      qparam: { delta: -20, wired: "yes", date_ms: 13 }, +      response: { +        orders: ordersFrom20to40, +      }, +    }); + +    const newDate = (d: Date) => { +      //console.log("new date", d); +    }; + +    const hookBehavior = await tests.hookBehaveLikeThis( +      () => { +        const date = new Date(12); +        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_ms: 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_ms: 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/merchant-backoffice-ui/src/hooks/order.ts b/packages/merchant-backoffice-ui/src/hooks/order.ts index d1e26b671..0bea6b963 100644 --- a/packages/merchant-backoffice-ui/src/hooks/order.ts +++ b/packages/merchant-backoffice-ui/src/hooks/order.ts @@ -14,20 +14,16 @@   GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>   */  import { useEffect, useState } from "preact/hooks"; -import useSWR, { useSWRConfig } from "swr"; -import { useBackendContext } from "../context/backend.js"; -import { useInstanceContext } from "../context/instance.js"; +import useSWR from "swr";  import { MerchantBackend } from "../declaration.js";  import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js";  import { -  fetcher,    HttpError,    HttpResponse,    HttpResponseOk,    HttpResponsePaginated, -  request, -  useMatchMutate, -} from "./backend.js"; +} from "../utils/request.js"; +import { useBackendInstanceRequest, useMatchMutate } from "./backend.js";  export interface OrderAPI {    //FIXME: add OutOfStockResponse on 410 @@ -48,52 +44,17 @@ export interface OrderAPI {  type YesOrNo = "yes" | "no"; -export function orderFetcher<T>( -  url: string, -  token: string, -  backend: string, -  paid?: YesOrNo, -  refunded?: YesOrNo, -  wired?: YesOrNo, -  searchDate?: Date, -  delta?: number, -): Promise<HttpResponseOk<T>> { -  const date_ms = -    delta && delta < 0 && searchDate -      ? searchDate.getTime() + 1 -      : searchDate?.getTime(); -  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_ms !== undefined) params.date_ms = date_ms; -  return request<T>(`${backend}${url}`, { token, params }); -} -  export function useOrderAPI(): OrderAPI {    const mutateAll = useMatchMutate(); -  const { url: baseUrl, token: adminToken } = useBackendContext(); -  const { token: instanceToken, id, admin } = useInstanceContext(); - -  const { url, token } = !admin -    ? { -        url: baseUrl, -        token: adminToken, -      } -    : { -        url: `${baseUrl}/instances/${id}`, -        token: instanceToken, -      }; +  const { request } = useBackendInstanceRequest();    const createOrder = async (      data: MerchantBackend.Orders.PostOrderRequest,    ): Promise<HttpResponseOk<MerchantBackend.Orders.PostOrderResponse>> => {      const res = await request<MerchantBackend.Orders.PostOrderResponse>( -      `${url}/private/orders`, +      `/private/orders`,        { -        method: "post", -        token, +        method: "POST",          data,        },      ); @@ -107,10 +68,9 @@ export function useOrderAPI(): OrderAPI {    ): Promise<HttpResponseOk<MerchantBackend.Orders.MerchantRefundResponse>> => {      mutateAll(/@"\/private\/orders"@/);      const res = request<MerchantBackend.Orders.MerchantRefundResponse>( -      `${url}/private/orders/${orderId}/refund`, +      `/private/orders/${orderId}/refund`,        { -        method: "post", -        token, +        method: "POST",          data,        },      ); @@ -125,9 +85,8 @@ export function useOrderAPI(): OrderAPI {      data: MerchantBackend.Orders.ForgetRequest,    ): Promise<HttpResponseOk<void>> => {      mutateAll(/@"\/private\/orders"@/); -    const res = request<void>(`${url}/private/orders/${orderId}/forget`, { -      method: "patch", -      token, +    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 @@ -138,9 +97,8 @@ export function useOrderAPI(): OrderAPI {      orderId: string,    ): Promise<HttpResponseOk<void>> => {      mutateAll(/@"\/private\/orders"@/); -    const res = request<void>(`${url}/private/orders/${orderId}`, { -      method: "delete", -      token, +    const res = request<void>(`/private/orders/${orderId}`, { +      method: "DELETE",      });      await mutateAll(/.*private\/orders.*/);      return res; @@ -150,10 +108,9 @@ export function useOrderAPI(): OrderAPI {      orderId: string,    ): Promise<HttpResponseOk<string>> => {      return request<MerchantBackend.Orders.MerchantOrderStatusResponse>( -      `${url}/private/orders/${orderId}`, +      `/private/orders/${orderId}`,        { -        method: "get", -        token, +        method: "GET",        },      ).then((res) => {        const url = @@ -172,17 +129,12 @@ export function useOrderAPI(): OrderAPI {  export function useOrderDetails(    oderId: string,  ): HttpResponse<MerchantBackend.Orders.MerchantOrderStatusResponse> { -  const { url: baseUrl, token: baseToken } = useBackendContext(); -  const { token: instanceToken, id, admin } = useInstanceContext(); - -  const { url, token } = !admin -    ? { url: baseUrl, token: baseToken } -    : { url: `${baseUrl}/instances/${id}`, token: instanceToken }; +  const { fetcher } = useBackendInstanceRequest();    const { data, error, isValidating } = useSWR<      HttpResponseOk<MerchantBackend.Orders.MerchantOrderStatusResponse>,      HttpError -  >([`/private/orders/${oderId}`, token, url], fetcher, { +  >([`/private/orders/${oderId}`], fetcher, {      refreshInterval: 0,      refreshWhenHidden: false,      revalidateOnFocus: false, @@ -207,12 +159,7 @@ export function useInstanceOrders(    args?: InstanceOrderFilter,    updateFilter?: (d: Date) => void,  ): HttpResponsePaginated<MerchantBackend.Orders.OrderHistory> { -  const { url: baseUrl, token: baseToken } = useBackendContext(); -  const { token: instanceToken, id, admin } = useInstanceContext(); - -  const { url, token } = !admin -    ? { url: baseUrl, token: baseToken } -    : { url: `${baseUrl}/instances/${id}`, token: instanceToken }; +  const { orderFetcher } = useBackendInstanceRequest();    const [pageBefore, setPageBefore] = useState(1);    const [pageAfter, setPageAfter] = useState(1); @@ -233,8 +180,6 @@ export function useInstanceOrders(    } = useSWR<HttpResponseOk<MerchantBackend.Orders.OrderHistory>, HttpError>(      [        `/private/orders`, -      token, -      url,        args?.paid,        args?.refunded,        args?.wired, @@ -250,8 +195,6 @@ export function useInstanceOrders(    } = useSWR<HttpResponseOk<MerchantBackend.Orders.OrderHistory>, HttpError>(      [        `/private/orders`, -      token, -      url,        args?.paid,        args?.refunded,        args?.wired, @@ -314,9 +257,9 @@ export function useInstanceOrders(      !beforeData || !afterData        ? []        : (beforeData || lastBefore).data.orders -          .slice() -          .reverse() -          .concat((afterData || lastAfter).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 }; diff --git a/packages/merchant-backoffice-ui/src/hooks/product.test.ts b/packages/merchant-backoffice-ui/src/hooks/product.test.ts new file mode 100644 index 000000000..a182b28f4 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/hooks/product.test.ts @@ -0,0 +1,326 @@ +/* + 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 { tests } from "@gnu-taler/web-util/lib/index.browser"; +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" }); + +  }) +})
\ No newline at end of file diff --git a/packages/merchant-backoffice-ui/src/hooks/product.ts b/packages/merchant-backoffice-ui/src/hooks/product.ts index fb7889834..af8ad74f3 100644 --- a/packages/merchant-backoffice-ui/src/hooks/product.ts +++ b/packages/merchant-backoffice-ui/src/hooks/product.ts @@ -14,18 +14,9 @@   GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>   */  import useSWR, { useSWRConfig } from "swr"; -import { useBackendContext } from "../context/backend.js"; -import { useInstanceContext } from "../context/instance.js";  import { MerchantBackend, WithId } from "../declaration.js"; -import { -  fetcher, -  HttpError, -  HttpResponse, -  HttpResponseOk, -  multiFetcher, -  request, -  useMatchMutate, -} from "./backend.js"; +import { HttpError, HttpResponse, HttpResponseOk } from "../utils/request.js"; +import { useBackendInstanceRequest, useMatchMutate } from "./backend.js";  export interface ProductAPI {    createProduct: ( @@ -45,19 +36,14 @@ export interface ProductAPI {  export function useProductAPI(): ProductAPI {    const mutateAll = useMatchMutate();    const { mutate } = useSWRConfig(); -  const { url: baseUrl, token: adminToken } = useBackendContext(); -  const { token: instanceToken, id, admin } = useInstanceContext(); -  const { url, token } = !admin -    ? { url: baseUrl, token: adminToken } -    : { url: `${baseUrl}/instances/${id}`, token: instanceToken }; +  const { request } = useBackendInstanceRequest();    const createProduct = async (      data: MerchantBackend.Products.ProductAddDetail,    ): Promise<void> => { -    const res = await request(`${url}/private/products`, { -      method: "post", -      token, +    const res = await request(`/private/products`, { +      method: "POST",        data,      }); @@ -68,9 +54,8 @@ export function useProductAPI(): ProductAPI {      productId: string,      data: MerchantBackend.Products.ProductPatchDetail,    ): Promise<void> => { -    const r = await request(`${url}/private/products/${productId}`, { -      method: "patch", -      token, +    const r = await request(`/private/products/${productId}`, { +      method: "PATCH",        data,      }); @@ -78,20 +63,18 @@ export function useProductAPI(): ProductAPI {    };    const deleteProduct = async (productId: string): Promise<void> => { -    await request(`${url}/private/products/${productId}`, { -      method: "delete", -      token, +    await request(`/private/products/${productId}`, { +      method: "DELETE",      }); -    await mutate([`/private/products`, token, url]); +    await mutate([`/private/products`]);    };    const lockProduct = async (      productId: string,      data: MerchantBackend.Products.LockRequest,    ): Promise<void> => { -    await request(`${url}/private/products/${productId}/lock`, { -      method: "post", -      token, +    await request(`/private/products/${productId}/lock`, { +      method: "POST",        data,      }); @@ -104,17 +87,12 @@ export function useProductAPI(): ProductAPI {  export function useInstanceProducts(): HttpResponse<    (MerchantBackend.Products.ProductDetail & WithId)[]  > { -  const { url: baseUrl, token: baseToken } = useBackendContext(); -  const { token: instanceToken, id, admin } = useInstanceContext(); - -  const { url, token } = !admin -    ? { url: baseUrl, token: baseToken } -    : { url: `${baseUrl}/instances/${id}`, token: instanceToken }; +  const { fetcher, multiFetcher } = useBackendInstanceRequest();    const { data: list, error: listError } = useSWR<      HttpResponseOk<MerchantBackend.Products.InventorySummaryResponse>,      HttpError -  >([`/private/products`, token, url], fetcher, { +  >([`/private/products`], fetcher, {      refreshInterval: 0,      refreshWhenHidden: false,      revalidateOnFocus: false, @@ -128,7 +106,7 @@ export function useInstanceProducts(): HttpResponse<    const { data: products, error: productError } = useSWR<      HttpResponseOk<MerchantBackend.Products.ProductDetail>[],      HttpError -  >([paths, token, url], multiFetcher, { +  >([paths], multiFetcher, {      refreshInterval: 0,      refreshWhenHidden: false,      revalidateOnFocus: false, @@ -144,7 +122,7 @@ export function useInstanceProducts(): HttpResponse<        //take the id from the queried url        return {          ...d.data, -        id: d.info?.url.replace(/.*\/private\/products\//, "") || "", +        id: d.info?.url.href.replace(/.*\/private\/products\//, "") || "",        };      });      return { ok: true, data: dataWithId }; @@ -155,23 +133,12 @@ export function useInstanceProducts(): HttpResponse<  export function useProductDetails(    productId: string,  ): HttpResponse<MerchantBackend.Products.ProductDetail> { -  const { url: baseUrl, token: baseToken } = useBackendContext(); -  const { token: instanceToken, id, admin } = useInstanceContext(); - -  const { url, token } = !admin -    ? { -        url: baseUrl, -        token: baseToken, -      } -    : { -        url: `${baseUrl}/instances/${id}`, -        token: instanceToken, -      }; +  const { fetcher } = useBackendInstanceRequest();    const { data, error, isValidating } = useSWR<      HttpResponseOk<MerchantBackend.Products.ProductDetail>,      HttpError -  >([`/private/products/${productId}`, token, url], fetcher, { +  >([`/private/products/${productId}`], fetcher, {      refreshInterval: 0,      refreshWhenHidden: false,      revalidateOnFocus: false, diff --git a/packages/merchant-backoffice-ui/src/hooks/reserve.test.ts b/packages/merchant-backoffice-ui/src/hooks/reserve.test.ts new file mode 100644 index 000000000..da0e054e5 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/hooks/reserve.test.ts @@ -0,0 +1,431 @@ +/* + 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, +  useTipDetails +} from "./reserves.js"; +import { ApiMockEnvironment } from "./testing.js"; +import { +  API_AUTHORIZE_TIP, +  API_AUTHORIZE_TIP_FOR_RESERVE, +  API_CREATE_RESERVE, +  API_DELETE_RESERVE, +  API_GET_RESERVE_BY_ID, +  API_GET_TIP_BY_ID, +  API_LIST_RESERVES +} from "./urls.js"; +import { tests } from "@gnu-taler/web-util/lib/index.browser"; + +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.Tips.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", +              payto_uri: "payto", +            }, +          }); + +          env.addRequestExpectation(API_LIST_RESERVES, { +            response: { +              reserves: [ +                { +                  reserve_pub: "11", +                } as MerchantBackend.Tips.ReserveStatusEntry, +                { +                  reserve_pub: "22", +                } as MerchantBackend.Tips.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.Tips.ReserveStatusEntry, +              { +                reserve_pub: "22", +              } as MerchantBackend.Tips.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.Tips.ReserveStatusEntry, +          { +            reserve_pub: "22", +          } as MerchantBackend.Tips.ReserveStatusEntry, +          { +            reserve_pub: "33", +          } as MerchantBackend.Tips.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.Tips.ReserveStatusEntry, +                { +                  reserve_pub: "33", +                } as MerchantBackend.Tips.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 tip for a specific reserve", async () => { +    const env = new ApiMockEnvironment(); + +    env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), { +      response: { +        payto_uri: "payto://here", +        tips: [{ reason: "why?", tip_id: "id1", total_amount: "USD:10" }], +      } as MerchantBackend.Tips.ReserveDetail, +      qparam: { +        tips: "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({ +            payto_uri: "payto://here", +            tips: [{ reason: "why?", tip_id: "id1", total_amount: "USD:10" }], +          }); + +          env.addRequestExpectation(API_AUTHORIZE_TIP_FOR_RESERVE("11"), { +            request: { +              amount: "USD:12", +              justification: "not", +              next_url: "http://taler.net", +            }, +            response: { +              tip_id: "id2", +              taler_tip_uri: "uri", +              tip_expiration: { t_s: 1 }, +              tip_status_url: "url", +            } +          },); + +          env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), { +            response: { +              payto_uri: "payto://here", +              tips: [ +                { reason: "why?", tip_id: "id1", total_amount: "USD:10" }, +                { reason: "not", tip_id: "id2", total_amount: "USD:12" }, +              ], +            } as MerchantBackend.Tips.ReserveDetail, +            qparam: { +              tips: "yes" +            } +          }); + +          api.authorizeTipReserve("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({ +            payto_uri: "payto://here", +            tips: [ +              { reason: "why?", tip_id: "id1", total_amount: "USD:10" }, +              { reason: "not", tip_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 tip for a random reserve", async () => { +    const env = new ApiMockEnvironment(); + +    env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), { +      response: { +        payto_uri: "payto://here", +        tips: [{ reason: "why?", tip_id: "id1", total_amount: "USD:10" }], +      } as MerchantBackend.Tips.ReserveDetail, +      qparam: { +        tips: "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({ +            payto_uri: "payto://here", +            tips: [{ reason: "why?", tip_id: "id1", total_amount: "USD:10" }], +          }); + +          env.addRequestExpectation(API_AUTHORIZE_TIP, { +            request: { +              amount: "USD:12", +              justification: "not", +              next_url: "http://taler.net", +            }, +            response: { +              tip_id: "id2", +              taler_tip_uri: "uri", +              tip_expiration: { t_s: 1 }, +              tip_status_url: "url", +            }, +          }); + +          env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), { +            response: { +              payto_uri: "payto://here", +              tips: [ +                { reason: "why?", tip_id: "id1", total_amount: "USD:10" }, +                { reason: "not", tip_id: "id2", total_amount: "USD:12" }, +              ], +            } as MerchantBackend.Tips.ReserveDetail, +            qparam: { +              tips: "yes" +            } +          }); + +          api.authorizeTip({ +            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({ +            payto_uri: "payto://here", +            tips: [ +              { reason: "why?", tip_id: "id1", total_amount: "USD:10" }, +              { reason: "not", tip_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 tip details", () => { + +  it("should list tips", async () => { +    const env = new ApiMockEnvironment(); + +    env.addRequestExpectation(API_GET_TIP_BY_ID("11"), { +      response: { +        total_picked_up: "USD:12", +        reason: "not", +      } as MerchantBackend.Tips.TipDetails, +      qparam: { +        pickups: "yes" +      } +    }); + +    const hookBehavior = await tests.hookBehaveLikeThis( +      () => { +        const query = useTipDetails("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/merchant-backoffice-ui/src/hooks/reserves.ts b/packages/merchant-backoffice-ui/src/hooks/reserves.ts index f6d77f113..dc127af13 100644 --- a/packages/merchant-backoffice-ui/src/hooks/reserves.ts +++ b/packages/merchant-backoffice-ui/src/hooks/reserves.ts @@ -14,27 +14,14 @@   GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>   */  import useSWR, { useSWRConfig } from "swr"; -import { useBackendContext } from "../context/backend.js"; -import { useInstanceContext } from "../context/instance.js";  import { MerchantBackend } from "../declaration.js"; -import { -  fetcher, -  HttpError, -  HttpResponse, -  HttpResponseOk, -  request, -  useMatchMutate, -} from "./backend.js"; +import { HttpError, HttpResponse, HttpResponseOk } from "../utils/request.js"; +import { useBackendInstanceRequest, useMatchMutate } from "./backend.js";  export function useReservesAPI(): ReserveMutateAPI {    const mutateAll = useMatchMutate();    const { mutate } = useSWRConfig(); -  const { url: baseUrl, token: adminToken } = useBackendContext(); -  const { token: instanceToken, id, admin } = useInstanceContext(); - -  const { url, token } = !admin -    ? { url: baseUrl, token: adminToken } -    : { url: `${baseUrl}/instances/${id}`, token: instanceToken }; +  const { request } = useBackendInstanceRequest();    const createReserve = async (      data: MerchantBackend.Tips.ReserveCreateRequest, @@ -42,10 +29,9 @@ export function useReservesAPI(): ReserveMutateAPI {      HttpResponseOk<MerchantBackend.Tips.ReserveCreateConfirmation>    > => {      const res = await request<MerchantBackend.Tips.ReserveCreateConfirmation>( -      `${url}/private/reserves`, +      `/private/reserves`,        { -        method: "post", -        token, +        method: "POST",          data,        },      ); @@ -61,16 +47,15 @@ export function useReservesAPI(): ReserveMutateAPI {      data: MerchantBackend.Tips.TipCreateRequest,    ): Promise<HttpResponseOk<MerchantBackend.Tips.TipCreateConfirmation>> => {      const res = await request<MerchantBackend.Tips.TipCreateConfirmation>( -      `${url}/private/reserves/${pub}/authorize-tip`, +      `/private/reserves/${pub}/authorize-tip`,        { -        method: "post", -        token, +        method: "POST",          data,        },      );      //evict reserve details query -    await mutate([`/private/reserves/${pub}`, token, url]); +    await mutate([`/private/reserves/${pub}`]);      return res;    }; @@ -79,10 +64,9 @@ export function useReservesAPI(): ReserveMutateAPI {      data: MerchantBackend.Tips.TipCreateRequest,    ): Promise<HttpResponseOk<MerchantBackend.Tips.TipCreateConfirmation>> => {      const res = await request<MerchantBackend.Tips.TipCreateConfirmation>( -      `${url}/private/tips`, +      `/private/tips`,        { -        method: "post", -        token, +        method: "POST",          data,        },      ); @@ -94,9 +78,8 @@ export function useReservesAPI(): ReserveMutateAPI {    };    const deleteReserve = async (pub: string): Promise<HttpResponse<void>> => { -    const res = await request<void>(`${url}/private/reserves/${pub}`, { -      method: "delete", -      token, +    const res = await request<void>(`/private/reserves/${pub}`, { +      method: "DELETE",      });      //evict reserve list query @@ -123,17 +106,12 @@ export interface ReserveMutateAPI {  }  export function useInstanceReserves(): HttpResponse<MerchantBackend.Tips.TippingReserveStatus> { -  const { url: baseUrl, token: baseToken } = useBackendContext(); -  const { token: instanceToken, id, admin } = useInstanceContext(); - -  const { url, token } = !admin -    ? { url: baseUrl, token: baseToken } -    : { url: `${baseUrl}/instances/${id}`, token: instanceToken }; +  const { fetcher } = useBackendInstanceRequest();    const { data, error, isValidating } = useSWR<      HttpResponseOk<MerchantBackend.Tips.TippingReserveStatus>,      HttpError -  >([`/private/reserves`, token, url], fetcher); +  >([`/private/reserves`], fetcher);    if (isValidating) return { loading: true, data: data?.data };    if (data) return data; @@ -144,15 +122,12 @@ export function useInstanceReserves(): HttpResponse<MerchantBackend.Tips.Tipping  export function useReserveDetails(    reserveId: string,  ): HttpResponse<MerchantBackend.Tips.ReserveDetail> { -  const { url: baseUrl } = useBackendContext(); -  const { token, id: instanceId, admin } = useInstanceContext(); - -  const url = !admin ? baseUrl : `${baseUrl}/instances/${instanceId}`; +  const { reserveDetailFetcher } = useBackendInstanceRequest();    const { data, error, isValidating } = useSWR<      HttpResponseOk<MerchantBackend.Tips.ReserveDetail>,      HttpError -  >([`/private/reserves/${reserveId}`, token, url], reserveDetailFetcher, { +  >([`/private/reserves/${reserveId}`], reserveDetailFetcher, {      refreshInterval: 0,      refreshWhenHidden: false,      revalidateOnFocus: false, @@ -169,15 +144,12 @@ export function useReserveDetails(  export function useTipDetails(    tipId: string,  ): HttpResponse<MerchantBackend.Tips.TipDetails> { -  const { url: baseUrl } = useBackendContext(); -  const { token, id: instanceId, admin } = useInstanceContext(); - -  const url = !admin ? baseUrl : `${baseUrl}/instances/${instanceId}`; +  const { tipsDetailFetcher } = useBackendInstanceRequest();    const { data, error, isValidating } = useSWR<      HttpResponseOk<MerchantBackend.Tips.TipDetails>,      HttpError -  >([`/private/tips/${tipId}`, token, url], tipsDetailFetcher, { +  >([`/private/tips/${tipId}`], tipsDetailFetcher, {      refreshInterval: 0,      refreshWhenHidden: false,      revalidateOnFocus: false, @@ -190,29 +162,3 @@ export function useTipDetails(    if (error) return error;    return { loading: true };  } - -function reserveDetailFetcher<T>( -  url: string, -  token: string, -  backend: string, -): Promise<HttpResponseOk<T>> { -  return request<T>(`${backend}${url}`, { -    token, -    params: { -      tips: "yes", -    }, -  }); -} - -function tipsDetailFetcher<T>( -  url: string, -  token: string, -  backend: string, -): Promise<HttpResponseOk<T>> { -  return request<T>(`${backend}${url}`, { -    token, -    params: { -      pickups: "yes", -    }, -  }); -} diff --git a/packages/merchant-backoffice-ui/src/hooks/templates.ts b/packages/merchant-backoffice-ui/src/hooks/templates.ts index 3e69d78d0..55c3875b5 100644 --- a/packages/merchant-backoffice-ui/src/hooks/templates.ts +++ b/packages/merchant-backoffice-ui/src/hooks/templates.ts @@ -14,57 +14,26 @@   GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>   */  import { MerchantBackend } from "../declaration.js"; -import { useBackendContext } from "../context/backend.js"; +import { useMatchMutate, useBackendInstanceRequest } from "./backend.js"; +import useSWR from "swr"; +import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js"; +import { useEffect, useState } from "preact/hooks";  import { -  request, -  HttpResponse,    HttpError, +  HttpResponse,    HttpResponseOk,    HttpResponsePaginated, -  useMatchMutate, -} from "./backend.js"; -import useSWR from "swr"; -import { useInstanceContext } from "../context/instance.js"; -import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js"; -import { useEffect, useState } from "preact/hooks"; - -async function templateFetcher<T>( -  url: string, -  token: string, -  backend: string, -  position?: string, -  delta?: number, -): Promise<HttpResponseOk<T>> { -  const params: any = {}; -  if (delta !== undefined) { -    params.limit = delta; -  } -  if (position !== undefined) params.offset = position; - -  return request<T>(`${backend}${url}`, { token, params }); -} +} from "../utils/request.js";  export function useTemplateAPI(): TemplateAPI {    const mutateAll = useMatchMutate(); -  const { url: baseUrl, token: adminToken } = useBackendContext(); -  const { token: instanceToken, id, admin } = useInstanceContext(); - -  const { url, token } = !admin -    ? { -        url: baseUrl, -        token: adminToken, -      } -    : { -        url: `${baseUrl}/instances/${id}`, -        token: instanceToken, -      }; +  const { request } = useBackendInstanceRequest();    const createTemplate = async (      data: MerchantBackend.Template.TemplateAddDetails,    ): Promise<HttpResponseOk<void>> => { -    const res = await request<void>(`${url}/private/templates`, { -      method: "post", -      token, +    const res = await request<void>(`/private/templates`, { +      method: "POST",        data,      });      await mutateAll(/.*private\/templates.*/); @@ -75,9 +44,8 @@ export function useTemplateAPI(): TemplateAPI {      templateId: string,      data: MerchantBackend.Template.TemplatePatchDetails,    ): Promise<HttpResponseOk<void>> => { -    const res = await request<void>(`${url}/private/templates/${templateId}`, { -      method: "patch", -      token, +    const res = await request<void>(`/private/templates/${templateId}`, { +      method: "PATCH",        data,      });      await mutateAll(/.*private\/templates.*/); @@ -87,9 +55,8 @@ export function useTemplateAPI(): TemplateAPI {    const deleteTemplate = async (      templateId: string,    ): Promise<HttpResponseOk<void>> => { -    const res = await request<void>(`${url}/private/templates/${templateId}`, { -      method: "delete", -      token, +    const res = await request<void>(`/private/templates/${templateId}`, { +      method: "DELETE",      });      await mutateAll(/.*private\/templates.*/);      return res; @@ -102,10 +69,9 @@ export function useTemplateAPI(): TemplateAPI {      HttpResponseOk<MerchantBackend.Template.UsingTemplateResponse>    > => {      const res = await request<MerchantBackend.Template.UsingTemplateResponse>( -      `${url}/private/templates/${templateId}`, +      `/private/templates/${templateId}`,        { -        method: "post", -        token, +        method: "POST",          data,        },      ); @@ -140,12 +106,7 @@ export function useInstanceTemplates(    args?: InstanceTemplateFilter,    updatePosition?: (id: string) => void,  ): HttpResponsePaginated<MerchantBackend.Template.TemplateSummaryResponse> { -  const { url: baseUrl, token: baseToken } = useBackendContext(); -  const { token: instanceToken, id, admin } = useInstanceContext(); - -  const { url, token } = !admin -    ? { url: baseUrl, token: baseToken } -    : { url: `${baseUrl}/instances/${id}`, token: instanceToken }; +  const { templateFetcher } = useBackendInstanceRequest();    // const [pageBefore, setPageBefore] = useState(1);    const [pageAfter, setPageAfter] = useState(1); @@ -180,10 +141,7 @@ export function useInstanceTemplates(    } = useSWR<      HttpResponseOk<MerchantBackend.Template.TemplateSummaryResponse>,      HttpError -  >( -    [`/private/templates`, token, url, args?.position, -totalAfter], -    templateFetcher, -  ); +  >([`/private/templates`, args?.position, -totalAfter], templateFetcher);    //this will save last result    // const [lastBefore, setLastBefore] = useState< @@ -216,10 +174,9 @@ export function useInstanceTemplates(        if (afterData.data.templates.length < MAX_RESULT_SIZE) {          setPageAfter(pageAfter + 1);        } else { -        const from = `${ -          afterData.data.templates[afterData.data.templates.length - 1] -            .template_id -        }`; +        const from = `${afterData.data.templates[afterData.data.templates.length - 1] +          .template_id +          }`;          if (from && updatePosition) updatePosition(from);        }      }, @@ -255,17 +212,12 @@ export function useInstanceTemplates(  export function useTemplateDetails(    templateId: string,  ): HttpResponse<MerchantBackend.Template.TemplateDetails> { -  const { url: baseUrl, token: baseToken } = useBackendContext(); -  const { token: instanceToken, id, admin } = useInstanceContext(); - -  const { url, token } = !admin -    ? { url: baseUrl, token: baseToken } -    : { url: `${baseUrl}/instances/${id}`, token: instanceToken }; +  const { templateFetcher } = useBackendInstanceRequest();    const { data, error, isValidating } = useSWR<      HttpResponseOk<MerchantBackend.Template.TemplateDetails>,      HttpError -  >([`/private/templates/${templateId}`, token, url], templateFetcher, { +  >([`/private/templates/${templateId}`], templateFetcher, {      refreshInterval: 0,      refreshWhenHidden: false,      revalidateOnFocus: false, diff --git a/packages/merchant-backoffice-ui/src/hooks/testing.tsx b/packages/merchant-backoffice-ui/src/hooks/testing.tsx new file mode 100644 index 000000000..8c5a5a36b --- /dev/null +++ b/packages/merchant-backoffice-ui/src/hooks/testing.tsx @@ -0,0 +1,120 @@ +/* + 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/lib/tests/mock"; +import { ComponentChildren, FunctionalComponent, h, VNode } from "preact"; +import { SWRConfig } from "swr"; +import { ApiContextProvider } from "../context/api.js"; +import { BackendContextProvider } from "../context/backend.js"; +import { InstanceContextProvider } from "../context/instance.js"; +import { HttpResponseOk, RequestOptions } from "../utils/request.js"; + +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, +          }, +        ); + +        return { +          ok: true, +          data: (!mocked ? undefined : mocked.payload) as T, +          loading: false, +          clientError: false, +          serverError: false, +          info: { +            hasToken: !!options.token, +            status: !mocked ? 200 : mocked.status, +            url: _url, +            payload: options.data, +          }, +        }; +      } +      const SC: any = SWRConfig; + +      return ( +        <BackendContextProvider +          defaultUrl="http://backend" +          initialToken={undefined} +        > +          <InstanceContextProvider +            value={{ +              token: undefined, +              id: "default", +              admin: true, +              changeToken: () => null, +            }} +          > +            <ApiContextProvider value={{ request }}> +              <SC +                value={{ +                  loadingTimeout: 0, +                  dedupingInterval: 0, +                  shouldRetryOnError: false, +                  errorRetryInterval: 0, +                  errorRetryCount: 0, +                  provider: () => new Map(), +                }} +              > +                {children} +              </SC> +            </ApiContextProvider> +          </InstanceContextProvider> +        </BackendContextProvider> +      ); +    }; +  } +} diff --git a/packages/merchant-backoffice-ui/src/hooks/transfer.test.ts b/packages/merchant-backoffice-ui/src/hooks/transfer.test.ts new file mode 100644 index 000000000..a553ed362 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/hooks/transfer.test.ts @@ -0,0 +1,277 @@ +/* + 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 { tests } from "@gnu-taler/web-util/lib/index.browser"; +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: 0 }, +      response: { +        transfers: [{ wtid: "2" } as MerchantBackend.Transfers.TransferDetails], +      }, +    }); +    // FIXME: is this query really needed? if the hook is rendered without +    // position argument then then backend is returning the newest and no need +    // to this second query +    env.addRequestExpectation(API_LIST_TRANSFERS, { +      qparam: { limit: -20 }, +      response: { +        transfers: [], +      }, +    }); + +    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: 0 }, +            response: { +              transfers: [{ wtid: "2" } as any, { wtid: "3" } as any], +            }, +          }); + +          env.addRequestExpectation(API_LIST_TRANSFERS, { +            qparam: { limit: -20 }, +            response: { +              transfers: [], +            }, +          }); + +          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: 0, payto_uri: "payto://" }, +      response: { +        transfers: [{ wtid: "2" } as any], +      }, +    }); + +    env.addRequestExpectation(API_LIST_TRANSFERS, { +      qparam: { limit: -20, payto_uri: "payto://" }, +      response: { +        transfers: [{ 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/merchant-backoffice-ui/src/hooks/transfer.ts b/packages/merchant-backoffice-ui/src/hooks/transfer.ts index d1ac2c285..c827772e4 100644 --- a/packages/merchant-backoffice-ui/src/hooks/transfer.ts +++ b/packages/merchant-backoffice-ui/src/hooks/transfer.ts @@ -13,55 +13,21 @@   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 { useEffect, useState } from "preact/hooks"; +import useSWR from "swr";  import { MerchantBackend } from "../declaration.js"; -import { useBackendContext } from "../context/backend.js"; +import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js";  import { -  request, -  HttpResponse,    HttpError, +  HttpResponse,    HttpResponseOk,    HttpResponsePaginated, -  useMatchMutate, -} from "./backend.js"; -import useSWR from "swr"; -import { useInstanceContext } from "../context/instance.js"; -import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js"; -import { useEffect, useState } from "preact/hooks"; - -async function transferFetcher<T>( -  url: string, -  token: string, -  backend: string, -  payto_uri?: string, -  verified?: string, -  position?: string, -  delta?: number, -): Promise<HttpResponseOk<T>> { -  const params: any = {}; -  if (payto_uri !== undefined) params.payto_uri = payto_uri; -  if (verified !== undefined) params.verified = verified; -  if (delta !== undefined) { -    params.limit = delta; -  } -  if (position !== undefined) params.offset = position; - -  return request<T>(`${backend}${url}`, { token, params }); -} +} from "../utils/request.js"; +import { useBackendInstanceRequest, useMatchMutate } from "./backend.js";  export function useTransferAPI(): TransferAPI {    const mutateAll = useMatchMutate(); -  const { url: baseUrl, token: adminToken } = useBackendContext(); -  const { token: instanceToken, id, admin } = useInstanceContext(); - -  const { url, token } = !admin -    ? { -        url: baseUrl, -        token: adminToken, -      } -    : { -        url: `${baseUrl}/instances/${id}`, -        token: instanceToken, -      }; +  const { request } = useBackendInstanceRequest();    const informTransfer = async (      data: MerchantBackend.Transfers.TransferInformation, @@ -70,10 +36,9 @@ export function useTransferAPI(): TransferAPI {    > => {      const res =        await request<MerchantBackend.Transfers.MerchantTrackTransferResponse>( -        `${url}/private/transfers`, +        `/private/transfers`,          { -          method: "post", -          token, +          method: "POST",            data,          },        ); @@ -103,12 +68,7 @@ export function useInstanceTransfers(    args?: InstanceTransferFilter,    updatePosition?: (id: string) => void,  ): HttpResponsePaginated<MerchantBackend.Transfers.TransferList> { -  const { url: baseUrl, token: baseToken } = useBackendContext(); -  const { token: instanceToken, id, admin } = useInstanceContext(); - -  const { url, token } = !admin -    ? { url: baseUrl, token: baseToken } -    : { url: `${baseUrl}/instances/${id}`, token: instanceToken }; +  const { transferFetcher } = useBackendInstanceRequest();    const [pageBefore, setPageBefore] = useState(1);    const [pageAfter, setPageAfter] = useState(1); @@ -129,8 +89,6 @@ export function useInstanceTransfers(    } = useSWR<HttpResponseOk<MerchantBackend.Transfers.TransferList>, HttpError>(      [        `/private/transfers`, -      token, -      url,        args?.payto_uri,        args?.verified,        args?.position, @@ -145,8 +103,6 @@ export function useInstanceTransfers(    } = useSWR<HttpResponseOk<MerchantBackend.Transfers.TransferList>, HttpError>(      [        `/private/transfers`, -      token, -      url,        args?.payto_uri,        args?.verified,        args?.position, @@ -185,10 +141,9 @@ export function useInstanceTransfers(        if (afterData.data.transfers.length < MAX_RESULT_SIZE) {          setPageAfter(pageAfter + 1);        } else { -        const from = `${ -          afterData.data.transfers[afterData.data.transfers.length - 1] +        const from = `${afterData.data.transfers[afterData.data.transfers.length - 1]              .transfer_serial_id -        }`; +          }`;          if (from && updatePosition) updatePosition(from);        }      }, @@ -197,10 +152,9 @@ export function useInstanceTransfers(        if (beforeData.data.transfers.length < MAX_RESULT_SIZE) {          setPageBefore(pageBefore + 1);        } else if (beforeData) { -        const from = `${ -          beforeData.data.transfers[beforeData.data.transfers.length - 1] +        const from = `${beforeData.data.transfers[beforeData.data.transfers.length - 1]              .transfer_serial_id -        }`; +          }`;          if (from && updatePosition) updatePosition(from);        }      }, @@ -210,9 +164,9 @@ export function useInstanceTransfers(      !beforeData || !afterData        ? []        : (beforeData || lastBefore).data.transfers -          .slice() -          .reverse() -          .concat((afterData || lastAfter).data.transfers); +        .slice() +        .reverse() +        .concat((afterData || lastAfter).data.transfers);    if (loadingAfter || loadingBefore)      return { loading: true, data: { transfers } };    if (beforeData && afterData) { diff --git a/packages/merchant-backoffice-ui/src/hooks/urls.ts b/packages/merchant-backoffice-ui/src/hooks/urls.ts new file mode 100644 index 000000000..05494c0c9 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/hooks/urls.ts @@ -0,0 +1,291 @@ +/* + 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/lib/tests/mock"; +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, +  MerchantBackend.Transfers.MerchantTrackTransferResponse +> = { +  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.Tips.ReserveCreateRequest, +  MerchantBackend.Tips.ReserveCreateConfirmation +> = { +  method: "POST", +  url: "http://backend/instances/default/private/reserves", +}; +export const API_LIST_RESERVES: Query< +  unknown, +  MerchantBackend.Tips.TippingReserveStatus +> = { +  method: "GET", +  url: "http://backend/instances/default/private/reserves", +}; + +export const API_GET_RESERVE_BY_ID = ( +  pub: string, +): Query<unknown, MerchantBackend.Tips.ReserveDetail> => ({ +  method: "GET", +  url: `http://backend/instances/default/private/reserves/${pub}`, +}); + +export const API_GET_TIP_BY_ID = ( +  pub: string, +): Query<unknown, MerchantBackend.Tips.TipDetails> => ({ +  method: "GET", +  url: `http://backend/instances/default/private/tips/${pub}`, +}); + +export const API_AUTHORIZE_TIP_FOR_RESERVE = ( +  pub: string, +): Query< +  MerchantBackend.Tips.TipCreateRequest, +  MerchantBackend.Tips.TipCreateConfirmation +> => ({ +  method: "POST", +  url: `http://backend/instances/default/private/reserves/${pub}/authorize-tip`, +}); + +export const API_AUTHORIZE_TIP: Query< +  MerchantBackend.Tips.TipCreateRequest, +  MerchantBackend.Tips.TipCreateConfirmation +> = { +  method: "POST", +  url: `http://backend/instances/default/private/tips`, +}; + +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.Instances.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}`, +}); + +//////////////////// +// 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.Instances.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`, +};  | 
