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`, +}; |