aboutsummaryrefslogtreecommitdiff
path: root/packages/merchant-backoffice-ui/src/hooks
diff options
context:
space:
mode:
Diffstat (limited to 'packages/merchant-backoffice-ui/src/hooks')
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/async.ts4
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/backend.ts461
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/index.ts15
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/instance.test.ts660
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/instance.ts113
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/order.test.ts579
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/order.ts97
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/product.test.ts326
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/product.ts69
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/reserve.test.ts431
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/reserves.ts90
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/templates.ts92
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/testing.tsx120
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/transfer.test.ts277
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/transfer.ts80
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/urls.ts291
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`,
+};