aboutsummaryrefslogtreecommitdiff
path: root/packages/merchant-backoffice-ui/src/hooks
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2023-01-03 01:57:39 -0300
committerSebastian <sebasjm@gmail.com>2023-01-03 01:58:18 -0300
commita2668c22f0d18386fc988f27299172145d9fa15d (patch)
tree38f06046ce4d71ee3af64ede931754bfae6dc954 /packages/merchant-backoffice-ui/src/hooks
parentd1aa79eae817b1cf4c23f800308ecad101692ac7 (diff)
downloadwallet-core-a2668c22f0d18386fc988f27299172145d9fa15d.tar.xz
refactor better QA
removed axios, use fetch removed jest, added mocha and chai moved the default request handler to runtime dependency (so it can be replaced for testing) refactored ALL the test to the standard web-utils all hooks now use ONE request handler moved the tests from test folder to src
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`,
+};