gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-merchant-backoffice] branch master updated: test case for orders,


From: gnunet
Subject: [taler-merchant-backoffice] branch master updated: test case for orders, products, reserves and transfers
Date: Tue, 14 Dec 2021 20:13:30 +0100

This is an automated email from the git hooks/post-receive script.

sebasjm pushed a commit to branch master
in repository merchant-backoffice.

The following commit(s) were added to refs/heads/master by this push:
     new 58d06f9  test case for orders, products, reserves and transfers
58d06f9 is described below

commit 58d06f9b5eaa41c083c7702d772f174a2048c9c2
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Tue Dec 14 16:11:53 2021 -0300

    test case for orders, products, reserves and transfers
---
 .../merchant-backoffice/src/context/backend.ts     |  14 +-
 packages/merchant-backoffice/src/hooks/backend.ts  |  14 +-
 packages/merchant-backoffice/src/hooks/index.ts    |   4 +-
 packages/merchant-backoffice/src/hooks/order.ts    |  32 +-
 packages/merchant-backoffice/src/hooks/product.ts  |   4 +-
 packages/merchant-backoffice/src/hooks/reserves.ts |  18 +-
 packages/merchant-backoffice/src/hooks/transfer.ts |  36 +-
 .../src/utils/switchableAxios.ts                   |  66 +++
 packages/merchant-backoffice/tests/axiosMock.ts    | 182 +++++--
 .../merchant-backoffice/tests/hooks/swr/index.tsx  |  45 ++
 .../tests/hooks/swr/order-create.test.tsx          | 139 -----
 .../tests/hooks/swr/order-pagination.test.tsx      | 126 -----
 .../tests/hooks/swr/order.test.ts                  | 567 +++++++++++++++++++++
 .../tests/hooks/swr/product-create.test.tsx        | 142 ------
 .../tests/hooks/swr/product-delete.test.tsx        | 120 -----
 .../hooks/swr/product-details-update.test.tsx      | 113 ----
 .../tests/hooks/swr/product-update.test.tsx        | 127 -----
 .../tests/hooks/swr/product.test.ts                | 338 ++++++++++++
 .../tests/hooks/swr/reserve.test.ts                | 378 +++++++++++++-
 .../tests/hooks/swr/transfer-pagination.test.tsx   | 120 -----
 .../tests/hooks/swr/transfer.test.ts               | 268 ++++++++++
 21 files changed, 1831 insertions(+), 1022 deletions(-)

diff --git a/packages/merchant-backoffice/src/context/backend.ts 
b/packages/merchant-backoffice/src/context/backend.ts
index 9168d32..9ef7bfd 100644
--- a/packages/merchant-backoffice/src/context/backend.ts
+++ b/packages/merchant-backoffice/src/context/backend.ts
@@ -43,10 +43,10 @@ const BackendContext = createContext<BackendContextType>({
   updateLoginStatus: () => null,
 })
 
-function useBackendContextState(defaultUrl?:string): BackendContextType {
+function useBackendContextState(defaultUrl?: string, initialToken?: string): 
BackendContextType {
   const [url, triedToLog, changeBackend, resetBackend] = 
useBackendURL(defaultUrl);
-  const [token, _updateToken] = useBackendDefaultToken();
-  const updateToken = (t?:string) => {
+  const [token, _updateToken] = useBackendDefaultToken(initialToken);
+  const updateToken = (t?: string) => {
     _updateToken(t)
   }
 
@@ -73,10 +73,10 @@ function useBackendContextState(defaultUrl?:string): 
BackendContextType {
   return { url, token, triedToLog, updateLoginStatus, resetBackend, 
clearAllTokens, addTokenCleaner: addTokenCleanerMemo }
 }
 
-export const BackendContextProvider = ({children, defaultUrl}:{children:any, 
defaultUrl?:string}):VNode => {
-  const value = useBackendContextState(defaultUrl)
-  
-  return h(BackendContext.Provider, {value, children});
+export const BackendContextProvider = ({ children, defaultUrl, initialToken }: 
{ children: any, defaultUrl?: string, initialToken?: string }): VNode => {
+  const value = useBackendContextState(defaultUrl, initialToken)
+
+  return h(BackendContext.Provider, { value, children });
 }
 
 export const useBackendContext = (): BackendContextType => 
useContext(BackendContext);
diff --git a/packages/merchant-backoffice/src/hooks/backend.ts 
b/packages/merchant-backoffice/src/hooks/backend.ts
index 59bd0b6..1b27cfe 100644
--- a/packages/merchant-backoffice/src/hooks/backend.ts
+++ b/packages/merchant-backoffice/src/hooks/backend.ts
@@ -25,6 +25,7 @@ import { MerchantBackend } from "../declaration";
 import { useBackendContext } from "../context/backend";
 import { useEffect, useState } from "preact/hooks";
 import { DEFAULT_REQUEST_TIMEOUT } from "../utils/constants";
+import { axiosHandler, removeAxiosCancelToken } from 
"../utils/switchableAxios";
 
 export function useMatchMutate(): (
   re: RegExp,
@@ -226,20 +227,11 @@ function buildRequestFailed(
 const CancelToken = axios.CancelToken;
 let source = CancelToken.source();
 
-export function cancelPendingRequest() {
+export function cancelPendingRequest(): void {
   source.cancel("canceled by the user");
   source = CancelToken.source();
 }
 
-let removeAxiosCancelToken = false;
-/**
- * Jest mocking seems to break when using the cancelToken property.
- * Using this workaround when testing while finding the correct solution
- */
-export function setAxiosRequestAsTestingEnvironment() {
-  removeAxiosCancelToken = true;
-}
-
 export function isAxiosError<T>(
   error: AxiosError | any
 ): error is AxiosError<T> {
@@ -255,7 +247,7 @@ export async function request<T>(
     : undefined;
 
   try {
-    const res = await axios({
+    const res = await axiosHandler({
       url,
       responseType: "json",
       headers,
diff --git a/packages/merchant-backoffice/src/hooks/index.ts 
b/packages/merchant-backoffice/src/hooks/index.ts
index 19d672a..a647e3e 100644
--- a/packages/merchant-backoffice/src/hooks/index.ts
+++ b/packages/merchant-backoffice/src/hooks/index.ts
@@ -43,8 +43,8 @@ export function useBackendURL(url?: string): [string, 
boolean, StateUpdater<stri
   return [value, !!triedToLog, checkedSetter, resetBackend]
 }
 
-export function useBackendDefaultToken(): [string | undefined, 
StateUpdater<string | undefined>] {
-  return useLocalStorage('backend-token')
+export function useBackendDefaultToken(initialValue?: string): [string | 
undefined, StateUpdater<string | undefined>] {
+  return useLocalStorage('backend-token', initialValue)
 }
 
 export function useBackendInstanceToken(id: string): [string | undefined, 
StateUpdater<string | undefined>] {
diff --git a/packages/merchant-backoffice/src/hooks/order.ts 
b/packages/merchant-backoffice/src/hooks/order.ts
index 883bcef..0f8afce 100644
--- a/packages/merchant-backoffice/src/hooks/order.ts
+++ b/packages/merchant-backoffice/src/hooks/order.ts
@@ -106,7 +106,7 @@ export function useOrderAPI(): OrderAPI {
     data: MerchantBackend.Orders.RefundRequest
   ): Promise<HttpResponseOk<MerchantBackend.Orders.MerchantRefundResponse>> => 
{
     mutateAll(/@"\/private\/orders"@/);
-    return request<MerchantBackend.Orders.MerchantRefundResponse>(
+    const res = request<MerchantBackend.Orders.MerchantRefundResponse>(
       `${url}/private/orders/${orderId}/refund`,
       {
         method: "post",
@@ -115,7 +115,9 @@ export function useOrderAPI(): OrderAPI {
       }
     );
 
-    // return res
+    // order list returns refundable information, so we must evict everything
+    await mutateAll(/.*private\/orders.*/);
+    return res
   };
 
   const forgetOrder = async (
@@ -123,20 +125,25 @@ export function useOrderAPI(): OrderAPI {
     data: MerchantBackend.Orders.ForgetRequest
   ): Promise<HttpResponseOk<void>> => {
     mutateAll(/@"\/private\/orders"@/);
-    return request(`${url}/private/orders/${orderId}/forget`, {
+    const res = request<void>(`${url}/private/orders/${orderId}/forget`, {
       method: "patch",
       token,
       data,
     });
+    // we may be forgetting some fields that are pare of the listing, so we 
must evict everything
+    await mutateAll(/.*private\/orders.*/);
+    return res
   };
   const deleteOrder = async (
     orderId: string
   ): Promise<HttpResponseOk<void>> => {
     mutateAll(/@"\/private\/orders"@/);
-    return request(`${url}/private/orders/${orderId}`, {
+    const res = request<void>(`${url}/private/orders/${orderId}`, {
       method: "delete",
       token,
     });
+    await mutateAll(/.*private\/orders.*/);
+    return res
   };
 
   const getPaymentURL = async (
@@ -266,18 +273,19 @@ export function useInstanceOrders(
     if (beforeData) setLastBefore(beforeData);
   }, [afterData, beforeData]);
 
-  // this has problems when there are some ids missing
-
   if (beforeError) return beforeError;
   if (afterError) return afterError;
 
+  // if the query returns less that we ask, then we have reach the end or 
beginning
+  const isReachingEnd = afterData && afterData.data.orders.length < totalAfter;
+  const isReachingStart = args?.date === undefined ||
+    (beforeData && beforeData.data.orders.length < totalBefore);
+
   const pagination = {
-    isReachingEnd: afterData && afterData.data.orders.length < totalAfter,
-    isReachingStart:
-      !args?.date ||
-      (beforeData && beforeData.data.orders.length < totalBefore),
+    isReachingEnd,
+    isReachingStart,
     loadMore: () => {
-      if (!afterData) return;
+      if (!afterData || isReachingEnd) return;
       if (afterData.data.orders.length < MAX_RESULT_SIZE) {
         setPageAfter(pageAfter + 1);
       } else {
@@ -288,7 +296,7 @@ export function useInstanceOrders(
       }
     },
     loadMorePrev: () => {
-      if (!beforeData) return;
+      if (!beforeData || isReachingStart) return;
       if (beforeData.data.orders.length < MAX_RESULT_SIZE) {
         setPageBefore(pageBefore + 1);
       } else if (beforeData) {
diff --git a/packages/merchant-backoffice/src/hooks/product.ts 
b/packages/merchant-backoffice/src/hooks/product.ts
index 1e6ebf2..c99542b 100644
--- a/packages/merchant-backoffice/src/hooks/product.ts
+++ b/packages/merchant-backoffice/src/hooks/product.ts
@@ -61,7 +61,7 @@ export function useProductAPI(): ProductAPI {
       data,
     });
 
-    return await mutateAll(/.*private\/products.*/);
+    return await mutateAll(/.*"\/private\/products.*/);
   };
 
   const updateProduct = async (
@@ -95,7 +95,7 @@ export function useProductAPI(): ProductAPI {
       data,
     });
 
-    await mutateAll(/@"\/private\/products"@/);
+    return await mutateAll(/.*"\/private\/products.*/);
   };
 
   return { createProduct, updateProduct, deleteProduct, lockProduct };
diff --git a/packages/merchant-backoffice/src/hooks/reserves.ts 
b/packages/merchant-backoffice/src/hooks/reserves.ts
index 259bd1c..7a662df 100644
--- a/packages/merchant-backoffice/src/hooks/reserves.ts
+++ b/packages/merchant-backoffice/src/hooks/reserves.ts
@@ -13,7 +13,7 @@
  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 useSWR from "swr";
+import useSWR, { useSWRConfig } from "swr";
 import { useBackendContext } from "../context/backend";
 import { useInstanceContext } from "../context/instance";
 import { MerchantBackend } from "../declaration";
@@ -28,6 +28,7 @@ import {
 
 export function useReservesAPI(): ReserveMutateAPI {
   const mutateAll = useMatchMutate();
+  const { mutate } = useSWRConfig();
   const { url: baseUrl, token: adminToken } = useBackendContext();
   const { token: instanceToken, id, admin } = useInstanceContext();
 
@@ -49,6 +50,7 @@ export function useReservesAPI(): ReserveMutateAPI {
       }
     );
 
+    //evict reserve list query
     await mutateAll(/.*private\/reserves.*/);
 
     return res;
@@ -66,7 +68,9 @@ export function useReservesAPI(): ReserveMutateAPI {
         data,
       }
     );
-    await mutateAll(/@"\/private\/reserves"@/);
+
+    //evict reserve details query
+    await mutate([`/private/reserves/${pub}`, token, url]);
 
     return res;
   };
@@ -83,7 +87,8 @@ export function useReservesAPI(): ReserveMutateAPI {
       }
     );
 
-    await mutateAll(/@"\/private\/reserves"@/);
+    //evict all details query
+    await mutateAll(/.*private\/reserves\/.*/);
 
     return res;
   };
@@ -94,7 +99,8 @@ export function useReservesAPI(): ReserveMutateAPI {
       token,
     });
 
-    await mutateAll(/@"\/private\/reserves"@/);
+    //evict reserve list query
+    await mutateAll(/.*private\/reserves.*/);
 
     return res;
   };
@@ -185,7 +191,7 @@ export function useTipDetails(
   return { loading: true };
 }
 
-export function reserveDetailFetcher<T>(
+function reserveDetailFetcher<T>(
   url: string,
   token: string,
   backend: string
@@ -198,7 +204,7 @@ export function reserveDetailFetcher<T>(
   });
 }
 
-export function tipsDetailFetcher<T>(
+function tipsDetailFetcher<T>(
   url: string,
   token: string,
   backend: string
diff --git a/packages/merchant-backoffice/src/hooks/transfer.ts 
b/packages/merchant-backoffice/src/hooks/transfer.ts
index 86ed420..0c12d6d 100644
--- a/packages/merchant-backoffice/src/hooks/transfer.ts
+++ b/packages/merchant-backoffice/src/hooks/transfer.ts
@@ -68,16 +68,15 @@ export function useTransferAPI(): TransferAPI {
   ): Promise<
     HttpResponseOk<MerchantBackend.Transfers.MerchantTrackTransferResponse>
   > => {
-    mutateAll(/@"\/private\/transfers"@/);
-
-    return request<MerchantBackend.Transfers.MerchantTrackTransferResponse>(
-      `${url}/private/transfers`,
-      {
-        method: "post",
-        token,
-        data,
-      }
-    );
+    const res = await 
request<MerchantBackend.Transfers.MerchantTrackTransferResponse>(
+      `${url}/private/transfers`, {
+      method: "post",
+      token,
+      data,
+    });
+
+    await mutateAll(/.*private\/transfers.*/);
+    return res
   };
 
   return { informTransfer };
@@ -165,18 +164,19 @@ export function useInstanceTransfers(
     if (beforeData) setLastBefore(beforeData);
   }, [afterData, beforeData]);
 
-  // this has problems when there are some ids missing
-
   if (beforeError) return beforeError;
   if (afterError) return afterError;
 
+  // if the query returns less that we ask, then we have reach the end or 
beginning
+  const isReachingEnd = afterData && afterData.data.transfers.length < 
totalAfter;
+  const isReachingStart = args?.position === undefined ||
+    (beforeData && beforeData.data.transfers.length < totalBefore);
+
   const pagination = {
-    isReachingEnd: afterData && afterData.data.transfers.length < totalAfter,
-    isReachingStart:
-      !args?.position ||
-      (beforeData && beforeData.data.transfers.length < totalBefore),
+    isReachingEnd,
+    isReachingStart,
     loadMore: () => {
-      if (!afterData) return;
+      if (!afterData || isReachingEnd) return;
       if (afterData.data.transfers.length < MAX_RESULT_SIZE) {
         setPageAfter(pageAfter + 1);
       } else {
@@ -188,7 +188,7 @@ export function useInstanceTransfers(
       }
     },
     loadMorePrev: () => {
-      if (!beforeData) return;
+      if (!beforeData || isReachingStart) return;
       if (beforeData.data.transfers.length < MAX_RESULT_SIZE) {
         setPageBefore(pageBefore + 1);
       } else if (beforeData) {
diff --git a/packages/merchant-backoffice/src/utils/switchableAxios.ts 
b/packages/merchant-backoffice/src/utils/switchableAxios.ts
new file mode 100644
index 0000000..be7eedd
--- /dev/null
+++ b/packages/merchant-backoffice/src/utils/switchableAxios.ts
@@ -0,0 +1,66 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+import axios, { AxiosPromise, AxiosRequestConfig } from "axios";
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+export let removeAxiosCancelToken = false;
+
+export let axiosHandler = function doAxiosRequest(config: AxiosRequestConfig): 
AxiosPromise<any> {
+  return axios(config)
+}
+
+/**
+ * Set this backend library to testing mode.
+ * Instead of calling the axios library the @handler will be called
+ * 
+ * @param handler callback that will mock axios 
+ */
+export function setAxiosRequestAsTestingEnvironment(handler: AxiosHandler): 
void {
+  removeAxiosCancelToken = true;
+  axiosHandler = function defaultTestingHandler(config) {
+    const currentHanlder = listOfHandlersToUseOnce.shift()
+    if (!currentHanlder) {
+      return handler(config)
+    }
+
+    return currentHanlder(config)
+  }
+}
+
+type AxiosHandler = (config: AxiosRequestConfig) => AxiosPromise<any>;
+type AxiosArguments = { args: AxiosRequestConfig | undefined }
+
+
+const listOfHandlersToUseOnce = new Array<AxiosHandler>()
+
+/**
+ * 
+ * @param handler mock function
+ * @returns savedArgs
+ */
+export function mockAxiosOnce(handler: AxiosHandler): { args: 
AxiosRequestConfig | undefined } {
+  const savedArgs: AxiosArguments = { args: undefined }
+  listOfHandlersToUseOnce.push((config: AxiosRequestConfig): AxiosPromise<any> 
=> {
+    savedArgs.args = config;
+    return handler(config)
+  })
+  return savedArgs;
+}
diff --git a/packages/merchant-backoffice/tests/axiosMock.ts 
b/packages/merchant-backoffice/tests/axiosMock.ts
index a6ecf60..7b33e2a 100644
--- a/packages/merchant-backoffice/tests/axiosMock.ts
+++ b/packages/merchant-backoffice/tests/axiosMock.ts
@@ -20,7 +20,8 @@
 */
 import * as axios from 'axios';
 import { MerchantBackend } from '../src/declaration';
-import { setAxiosRequestAsTestingEnvironment } from "../src/hooks/backend";
+import { mockAxiosOnce, setAxiosRequestAsTestingEnvironment } from 
'../src/utils/switchableAxios';
+// import { mockAxiosOnce, setAxiosRequestAsTestingEnvironment } from 
"../src/hooks/backend";
 
 export type Query<Req, Res> = (GetQuery | PostQuery | DeleteQuery | 
PatchQuery) & RequestResponse<Req, Res>
 
@@ -32,29 +33,28 @@ interface PostQuery { post: string }
 interface DeleteQuery { delete: string }
 interface PatchQuery { patch: string }
 
-setAxiosRequestAsTestingEnvironment();
 
 const JEST_DEBUG_LOG = process.env['JEST_DEBUG_LOG'] !== undefined
 
 type TestValues = [axios.AxiosRequestConfig | undefined, { query: Query<any, 
any>; params?: { request?: any, qparam?: any, response?: any } } | undefined]
 
-const defaultCallback = (actualQuery?: axios.AxiosRequestConfig) => {
+const defaultCallback = (actualQuery?: axios.AxiosRequestConfig): 
axios.AxiosPromise<any> => {
   if (JEST_DEBUG_LOG) {
     console.log('UNEXPECTED QUERY', actualQuery)
   }
+  throw Error('Default Axios mock callback is called, this mean that the test 
did a tried to use axios but there was no expectation in place, try using 
JEST_DEBUG_LOG env')
 }
 
-export class AxiosMockEnvironment {
-  expectations: Array<{ query: Query<any, any>, params?: { request?: any, 
qparam?: any, response?: any } } | undefined> = []
-  axiosMock: jest.MockedFunction<axios.AxiosStatic>
+setAxiosRequestAsTestingEnvironment(
+  defaultCallback
+);
 
-  constructor() {
-    this.axiosMock = (axios.default as 
jest.MockedFunction<axios.AxiosStatic>).mockImplementation(defaultCallback as 
any)
-  }
+export class AxiosMockEnvironment {
+  expectations: Array<{ query: Query<any, any>, params?: { request?: any, 
qparam?: any, response?: any }, result: { args: axios.AxiosRequestConfig | 
undefined } } | undefined> = []
+  // axiosMock: jest.MockedFunction<axios.AxiosStatic>
 
   addRequestExpectation<RequestType, ResponseType>(expectedQuery: 
Query<RequestType, ResponseType>, params: { request?: RequestType, qparam?: 
any, response?: ResponseType }): void {
-    this.expectations.push(expectedQuery ? { query: expectedQuery, params } : 
undefined)
-    this.axiosMock = this.axiosMock.mockImplementationOnce(function 
(actualQuery?: axios.AxiosRequestConfig): axios.AxiosPromise {
+    const result = mockAxiosOnce(function (actualQuery?: 
axios.AxiosRequestConfig): axios.AxiosPromise {
 
       if (JEST_DEBUG_LOG) {
         console.log('query to the backend is made', actualQuery)
@@ -91,23 +91,15 @@ export class AxiosMockEnvironment {
       })
 
     } as any)
+
+    this.expectations.push(expectedQuery ? { query: expectedQuery, params, 
result } : undefined)
   }
 
   getLastTestValues(): TestValues {
-    const lastCall = this.axiosMock.mock.calls[0]
-    if (lastCall === undefined) {
-      const expectedQuery = this.expectations.shift()
-      return [undefined, expectedQuery]
-    }
-    const actualQuery = lastCall[0] as axios.AxiosRequestConfig
-
-    //Remove values from the last call
     const expectedQuery = this.expectations.shift()
-    this.axiosMock.mock.calls.shift()
-    this.axiosMock.mock.results.shift()
 
     return [
-      actualQuery, expectedQuery
+      expectedQuery?.result.args, expectedQuery
     ]
   }
 
@@ -170,19 +162,25 @@ export function assertNextRequest(env: 
AxiosMockEnvironment): void {
 
 }
 
-export const API_LIST_PRODUCTS: Query<
-  unknown,
-  MerchantBackend.Products.InventorySummaryResponse
+////////////////////
+// ORDER
+////////////////////
+
+export const API_CREATE_ORDER: Query<
+  MerchantBackend.Orders.PostOrderRequest,
+  MerchantBackend.Orders.PostOrderResponse
 > = {
-  get: "http://backend/instances/default/private/products";,
+  post: "http://backend/instances/default/private/orders";,
 };
 
-export const API_LIST_RESERVES: Query<
+export const API_GET_ORDER_BY_ID = (
+  id: string
+): Query<
   unknown,
-  MerchantBackend.Tips.TippingReserveStatus
-> = {
-  get: "http://backend/instances/default/private/reserves";,
-};
+  MerchantBackend.Orders.MerchantOrderStatusResponse
+> => ({
+  get: `http://backend/instances/default/private/orders/${id}`,
+});
 
 export const API_LIST_ORDERS: Query<
   unknown,
@@ -191,6 +189,37 @@ export const API_LIST_ORDERS: Query<
   get: "http://backend/instances/default/private/orders";,
 };
 
+export const API_REFUND_ORDER_BY_ID = (
+  id: string
+): Query<
+  MerchantBackend.Orders.RefundRequest,
+  MerchantBackend.Orders.MerchantRefundResponse
+> => ({
+  post: `http://backend/instances/default/private/orders/${id}/refund`,
+});
+
+export const API_FORGET_ORDER_BY_ID = (
+  id: string
+): Query<
+  MerchantBackend.Orders.ForgetRequest,
+  unknown
+> => ({
+  patch: `http://backend/instances/default/private/orders/${id}/forget`,
+});
+
+export const API_DELETE_ORDER = (
+  id: string
+): Query<
+  MerchantBackend.Orders.ForgetRequest,
+  unknown
+> => ({
+  delete: `http://backend/instances/default/private/orders/${id}`,
+});
+
+////////////////////
+// TRANSFER
+////////////////////
+
 export const API_LIST_TRANSFERS: Query<
   unknown,
   MerchantBackend.Transfers.TransferList
@@ -198,31 +227,29 @@ export const API_LIST_TRANSFERS: Query<
   get: "http://backend/instances/default/private/transfers";,
 };
 
+export const API_INFORM_TRANSFERS: Query<
+  MerchantBackend.Transfers.TransferInformation,
+  MerchantBackend.Transfers.MerchantTrackTransferResponse
+> = {
+  post: "http://backend/instances/default/private/transfers";,
+};
+
+////////////////////
+// PRODUCT
+////////////////////
+
 export const API_CREATE_PRODUCT: Query<
   MerchantBackend.Products.ProductAddDetail,
   unknown
 > = {
   post: "http://backend/instances/default/private/products";,
 };
-export const API_CREATE_RESERVE: Query<
-  MerchantBackend.Tips.ReserveCreateRequest,
-  MerchantBackend.Tips.ReserveCreateConfirmation
-> = {
-  post: "http://backend/instances/default/private/reserves";,
-};
 
-export const API_DELETE_RESERVE: Query<
-  MerchantBackend.Tips.ReserveCreateRequest,
-  MerchantBackend.Tips.ReserveCreateConfirmation
-> = {
-  delete: "http://backend/instances/default/private/reserves";,
-};
-
-export const API_CREATE_ORDER: Query<
-  MerchantBackend.Orders.PostOrderRequest,
-  MerchantBackend.Orders.PostOrderResponse
+export const API_LIST_PRODUCTS: Query<
+  unknown,
+  MerchantBackend.Products.InventorySummaryResponse
 > = {
-  post: "http://backend/instances/default/private/orders";,
+  get: "http://backend/instances/default/private/products";,
 };
 
 export const API_GET_PRODUCT_BY_ID = (
@@ -234,15 +261,72 @@ export const API_GET_PRODUCT_BY_ID = (
 export const API_UPDATE_PRODUCT_BY_ID = (
   id: string
 ): Query<
-  Partial<MerchantBackend.Products.ProductPatchDetail>,
+  MerchantBackend.Products.ProductPatchDetail,
   MerchantBackend.Products.InventorySummaryResponse
 > => ({
   patch: `http://backend/instances/default/private/products/${id}`,
 });
 
+export const API_DELETE_PRODUCT = (
+  id: string
+): Query<
+  unknown, unknown
+> => ({
+  delete: `http://backend/instances/default/private/products/${id}`,
+});
+
+////////////////////
+// RESERVES
+////////////////////
+
+export const API_CREATE_RESERVE: Query<
+  MerchantBackend.Tips.ReserveCreateRequest,
+  MerchantBackend.Tips.ReserveCreateConfirmation
+> = {
+  post: "http://backend/instances/default/private/reserves";,
+};
+export const API_LIST_RESERVES: Query<
+  unknown,
+  MerchantBackend.Tips.TippingReserveStatus
+> = {
+  get: "http://backend/instances/default/private/reserves";,
+};
+
 export const API_GET_RESERVE_BY_ID = (
   pub: string
 ): Query<unknown, MerchantBackend.Tips.ReserveDetail> => ({
   get: `http://backend/instances/default/private/reserves/${pub}`,
 });
 
+export const API_GET_TIP_BY_ID = (
+  pub: string
+): Query<
+  unknown,
+  MerchantBackend.Tips.TipDetails
+> => ({
+  get: `http://backend/instances/default/private/tips/${pub}`,
+});
+
+export const API_AUTHORIZE_TIP_FOR_RESERVE = (
+  pub: string
+): Query<
+  MerchantBackend.Tips.TipCreateRequest,
+  MerchantBackend.Tips.TipCreateConfirmation
+> => ({
+  post: 
`http://backend/instances/default/private/reserves/${pub}/authorize-tip`,
+});
+
+export const API_AUTHORIZE_TIP: Query<
+  MerchantBackend.Tips.TipCreateRequest,
+  MerchantBackend.Tips.TipCreateConfirmation
+> = ({
+  post: `http://backend/instances/default/private/tips`,
+});
+
+
+export const API_DELETE_RESERVE = (
+  id: string
+): Query<unknown, unknown> => ({
+  delete: `http://backend/instances/default/private/reserves/${id}`,
+});
+
diff --git a/packages/merchant-backoffice/tests/hooks/swr/index.tsx 
b/packages/merchant-backoffice/tests/hooks/swr/index.tsx
new file mode 100644
index 0000000..4451485
--- /dev/null
+++ b/packages/merchant-backoffice/tests/hooks/swr/index.tsx
@@ -0,0 +1,45 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 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 { ComponentChildren, h, VNode } from "preact";
+import { SWRConfig } from "swr";
+import { BackendContextProvider } from "../../../src/context/backend";
+import { InstanceContextProvider } from "../../../src/context/instance";
+
+interface TestingContextProps {
+  children?: ComponentChildren;
+}
+export function TestingContext({ children }: TestingContextProps): VNode {
+  return (
+    <BackendContextProvider defaultUrl="http://backend"; initialToken="token">
+      <InstanceContextProvider
+        value={{
+          token: "token",
+          id: "default",
+          admin: true,
+          changeToken: () => null,
+        }}
+      >
+        <SWRConfig value={{ provider: () => new Map() }}>{children}</SWRConfig>
+      </InstanceContextProvider>
+    </BackendContextProvider>
+  );
+}
diff --git a/packages/merchant-backoffice/tests/hooks/swr/order-create.test.tsx 
b/packages/merchant-backoffice/tests/hooks/swr/order-create.test.tsx
deleted file mode 100644
index 4563acd..0000000
--- a/packages/merchant-backoffice/tests/hooks/swr/order-create.test.tsx
+++ /dev/null
@@ -1,139 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 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 { renderHook } from "@testing-library/preact-hooks";
-import { act } from "preact/test-utils";
-import * as backend from "../../../src/context/backend";
-import * as instance from "../../../src/context/instance";
-import { MerchantBackend } from "../../../src/declaration";
-import { useInstanceOrders, useOrderAPI } from "../../../src/hooks/order";
-import {
-  API_CREATE_ORDER,
-  API_LIST_ORDERS,
-  AxiosMockEnvironment,
-  assertNoMoreRequestWereMade,
-  assertNextRequest,
-} from "../../axiosMock";
-
-jest.mock("axios");
-
-describe("order api", () => {
-  beforeEach(() => {
-    jest
-      .spyOn(backend, "useBackendContext")
-      .mockImplementation(
-        () => ({ url: "http://backend";, token: "token" } as any)
-      );
-    jest
-      .spyOn(instance, "useInstanceContext")
-      .mockImplementation(
-        () => ({ token: "token", id: "default", admin: true } as any)
-      );
-  });
-
-  it("should not have problem with cache after an creation", async () => {
-    const env = new AxiosMockEnvironment();
-    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 { result, waitForNextUpdate } = renderHook(() => {
-      const query = useInstanceOrders({ paid: "yes" }, newDate);
-      const api = useOrderAPI();
-
-      return { query, api };
-    }); // get products -> loading
-
-    if (!result.current) {
-      expect(result.current).toBeDefined();
-      return;
-    }
-
-    expect(result.current.query.loading).toBeTruthy();
-    await waitForNextUpdate();
-    assertNextRequest(env);
-    assertNextRequest(env);
-    assertNoMoreRequestWereMade(env);
-
-    expect(result.current.query.loading).toBeFalsy();
-    expect(result.current?.query.ok).toBeTruthy();
-    if (!result.current?.query.ok) return;
-
-    expect(result.current.query.data).toEqual({
-      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],
-      },
-    });
-
-    act(async () => {
-      await result.current?.api.createOrder({
-        order: { amount: "ARS:12", summary: "pay me" },
-      } as any);
-    });
-
-    assertNextRequest(env); //post
-    await waitForNextUpdate();
-    assertNextRequest(env); //get
-    assertNextRequest(env); //get
-    assertNoMoreRequestWereMade(env);
-
-    expect(result.current.query.loading).toBeFalsy();
-    expect(result.current?.query.ok).toBeTruthy();
-    if (!result.current?.query.ok) return;
-
-    expect(result.current.query.data).toEqual({
-      orders: [{ order_id: "1" }, { order_id: "2" }, { order_id: "3" }],
-    });
-  });
-});
diff --git 
a/packages/merchant-backoffice/tests/hooks/swr/order-pagination.test.tsx 
b/packages/merchant-backoffice/tests/hooks/swr/order-pagination.test.tsx
deleted file mode 100644
index b29f057..0000000
--- a/packages/merchant-backoffice/tests/hooks/swr/order-pagination.test.tsx
+++ /dev/null
@@ -1,126 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 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 { act, renderHook } from "@testing-library/preact-hooks";
-import * as backend from "../../../src/context/backend";
-import * as instance from "../../../src/context/instance";
-import { useInstanceOrders } from "../../../src/hooks/order";
-import {
-  API_LIST_ORDERS,
-  AxiosMockEnvironment,
-  assertNoMoreRequestWereMade,
-  assertNextRequest,
-} from "../../axiosMock";
-
-jest.mock("axios");
-
-describe("order pagination", () => {
-  beforeEach(() => {
-    jest
-      .spyOn(backend, "useBackendContext")
-      .mockImplementation(
-        () => ({ url: "http://backend";, token: "token" } as any)
-      );
-    jest
-      .spyOn(instance, "useInstanceContext")
-      .mockImplementation(
-        () => ({ token: "token", id: "default", admin: true } as any)
-      );
-  });
-
-  it("should change pagination", async () => {
-    const env = new AxiosMockEnvironment();
-    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 date = new Date(12);
-    const { result, waitForNextUpdate } = renderHook(() =>
-      useInstanceOrders({ wired: "yes", date }, newDate)
-    );
-
-    assertNextRequest(env);
-    assertNextRequest(env);
-    assertNoMoreRequestWereMade(env);
-
-    await waitForNextUpdate();
-
-    expect(result.current?.ok).toBeTruthy();
-    if (!result.current?.ok) return;
-
-    expect(result.current.data).toEqual({
-      orders: [{ order_id: "1" }, { order_id: "2" }],
-    });
-
-    env.addRequestExpectation(API_LIST_ORDERS, {
-      qparam: { delta: -40, wired: "yes", date_ms: 13 },
-      response: {
-        orders: [{ order_id: "2" } as any, { order_id: "3" } as any],
-      },
-    });
-
-    await act(() => {
-      if (!result.current?.ok) throw Error("not ok");
-      result.current.loadMore();
-    });
-    await waitForNextUpdate();
-
-    assertNextRequest(env);
-    assertNoMoreRequestWereMade(env);
-
-    env.addRequestExpectation(API_LIST_ORDERS, {
-      qparam: { delta: 40, wired: "yes", date_ms: 12 },
-      response: {
-        orders: [{ order_id: "1" } as any, { order_id: "0" } as any],
-      },
-    });
-    await act(() => {
-      if (!result.current?.ok) throw Error("not ok");
-      result.current.loadMorePrev();
-    });
-    await waitForNextUpdate();
-    assertNextRequest(env);
-    assertNoMoreRequestWereMade(env);
-
-    expect(result.current.data).toEqual({
-      orders: [
-        { order_id: "0" },
-        { order_id: "1" },
-        { order_id: "2" },
-        { order_id: "3" },
-      ],
-    });
-  });
-});
diff --git a/packages/merchant-backoffice/tests/hooks/swr/order.test.ts 
b/packages/merchant-backoffice/tests/hooks/swr/order.test.ts
new file mode 100644
index 0000000..e7f6c93
--- /dev/null
+++ b/packages/merchant-backoffice/tests/hooks/swr/order.test.ts
@@ -0,0 +1,567 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 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 { renderHook } from "@testing-library/preact-hooks";
+import { act } from "preact/test-utils";
+import { TestingContext } from ".";
+import { MerchantBackend } from "../../../src/declaration";
+import { useInstanceOrders, useOrderAPI, useOrderDetails } from 
"../../../src/hooks/order";
+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, assertJustExpectedRequestWereMade, 
assertNextRequest, assertNoMoreRequestWereMade, AxiosMockEnvironment
+} from "../../axiosMock";
+
+describe("order api interaction with listing", () => {
+
+  it("should evict cache when creating an order", async () => {
+    const env = new AxiosMockEnvironment();
+
+    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 { result, waitForNextUpdate } = renderHook(() => {
+      const newDate = (d: Date) => {
+        console.log("new date", d);
+      };
+      const query = useInstanceOrders({ paid: "yes" }, newDate);
+      const api = useOrderAPI();
+
+      return { query, api };
+    }, { wrapper: TestingContext });
+
+    if (!result.current) {
+      expect(result.current).toBeDefined();
+      return;
+    }
+
+    expect(result.current.query.loading).toBeTruthy();
+    await waitForNextUpdate({ timeout: 1 });
+    assertJustExpectedRequestWereMade(env);
+
+    expect(result.current.query.loading).toBeFalsy();
+    expect(result.current?.query.ok).toBeTruthy();
+    if (!result.current?.query.ok) return;
+
+    expect(result.current.query.data).toEqual({
+      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],
+      },
+    });
+
+    act(async () => {
+      await result.current?.api.createOrder({
+        order: { amount: "ARS:12", summary: "pay me" },
+      } as any);
+    });
+
+    await waitForNextUpdate({ timeout: 1 });
+    assertJustExpectedRequestWereMade(env);
+
+    expect(result.current.query.loading).toBeFalsy();
+    expect(result.current?.query.ok).toBeTruthy();
+    if (!result.current?.query.ok) return;
+
+    expect(result.current.query.data).toEqual({
+      orders: [{ order_id: "1" }, { order_id: "2" }, { order_id: "3" }],
+    });
+  });
+  it("should evict cache when doing a refund", async () => {
+    const env = new AxiosMockEnvironment();
+
+    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 { result, waitForNextUpdate } = renderHook(() => {
+      const newDate = (d: Date) => {
+        console.log("new date", d);
+      };
+      const query = useInstanceOrders({ paid: "yes" }, newDate);
+      const api = useOrderAPI();
+
+      return { query, api };
+    }, { wrapper: TestingContext });
+
+    if (!result.current) {
+      expect(result.current).toBeDefined();
+      return;
+    }
+
+    expect(result.current.query.loading).toBeTruthy();
+    await waitForNextUpdate({ timeout: 1 });
+    assertJustExpectedRequestWereMade(env);
+
+
+    expect(result.current.query.loading).toBeFalsy();
+    expect(result.current?.query.ok).toBeTruthy();
+    if (!result.current?.query.ok) return;
+
+    expect(result.current.query.data).toEqual({
+      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: [], },
+    });
+
+    act(async () => {
+      await result.current?.api.refundOrder('1', {
+        reason: 'double pay',
+        refund: 'EUR:1'
+      });
+    });
+
+    await waitForNextUpdate({ timeout: 1 });
+    assertJustExpectedRequestWereMade(env);
+
+    expect(result.current.query.loading).toBeFalsy();
+    expect(result.current?.query.ok).toBeTruthy();
+    if (!result.current?.query.ok) return;
+
+    expect(result.current.query.data).toEqual({
+      orders: [{
+        order_id: "1",
+        amount: 'EUR:12',
+        refundable: false,
+      }],
+    });
+  });
+  it("should evict cache when deleting an order", async () => {
+    const env = new AxiosMockEnvironment();
+
+    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 { result, waitForNextUpdate } = renderHook(() => {
+      const newDate = (d: Date) => {
+        console.log("new date", d);
+      };
+      const query = useInstanceOrders({ paid: "yes" }, newDate);
+      const api = useOrderAPI();
+
+      return { query, api };
+    }, { wrapper: TestingContext });
+
+    if (!result.current) {
+      expect(result.current).toBeDefined();
+      return;
+    }
+
+    expect(result.current.query.loading).toBeTruthy();
+    await waitForNextUpdate({ timeout: 1 });
+    assertJustExpectedRequestWereMade(env);
+
+    expect(result.current.query.loading).toBeFalsy();
+    expect(result.current?.query.ok).toBeTruthy();
+    if (!result.current?.query.ok) return;
+
+    expect(result.current.query.data).toEqual({
+      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],
+      },
+    });
+
+    act(async () => {
+      await result.current?.api.deleteOrder('1');
+    });
+
+    await waitForNextUpdate({ timeout: 1 });
+    assertJustExpectedRequestWereMade(env);
+
+    expect(result.current.query.loading).toBeFalsy();
+    expect(result.current?.query.ok).toBeTruthy();
+    if (!result.current?.query.ok) return;
+
+    expect(result.current.query.data).toEqual({
+      orders: [{ order_id: "2" }],
+    });
+  });
+
+});
+
+describe("order api interaction with details", () => {
+
+  it("should evict cache when doing a refund", async () => {
+    const env = new AxiosMockEnvironment();
+
+    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 { result, waitForNextUpdate } = renderHook(() => {
+      const query = useOrderDetails('1')
+      const api = useOrderAPI();
+
+      return { query, api };
+    }, { wrapper: TestingContext });
+
+    if (!result.current) {
+      expect(result.current).toBeDefined();
+      return;
+    }
+
+    expect(result.current.query.loading).toBeTruthy();
+    await waitForNextUpdate({ timeout: 1 });
+    assertJustExpectedRequestWereMade(env);
+
+    expect(result.current.query.loading).toBeFalsy();
+    expect(result.current?.query.ok).toBeTruthy();
+    if (!result.current?.query.ok) return;
+
+    expect(result.current.query.data).toEqual({
+      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,
+    });
+
+    act(async () => {
+      await result.current?.api.refundOrder('1', {
+        reason: 'double pay',
+        refund: 'EUR:1'
+      });
+    });
+
+    await waitForNextUpdate({ timeout: 1 });
+    assertJustExpectedRequestWereMade(env);
+
+    expect(result.current.query.loading).toBeFalsy();
+    expect(result.current?.query.ok).toBeTruthy();
+    if (!result.current?.query.ok) return;
+
+    expect(result.current.query.data).toEqual({
+      summary: 'description',
+      refund_amount: 'EUR:1',
+    });
+  })
+  it("should evict cache when doing a forget", async () => {
+    const env = new AxiosMockEnvironment();
+
+    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 { result, waitForNextUpdate } = renderHook(() => {
+      const query = useOrderDetails('1')
+      const api = useOrderAPI();
+
+      return { query, api };
+    }, { wrapper: TestingContext });
+
+    if (!result.current) {
+      expect(result.current).toBeDefined();
+      return;
+    }
+
+    expect(result.current.query.loading).toBeTruthy();
+    await waitForNextUpdate({ timeout: 1 });
+    assertJustExpectedRequestWereMade(env);
+
+    expect(result.current.query.loading).toBeFalsy();
+    expect(result.current?.query.ok).toBeTruthy();
+    if (!result.current?.query.ok) return;
+
+    expect(result.current.query.data).toEqual({
+      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,
+    });
+
+    act(async () => {
+      await result.current?.api.forgetOrder('1', {
+        fields: ['$.summary']
+      });
+    });
+
+    await waitForNextUpdate({ timeout: 1 });
+    assertJustExpectedRequestWereMade(env);
+
+    expect(result.current.query.loading).toBeFalsy();
+    expect(result.current?.query.ok).toBeTruthy();
+    if (!result.current?.query.ok) return;
+
+    expect(result.current.query.data).toEqual({
+      summary: undefined,
+    });
+  })
+})
+
+describe("order listing pagination", () => {
+
+  it("should not load more if has reach the end", async () => {
+    const env = new AxiosMockEnvironment();
+    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 { result, waitForNextUpdate } = renderHook(() => {
+      const newDate = (d: Date) => {
+        console.log("new date", d);
+      };
+      const date = new Date(12);
+      const query = useInstanceOrders({ wired: "yes", date }, newDate)
+      return { query }
+    }, { wrapper: TestingContext });
+
+    assertJustExpectedRequestWereMade(env);
+
+    await waitForNextUpdate();
+
+    expect(result.current?.query.ok).toBeTruthy();
+    if (!result.current?.query.ok) return;
+
+    expect(result.current.query.data).toEqual({
+      orders: [{ order_id: "1" }, { order_id: "2" }],
+    });
+
+    expect(result.current.query.isReachingEnd).toBeTruthy()
+    expect(result.current.query.isReachingStart).toBeTruthy()
+
+    await act(() => {
+      if (!result.current?.query.ok) throw Error("not ok");
+      result.current.query.loadMore();
+    });
+    assertNoMoreRequestWereMade(env);
+
+    await act(() => {
+      if (!result.current?.query.ok) throw Error("not ok");
+      result.current.query.loadMorePrev();
+    });
+    assertNoMoreRequestWereMade(env);
+
+    expect(result.current.query.data).toEqual({
+      orders: [
+        { order_id: "1" },
+        { order_id: "2" },
+      ],
+    });
+  });
+
+  it("should load more if result brings more that PAGE_SIZE", async () => {
+    const env = new AxiosMockEnvironment();
+
+    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 { result, waitForNextUpdate } = renderHook(() => {
+      const newDate = (d: Date) => {
+        console.log("new date", d);
+      };
+      const date = new Date(12);
+      const query = useInstanceOrders({ wired: "yes", date }, newDate)
+      return { query }
+    }, { wrapper: TestingContext });
+
+    assertJustExpectedRequestWereMade(env);
+
+    await waitForNextUpdate({ timeout: 1 });
+
+    expect(result.current?.query.ok).toBeTruthy();
+    if (!result.current?.query.ok) return;
+
+    expect(result.current.query.data).toEqual({
+      orders: [...ordersFrom20to0, ...ordersFrom20to40],
+    });
+
+    expect(result.current.query.isReachingEnd).toBeFalsy()
+    expect(result.current.query.isReachingStart).toBeFalsy()
+
+    env.addRequestExpectation(API_LIST_ORDERS, {
+      qparam: { delta: -40, wired: "yes", date_ms: 13 },
+      response: {
+        orders: [...ordersFrom20to40, { order_id: '41' }],
+      },
+    });
+
+    await act(() => {
+      if (!result.current?.query.ok) throw Error("not ok");
+      result.current.query.loadMore();
+    });
+    await waitForNextUpdate({ timeout: 1 });
+
+    assertJustExpectedRequestWereMade(env);
+
+    env.addRequestExpectation(API_LIST_ORDERS, {
+      qparam: { delta: 40, wired: "yes", date_ms: 12 },
+      response: {
+        orders: [...ordersFrom0to20, { order_id: '-1' }],
+      },
+    });
+
+    await act(() => {
+      if (!result.current?.query.ok) throw Error("not ok");
+      result.current.query.loadMorePrev();
+    });
+    await waitForNextUpdate({ timeout: 1 });
+    assertJustExpectedRequestWereMade(env);
+
+    expect(result.current.query.data).toEqual({
+      orders: [{ order_id: '-1' }, ...ordersFrom20to0, ...ordersFrom20to40, { 
order_id: '41' }],
+    });
+  });
+
+
+});
diff --git 
a/packages/merchant-backoffice/tests/hooks/swr/product-create.test.tsx 
b/packages/merchant-backoffice/tests/hooks/swr/product-create.test.tsx
deleted file mode 100644
index 8b49b8d..0000000
--- a/packages/merchant-backoffice/tests/hooks/swr/product-create.test.tsx
+++ /dev/null
@@ -1,142 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 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 { renderHook } from "@testing-library/preact-hooks";
-import { act } from "preact/test-utils";
-import * as backend from "../../../src/context/backend";
-import * as instance from "../../../src/context/instance";
-import { MerchantBackend } from "../../../src/declaration";
-import { useInstanceProducts, useProductAPI } from 
"../../../src/hooks/product";
-import {
-  API_CREATE_PRODUCT,
-  API_LIST_PRODUCTS,
-  API_GET_PRODUCT_BY_ID,
-  AxiosMockEnvironment,
-  assertJustExpectedRequestWereMade,
-} from "../../axiosMock";
-
-jest.mock("axios");
-
-describe("product create api", () => {
-  beforeEach(() => {
-    jest
-      .spyOn(backend, "useBackendContext")
-      .mockImplementation(
-        () => ({ url: "http://backend";, token: "token" } as any)
-      );
-    jest
-      .spyOn(instance, "useInstanceContext")
-      .mockImplementation(
-        () => ({ token: "token", id: "default", admin: true } as any)
-      );
-  });
-
-  it("should not have problem with cache after an creation", async () => {
-    const env = new AxiosMockEnvironment();
-
-    env.addRequestExpectation(API_LIST_PRODUCTS, {
-      response: {
-        products: [{ product_id: "1234" }],
-      },
-    });
-
-    env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), {
-      response: { price: "ARS:33" } as MerchantBackend.Products.ProductDetail,
-    });
-
-    const { result, waitForNextUpdate } = renderHook(() => {
-      const api = useProductAPI();
-      const query = useInstanceProducts();
-
-      return { query, api };
-    });
-    if (!result.current) {
-      expect(result.current).toBeDefined();
-      return;
-    }
-    expect(result.current.query.loading).toBeTruthy();
-
-    await waitForNextUpdate(); // first query to list products
-    expect(result.current.query.loading).toBeTruthy();
-
-    await waitForNextUpdate(); // second query to get product details
-    assertJustExpectedRequestWereMade(env);
-
-    expect(result.current.query.loading).toBeFalsy();
-    expect(result.current?.query.ok).toBeTruthy();
-    if (!result.current?.query.ok) return;
-
-    expect(result.current.query.data).toEqual([
-      { id: "1234", price: "ARS:33" },
-    ]);
-
-    env.addRequestExpectation(API_CREATE_PRODUCT, {
-      request: {
-        price: "ARS:3333",
-      } as MerchantBackend.Products.ProductAddDetail,
-    });
-
-    act(async () => {
-      return await result.current?.api.createProduct({
-        price: "ARS:3333",
-      } as any);
-    });
-
-    assertJustExpectedRequestWereMade(env);
-
-    env.addRequestExpectation(API_LIST_PRODUCTS, {
-      response: {
-        products: [{ product_id: "1234" }, { product_id: "2222" }],
-      },
-    });
-
-    env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), {
-      response: { price: "ARS:33" } as MerchantBackend.Products.ProductDetail,
-    });
-    env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), {
-      response: { price: "ARS:33" } as MerchantBackend.Products.ProductDetail,
-    });
-
-    env.addRequestExpectation(API_GET_PRODUCT_BY_ID("2222"), {
-      response: { price: "ARS:3333" } as 
MerchantBackend.Products.ProductDetail,
-    });
-    expect(result.current.query.loading).toBeFalsy();
-
-    await waitForNextUpdate(); // loading product -> products
-    await waitForNextUpdate(); // loading product -> products
-
-    assertJustExpectedRequestWereMade(env);
-
-    expect(result.current.query.loading).toBeFalsy();
-    expect(result.current.query.ok).toBeTruthy();
-
-    expect(result.current.query.data).toEqual([
-      {
-        id: "1234",
-        price: "ARS:33",
-      },
-      {
-        id: "2222",
-        price: "ARS:3333",
-      },
-    ]);
-  });
-});
diff --git 
a/packages/merchant-backoffice/tests/hooks/swr/product-delete.test.tsx 
b/packages/merchant-backoffice/tests/hooks/swr/product-delete.test.tsx
deleted file mode 100644
index 7586af4..0000000
--- a/packages/merchant-backoffice/tests/hooks/swr/product-delete.test.tsx
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 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 { renderHook } from "@testing-library/preact-hooks";
-import { act } from "preact/test-utils";
-import * as backend from "../../../src/context/backend";
-import * as instance from "../../../src/context/instance";
-import { MerchantBackend } from "../../../src/declaration";
-import { useInstanceProducts, useProductAPI } from 
"../../../src/hooks/product";
-import {
-  API_LIST_PRODUCTS,
-  API_GET_PRODUCT_BY_ID,
-  AxiosMockEnvironment,
-  assertNextRequest,
-  assertJustExpectedRequestWereMade,
-} from "../../axiosMock";
-
-jest.mock("axios");
-
-describe("product delete api", () => {
-  beforeEach(() => {
-    jest
-      .spyOn(backend, "useBackendContext")
-      .mockImplementation(
-        () => ({ url: "http://backend";, token: "token" } as any)
-      );
-    jest
-      .spyOn(instance, "useInstanceContext")
-      .mockImplementation(
-        () => ({ token: "token", id: "default", admin: true } as any)
-      );
-  });
-
-  it("should not have problem with cache after a delete", async () => {
-    const env = new AxiosMockEnvironment();
-
-    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 { result, waitForNextUpdate } = renderHook(() => {
-      const query = useInstanceProducts();
-      const api = useProductAPI();
-      return { query, api };
-    });
-
-    await waitForNextUpdate();
-    await waitForNextUpdate();
-    assertJustExpectedRequestWereMade(env);
-
-    expect(result.current?.query.ok).toBeTruthy();
-    if (!result.current?.query.ok) return;
-
-    expect(result.current.query.data).toEqual([
-      { id: "1234", price: "ARS:12" },
-      { id: "2345", price: "ARS:23" },
-    ]);
-
-    env.addRequestExpectation(
-      {
-        delete: "http://backend/instances/default/private/products/1234";,
-      },
-      {}
-    );
-
-    env.addRequestExpectation(API_LIST_PRODUCTS, {
-      response: {
-        products: [{ product_id: "2345" }],
-      },
-    });
-
-    env.addRequestExpectation(API_GET_PRODUCT_BY_ID("2345"), {
-      response: { price: "ARS:23" } as MerchantBackend.Products.ProductDetail,
-    });
-
-    act(async () => {
-      await result.current?.api.deleteProduct("1234");
-    });
-
-    assertNextRequest(env);
-    await waitForNextUpdate();
-    await waitForNextUpdate();
-    assertJustExpectedRequestWereMade(env);
-
-    expect(result.current.query.data).toEqual([
-      {
-        id: "2345",
-        price: "ARS:23",
-      },
-    ]);
-  });
-});
diff --git 
a/packages/merchant-backoffice/tests/hooks/swr/product-details-update.test.tsx 
b/packages/merchant-backoffice/tests/hooks/swr/product-details-update.test.tsx
deleted file mode 100644
index 6b43520..0000000
--- 
a/packages/merchant-backoffice/tests/hooks/swr/product-details-update.test.tsx
+++ /dev/null
@@ -1,113 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 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 { renderHook, act } from "@testing-library/preact-hooks";
-
-import * as backend from "../../../src/context/backend";
-import * as instance from "../../../src/context/instance";
-import { MerchantBackend } from "../../../src/declaration";
-import { useProductAPI, useProductDetails } from "../../../src/hooks/product";
-import {
-  API_GET_PRODUCT_BY_ID,
-  API_UPDATE_PRODUCT_BY_ID,
-  AxiosMockEnvironment,
-  assertNextRequest,
-  assertJustExpectedRequestWereMade,
-} from "../../axiosMock";
-
-jest.mock("axios");
-
-describe("product details api", () => {
-  beforeEach(() => {
-    jest
-      .spyOn(backend, "useBackendContext")
-      .mockImplementation(
-        () => ({ url: "http://backend";, token: "token" } as any)
-      );
-    jest
-      .spyOn(instance, "useInstanceContext")
-      .mockImplementation(
-        () => ({ token: "token", id: "default", admin: true } as any)
-      );
-  });
-
-  it("should not have problem with cache after an update", async () => {
-    const env = new AxiosMockEnvironment();
-
-    env.addRequestExpectation(API_GET_PRODUCT_BY_ID("12"), {
-      response: {
-        description: "this is a description",
-      } as MerchantBackend.Products.ProductDetail,
-    });
-
-    const { result, waitForNextUpdate } = renderHook(() => {
-      const query = useProductDetails("12");
-      const api = useProductAPI();
-      return { query, api };
-    });
-
-    if (!result.current) {
-      expect(result.current).toBeDefined();
-      return;
-    }
-    expect(result.current.query.loading).toBeTruthy();
-    await waitForNextUpdate();
-
-    assertJustExpectedRequestWereMade(env);
-
-    expect(result.current.query.loading).toBeFalsy();
-    expect(result.current?.query.ok).toBeTruthy();
-    if (!result.current?.query.ok) return;
-
-    expect(result.current.query.data).toEqual({
-      description: "this is a description",
-    });
-
-    env.addRequestExpectation(API_UPDATE_PRODUCT_BY_ID("12"), {
-      request: { description: "other description" },
-    });
-
-    env.addRequestExpectation(API_GET_PRODUCT_BY_ID("12"), {
-      response: {
-        description: "other description",
-      } as MerchantBackend.Products.ProductDetail,
-    });
-
-    act(async () => {
-      return await result.current?.api.updateProduct("12", {
-        description: "other description",
-      } as any);
-    });
-
-    assertNextRequest(env);
-    await waitForNextUpdate();
-
-    assertJustExpectedRequestWereMade(env);
-
-    expect(result.current.query.loading).toBeFalsy();
-    expect(result.current?.query.ok).toBeTruthy();
-    if (!result.current?.query.ok) return;
-
-    expect(result.current.query.data).toEqual({
-      description: "other description",
-    });
-  });
-});
diff --git 
a/packages/merchant-backoffice/tests/hooks/swr/product-update.test.tsx 
b/packages/merchant-backoffice/tests/hooks/swr/product-update.test.tsx
deleted file mode 100644
index ca94367..0000000
--- a/packages/merchant-backoffice/tests/hooks/swr/product-update.test.tsx
+++ /dev/null
@@ -1,127 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 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 { renderHook } from "@testing-library/preact-hooks";
-import { act } from "preact/test-utils";
-import * as backend from "../../../src/context/backend";
-import * as instance from "../../../src/context/instance";
-import { MerchantBackend } from "../../../src/declaration";
-import { useInstanceProducts, useProductAPI } from 
"../../../src/hooks/product";
-import {
-  API_GET_PRODUCT_BY_ID,
-  API_LIST_PRODUCTS,
-  API_UPDATE_PRODUCT_BY_ID,
-  AxiosMockEnvironment,
-  assertJustExpectedRequestWereMade,
-  assertNextRequest,
-} from "../../axiosMock";
-
-jest.mock("axios");
-
-describe("product list api", () => {
-  beforeEach(() => {
-    jest
-      .spyOn(backend, "useBackendContext")
-      .mockImplementation(
-        () => ({ url: "http://backend";, token: "token" } as any)
-      );
-    jest
-      .spyOn(instance, "useInstanceContext")
-      .mockImplementation(
-        () => ({ token: "token", id: "default", admin: true } as any)
-      );
-  });
-
-  it("should not have problem with cache after an update", async () => {
-    const env = new AxiosMockEnvironment();
-
-    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 { result, waitForNextUpdate } = renderHook(() => {
-      const query = useInstanceProducts();
-      const api = useProductAPI();
-      return { api, query };
-    }); // get products -> loading
-
-    if (!result.current) {
-      expect(result.current).toBeDefined();
-      return;
-    }
-    expect(result.current.query.loading).toBeTruthy();
-    await waitForNextUpdate();
-
-    await waitForNextUpdate();
-    assertJustExpectedRequestWereMade(env);
-
-    expect(result.current.query.loading).toBeFalsy();
-    expect(result.current.query.ok).toBeTruthy();
-    if (!result.current?.query.ok) return;
-
-    expect(result.current.query.data).toEqual([
-      { id: "1234", price: "ARS:12" },
-    ]);
-
-    env.addRequestExpectation(API_UPDATE_PRODUCT_BY_ID("1234"), {
-      request: { price: "ARS:13" },
-    });
-
-    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,
-    });
-
-    act(async () => {
-      await result.current?.api.updateProduct("1234", {
-        price: "ARS:13",
-      } as any);
-    });
-
-    assertNextRequest(env);
-    await waitForNextUpdate();
-    // await waitForNextUpdate();
-
-    assertJustExpectedRequestWereMade(env);
-    // await waitForNextUpdate();
-
-    expect(result.current.query.loading).toBeFalsy();
-    expect(result.current?.query.ok).toBeTruthy();
-    if (!result.current?.query.ok) return;
-
-    expect(result.current.query.data).toEqual([
-      {
-        id: "1234",
-        price: "ARS:13",
-      },
-    ]);
-  });
-});
diff --git a/packages/merchant-backoffice/tests/hooks/swr/product.test.ts 
b/packages/merchant-backoffice/tests/hooks/swr/product.test.ts
new file mode 100644
index 0000000..5d39a7c
--- /dev/null
+++ b/packages/merchant-backoffice/tests/hooks/swr/product.test.ts
@@ -0,0 +1,338 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 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 { renderHook } from "@testing-library/preact-hooks";
+import { act } from "preact/test-utils";
+import { TestingContext } from ".";
+import { MerchantBackend } from "../../../src/declaration";
+import { useInstanceProducts, useProductAPI, useProductDetails } from 
"../../../src/hooks/product";
+import {
+  API_CREATE_PRODUCT,
+  API_DELETE_PRODUCT, API_GET_PRODUCT_BY_ID,
+  API_LIST_PRODUCTS,
+  API_UPDATE_PRODUCT_BY_ID,
+  assertJustExpectedRequestWereMade,
+  assertNextRequest,
+  AxiosMockEnvironment
+} from "../../axiosMock";
+
+describe("product api interaction with listing ", () => {
+  it("should evict cache when creating a product", async () => {
+    const env = new AxiosMockEnvironment();
+
+    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 { result, waitForNextUpdate } = renderHook(
+      () => {
+        const query = useInstanceProducts();
+        const api = useProductAPI();
+        return { api, query };
+      },
+      { wrapper: TestingContext }
+    ); // get products -> loading
+
+    if (!result.current) {
+      expect(result.current).toBeDefined();
+      return;
+    }
+    expect(result.current.query.loading).toBeTruthy();
+    await waitForNextUpdate({ timeout: 1 });
+
+    await waitForNextUpdate({ timeout: 1 });
+    assertJustExpectedRequestWereMade(env);
+    expect(result.current.query.loading).toBeFalsy();
+    expect(result.current.query.ok).toBeTruthy();
+    if (!result.current?.query.ok) return;
+
+    expect(result.current.query.data).toEqual([
+      { 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,
+    });
+
+    act(async () => {
+      await result.current?.api.createProduct({
+        price: "ARS:23",
+      } as any);
+    });
+
+    assertNextRequest(env);
+    await waitForNextUpdate({ timeout: 1 });
+    await waitForNextUpdate({ timeout: 1 });
+
+    assertJustExpectedRequestWereMade(env);
+
+    expect(result.current.query.loading).toBeFalsy();
+    expect(result.current?.query.ok).toBeTruthy();
+    if (!result.current?.query.ok) return;
+
+    expect(result.current.query.data).toEqual([
+      {
+        id: "1234",
+        price: "ARS:12",
+      },
+      {
+        id: "2345",
+        price: "ARS:23",
+      },
+    ]);
+  });
+
+  it("should evict cache when updating a product", async () => {
+    const env = new AxiosMockEnvironment();
+
+    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 { result, waitForNextUpdate } = renderHook(
+      () => {
+        const query = useInstanceProducts();
+        const api = useProductAPI();
+        return { api, query };
+      },
+      { wrapper: TestingContext }
+    ); // get products -> loading
+
+    if (!result.current) {
+      expect(result.current).toBeDefined();
+      return;
+    }
+    expect(result.current.query.loading).toBeTruthy();
+    await waitForNextUpdate({ timeout: 1 });
+
+    await waitForNextUpdate({ timeout: 1 });
+    assertJustExpectedRequestWereMade(env);
+    expect(result.current.query.loading).toBeFalsy();
+    expect(result.current.query.ok).toBeTruthy();
+    if (!result.current?.query.ok) return;
+
+    expect(result.current.query.data).toEqual([
+      { 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,
+    });
+
+    act(async () => {
+      await result.current?.api.updateProduct("1234", {
+        price: "ARS:13",
+      } as any);
+    });
+
+    assertNextRequest(env);
+    await waitForNextUpdate({ timeout: 1 });
+
+    assertJustExpectedRequestWereMade(env);
+
+    expect(result.current.query.loading).toBeFalsy();
+    expect(result.current?.query.ok).toBeTruthy();
+    if (!result.current?.query.ok) return;
+
+    expect(result.current.query.data).toEqual([
+      {
+        id: "1234",
+        price: "ARS:13",
+      },
+    ]);
+  });
+
+  it("should evict cache when deleting a product", async () => {
+    const env = new AxiosMockEnvironment();
+
+    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 { result, waitForNextUpdate } = renderHook(
+      () => {
+        const query = useInstanceProducts();
+        const api = useProductAPI();
+        return { api, query };
+      },
+      { wrapper: TestingContext }
+    ); // get products -> loading
+
+    if (!result.current) {
+      expect(result.current).toBeDefined();
+      return;
+    }
+    expect(result.current.query.loading).toBeTruthy();
+    await waitForNextUpdate({ timeout: 1 });
+
+    await waitForNextUpdate({ timeout: 1 });
+    // await waitForNextUpdate({ timeout: 1 });
+    assertJustExpectedRequestWereMade(env);
+
+    expect(result.current.query.loading).toBeFalsy();
+    expect(result.current.query.ok).toBeTruthy();
+    if (!result.current?.query.ok) return;
+
+    expect(result.current.query.data).toEqual([
+      { 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:13" } as MerchantBackend.Products.ProductDetail,
+    });
+
+    act(async () => {
+      await result.current?.api.deleteProduct("2345");
+    });
+
+    assertNextRequest(env);
+    await waitForNextUpdate({ timeout: 1 });
+    await waitForNextUpdate({ timeout: 1 });
+
+    assertJustExpectedRequestWereMade(env);
+
+    expect(result.current.query.loading).toBeFalsy();
+    expect(result.current?.query.ok).toBeTruthy();
+    if (!result.current?.query.ok) return;
+
+    expect(result.current.query.data).toEqual([
+      {
+        id: "1234",
+        price: "ARS:13",
+      },
+    ]);
+  });
+
+});
+
+describe("product api interaction with details", () => {
+  it("should evict cache when updating a product", async () => {
+    const env = new AxiosMockEnvironment();
+
+    env.addRequestExpectation(API_GET_PRODUCT_BY_ID("12"), {
+      response: {
+        description: "this is a description",
+      } as MerchantBackend.Products.ProductDetail,
+    });
+
+    const { result, waitForNextUpdate } = renderHook(() => {
+      const query = useProductDetails("12");
+      const api = useProductAPI();
+      return { query, api };
+    }, { wrapper: TestingContext });
+
+    if (!result.current) {
+      expect(result.current).toBeDefined();
+      return;
+    }
+    expect(result.current.query.loading).toBeTruthy();
+    await waitForNextUpdate();
+
+    assertJustExpectedRequestWereMade(env);
+
+    expect(result.current.query.loading).toBeFalsy();
+    expect(result.current?.query.ok).toBeTruthy();
+    if (!result.current?.query.ok) return;
+
+    expect(result.current.query.data).toEqual({
+      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,
+    });
+
+    act(async () => {
+      return await result.current?.api.updateProduct("12", {
+        description: "other description",
+      } as any);
+    });
+
+    assertNextRequest(env);
+    await waitForNextUpdate();
+
+    assertJustExpectedRequestWereMade(env);
+
+    expect(result.current.query.loading).toBeFalsy();
+    expect(result.current?.query.ok).toBeTruthy();
+    if (!result.current?.query.ok) return;
+
+    expect(result.current.query.data).toEqual({
+      description: "other description",
+    });
+  })
+})
\ No newline at end of file
diff --git a/packages/merchant-backoffice/tests/hooks/swr/reserve.test.ts 
b/packages/merchant-backoffice/tests/hooks/swr/reserve.test.ts
index 96810d3..c6c8818 100644
--- a/packages/merchant-backoffice/tests/hooks/swr/reserve.test.ts
+++ b/packages/merchant-backoffice/tests/hooks/swr/reserve.test.ts
@@ -21,34 +21,28 @@
 
 import { renderHook } from "@testing-library/preact-hooks";
 import { act } from "preact/test-utils";
-import * as backend from "../../../src/context/backend";
-import * as instance from "../../../src/context/instance";
 import { MerchantBackend } from "../../../src/declaration";
 import {
   useInstanceReserves,
-  useReservesAPI
+  useReserveDetails,
+  useReservesAPI,
+  useTipDetails,
 } from "../../../src/hooks/reserves";
 import {
-  API_CREATE_RESERVE, API_LIST_RESERVES, assertJustExpectedRequestWereMade, 
AxiosMockEnvironment
+  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,
+  assertJustExpectedRequestWereMade,
+  AxiosMockEnvironment,
 } from "../../axiosMock";
+import { TestingContext } from "./index";
 
-jest.mock("axios");
-
-describe("reserve api", () => {
-  beforeEach(() => {
-    jest
-      .spyOn(backend, "useBackendContext")
-      .mockImplementation(
-        () => ({ url: "http://backend";, token: "token" } as any)
-      );
-    jest
-      .spyOn(instance, "useInstanceContext")
-      .mockImplementation(
-        () => ({ token: "token", id: "default", admin: true } as any)
-      );
-  });
-
-  it("should mutate list cache when creating a reserve", async () => {
+describe("reserve api interaction with listing ", () => {
+  it("should evict cache when creating a reserve", async () => {
     const env = new AxiosMockEnvironment();
 
     env.addRequestExpectation(API_LIST_RESERVES, {
@@ -61,12 +55,15 @@ describe("reserve api", () => {
       },
     });
 
-    const { result, waitForNextUpdate } = renderHook(() => {
-      const api = useReservesAPI();
-      const query = useInstanceReserves();
+    const { result, waitForNextUpdate } = renderHook(
+      () => {
+        const api = useReservesAPI();
+        const query = useInstanceReserves();
 
-      return { query, api };
-    });
+        return { query, api };
+      },
+      { wrapper: TestingContext }
+    );
 
     if (!result.current) {
       expect(result.current).toBeDefined();
@@ -74,7 +71,7 @@ describe("reserve api", () => {
     }
     expect(result.current.query.loading).toBeTruthy();
 
-    await waitForNextUpdate();
+    await waitForNextUpdate({ timeout: 1 });
 
     assertJustExpectedRequestWereMade(env);
 
@@ -124,7 +121,7 @@ describe("reserve api", () => {
 
     expect(result.current.query.loading).toBeFalsy();
 
-    await waitForNextUpdate();
+    await waitForNextUpdate({ timeout: 1 });
 
     assertJustExpectedRequestWereMade(env);
 
@@ -143,6 +140,331 @@ describe("reserve api", () => {
     });
   });
 
+  it("should evict cache when deleting a reserve", async () => {
+    const env = new AxiosMockEnvironment();
+
+    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 { result, waitForNextUpdate } = renderHook(
+      () => {
+        const api = useReservesAPI();
+        const query = useInstanceReserves();
+
+        return { query, api };
+      },
+      {
+        wrapper: TestingContext,
+      }
+    );
+
+    if (!result.current) {
+      expect(result.current).toBeDefined();
+      return;
+    }
+    expect(result.current.query.loading).toBeTruthy();
+
+    await waitForNextUpdate({ timeout: 1 });
+
+    assertJustExpectedRequestWereMade(env);
+
+    expect(result.current.query.loading).toBeFalsy();
+    expect(result.current?.query.ok).toBeTruthy();
+    if (!result.current?.query.ok) return;
+
+    expect(result.current.query.data).toEqual({
+      reserves: [
+        { reserve_pub: "11" },
+        { reserve_pub: "22" },
+        { reserve_pub: "33" },
+      ],
+    });
+
+    env.addRequestExpectation(API_DELETE_RESERVE("11"), {});
+
+    act(async () => {
+      await result.current?.api.deleteReserve("11");
+      return;
+    });
+
+    assertJustExpectedRequestWereMade(env);
+
+    env.addRequestExpectation(API_LIST_RESERVES, {
+      response: {
+        reserves: [
+          {
+            reserve_pub: "22",
+          } as MerchantBackend.Tips.ReserveStatusEntry,
+          {
+            reserve_pub: "33",
+          } as MerchantBackend.Tips.ReserveStatusEntry,
+        ],
+      },
+    });
+
+    expect(result.current.query.loading).toBeFalsy();
+
+    await waitForNextUpdate({ timeout: 1 });
+
+    assertJustExpectedRequestWereMade(env);
+
+    expect(result.current.query.loading).toBeFalsy();
+    expect(result.current.query.ok).toBeTruthy();
+
+    expect(result.current.query.data).toEqual({
+      reserves: [
+        {
+          reserve_pub: "22",
+        } as MerchantBackend.Tips.ReserveStatusEntry,
+        {
+          reserve_pub: "33",
+        } as MerchantBackend.Tips.ReserveStatusEntry,
+      ],
+    });
+  });
+});
+
+describe("reserve api interaction with details", () => {
+  it("should evict cache when adding a tip for a specific reserve", async () 
=> {
+    const env = new AxiosMockEnvironment();
+
+    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,
+    });
+
+    const { result, waitForNextUpdate } = renderHook(
+      () => {
+        const api = useReservesAPI();
+        const query = useReserveDetails("11");
+
+        return { query, api };
+      },
+      {
+        wrapper: TestingContext,
+      }
+    );
+
+    if (!result.current) {
+      expect(result.current).toBeDefined();
+      return;
+    }
+    expect(result.current.query.loading).toBeTruthy();
+
+    await waitForNextUpdate({ timeout: 1 });
+
+    assertJustExpectedRequestWereMade(env);
+
+    expect(result.current.query.loading).toBeFalsy();
+    expect(result.current?.query.ok).toBeTruthy();
+    if (!result.current?.query.ok) return;
+
+    expect(result.current.query.data).toEqual({
+      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_ms: 1 },
+        tip_status_url: "url",
+      },
+    });
+
+    act(async () => {
+      await result.current?.api.authorizeTipReserve("11", {
+        amount: "USD:12",
+        justification: "not",
+        next_url: "http://taler.net";,
+      });
+    });
+
+    assertJustExpectedRequestWereMade(env);
+
+    expect(result.current.query.loading).toBeFalsy();
+
+    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,
+    });
+
+    await waitForNextUpdate({ timeout: 1 });
+
+    assertJustExpectedRequestWereMade(env);
+
+    expect(result.current.query.loading).toBeFalsy();
+    expect(result.current.query.ok).toBeTruthy();
+
+    expect(result.current.query.data).toEqual({
+      payto_uri: "payto://here",
+      tips: [
+        { reason: "why?", tip_id: "id1", total_amount: "USD:10" },
+        { reason: "not", tip_id: "id2", total_amount: "USD:12" },
+      ],
+    });
+  });
+
+  it("should evict cache when adding a tip for a random reserve", async () => {
+    const env = new AxiosMockEnvironment();
+
+    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,
+    });
+
+    const { result, waitForNextUpdate } = renderHook(
+      () => {
+        const api = useReservesAPI();
+        const query = useReserveDetails("11");
+
+        return { query, api };
+      },
+      {
+        wrapper: TestingContext,
+      }
+    );
+
+    if (!result.current) {
+      expect(result.current).toBeDefined();
+      return;
+    }
+    expect(result.current.query.loading).toBeTruthy();
+
+    await waitForNextUpdate({ timeout: 1 });
 
+    assertJustExpectedRequestWereMade(env);
 
+    expect(result.current.query.loading).toBeFalsy();
+    expect(result.current?.query.ok).toBeTruthy();
+    if (!result.current?.query.ok) return;
+
+    expect(result.current.query.data).toEqual({
+      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_ms: 1 },
+        tip_status_url: "url",
+      },
+    });
+
+    act(async () => {
+      await result.current?.api.authorizeTip({
+        amount: "USD:12",
+        justification: "not",
+        next_url: "http://taler.net";,
+      });
+    });
+
+    assertJustExpectedRequestWereMade(env);
+
+    expect(result.current.query.loading).toBeFalsy();
+
+    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,
+    });
+
+    await waitForNextUpdate({ timeout: 1 });
+
+    assertJustExpectedRequestWereMade(env);
+
+    expect(result.current.query.loading).toBeFalsy();
+    expect(result.current.query.ok).toBeTruthy();
+
+    expect(result.current.query.data).toEqual({
+      payto_uri: "payto://here",
+      tips: [
+        { reason: "why?", tip_id: "id1", total_amount: "USD:10" },
+        { reason: "not", tip_id: "id2", total_amount: "USD:12" },
+      ],
+    });
+  });
+});
+
+describe("reserve api interaction with tip details", () => {
+  it("should list tips", async () => {
+    const env = new AxiosMockEnvironment();
+
+    env.addRequestExpectation(API_GET_TIP_BY_ID("11"), {
+      response: {
+        total_picked_up: "USD:12",
+        reason: "not",
+      } as MerchantBackend.Tips.TipDetails,
+    });
+
+    const { result, waitForNextUpdate } = renderHook(
+      () => {
+        // const api = useReservesAPI();
+        const query = useTipDetails("11");
+
+        return { query };
+      },
+      {
+        wrapper: TestingContext,
+      }
+    );
+
+    if (!result.current) {
+      expect(result.current).toBeDefined();
+      return;
+    }
+    expect(result.current.query.loading).toBeTruthy();
+
+    await waitForNextUpdate({ timeout: 1 });
+
+    assertJustExpectedRequestWereMade(env);
+
+    expect(result.current.query.loading).toBeFalsy();
+    expect(result.current?.query.ok).toBeTruthy();
+    if (!result.current?.query.ok) return;
+
+    expect(result.current.query.data).toEqual({
+      total_picked_up: "USD:12",
+      reason: "not",
+    });
+  });
 });
diff --git 
a/packages/merchant-backoffice/tests/hooks/swr/transfer-pagination.test.tsx 
b/packages/merchant-backoffice/tests/hooks/swr/transfer-pagination.test.tsx
deleted file mode 100644
index 34b4ce9..0000000
--- a/packages/merchant-backoffice/tests/hooks/swr/transfer-pagination.test.tsx
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 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 { act, renderHook } from "@testing-library/preact-hooks";
-import * as backend from "../../../src/context/backend";
-import * as instance from "../../../src/context/instance";
-import { useInstanceTransfers } from "../../../src/hooks/transfer";
-import {
-  API_LIST_TRANSFERS,
-  assertNextRequest,
-  assertNoMoreRequestWereMade,
-  AxiosMockEnvironment,
-} from "../../axiosMock";
-
-jest.mock("axios");
-
-describe("transfer pagination", () => {
-  beforeEach(() => {
-    jest
-      .spyOn(backend, "useBackendContext")
-      .mockImplementation(
-        () => ({ url: "http://backend";, token: "token" } as any)
-      );
-    jest
-      .spyOn(instance, "useInstanceContext")
-      .mockImplementation(
-        () => ({ token: "token", id: "default", admin: true } as any)
-      );
-  });
-
-  it("should change pagination", async () => {
-    const env = new AxiosMockEnvironment();
-    env.addRequestExpectation(API_LIST_TRANSFERS, {
-      qparam: { limit: 20, verified: "yes" },
-      response: {
-        transfers: [{ wtid: "1" } as any],
-      },
-    });
-
-    env.addRequestExpectation(API_LIST_TRANSFERS, {
-      qparam: { limit: -20, verified: "yes" },
-      response: {
-        transfers: [{ wtid: "3" } as any],
-      },
-    });
-
-    const updatePosition = (d: string) => {
-      console.log("updatePosition", d);
-    };
-
-    const { result, waitForNextUpdate } = renderHook(() =>
-      useInstanceTransfers({ verified: "yes", position: "" }, updatePosition)
-    );
-
-    assertNextRequest(env);
-    assertNextRequest(env);
-    assertNoMoreRequestWereMade(env);
-
-    await waitForNextUpdate(); // get info of every product, -> loading
-
-    expect(result.current?.ok).toBeTruthy();
-    if (!result.current?.ok) return;
-
-    expect(result.current.data).toEqual({
-      transfers: [{ wtid: "1" }, { wtid: "3" }],
-    });
-
-    env.addRequestExpectation(API_LIST_TRANSFERS, {
-      qparam: { limit: -40, verified: "yes" },
-      response: {
-        transfers: [{ wtid: "4" } as any, { wtid: "3" } as any],
-      },
-    });
-
-    await act(() => {
-      if (!result.current?.ok) throw Error("not ok");
-      result.current.loadMore();
-    });
-    await waitForNextUpdate();
-    assertNextRequest(env);
-    assertNoMoreRequestWereMade(env);
-
-    env.addRequestExpectation(API_LIST_TRANSFERS, {
-      qparam: { limit: 40, verified: "yes" },
-      response: {
-        transfers: [{ wtid: "1" } as any, { wtid: "2" } as any],
-      },
-    });
-
-    await act(() => {
-      if (!result.current?.ok) throw Error("not ok");
-      result.current.loadMorePrev();
-    });
-    await waitForNextUpdate();
-    assertNextRequest(env);
-    assertNoMoreRequestWereMade(env);
-
-    expect(result.current.data).toEqual({
-      transfers: [{ wtid: "2" }, { wtid: "1" }, { wtid: "4" }, { wtid: "3" }],
-    });
-  });
-});
diff --git a/packages/merchant-backoffice/tests/hooks/swr/transfer.test.ts 
b/packages/merchant-backoffice/tests/hooks/swr/transfer.test.ts
new file mode 100644
index 0000000..612cf88
--- /dev/null
+++ b/packages/merchant-backoffice/tests/hooks/swr/transfer.test.ts
@@ -0,0 +1,268 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 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 { act, renderHook } from "@testing-library/preact-hooks";
+import { TestingContext } from "./index";
+import { useInstanceTransfers, useTransferAPI } from 
"../../../src/hooks/transfer";
+import {
+  API_INFORM_TRANSFERS,
+  API_LIST_TRANSFERS,
+  assertJustExpectedRequestWereMade,
+  assertNoMoreRequestWereMade,
+  AxiosMockEnvironment,
+} from "../../axiosMock";
+import { MerchantBackend } from "../../../src/declaration";
+
+describe("transfer api interaction with listing", () => {
+
+  it("should evict cache when informing a transfer", async () => {
+    const env = new AxiosMockEnvironment();
+
+    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 { result, waitForNextUpdate } = renderHook(() => {
+      const moveCursor = (d: string) => {
+        console.log("new position", d);
+      };
+      const query = useInstanceTransfers({}, moveCursor);
+      const api = useTransferAPI();
+
+      return { query, api };
+    }, { wrapper: TestingContext });
+
+    if (!result.current) {
+      expect(result.current).toBeDefined();
+      return;
+    }
+
+    expect(result.current.query.loading).toBeTruthy();
+    await waitForNextUpdate({ timeout: 1 });
+    assertJustExpectedRequestWereMade(env);
+
+    expect(result.current.query.loading).toBeFalsy();
+    expect(result.current.query.ok).toBeTruthy();
+    if (!result.current.query.ok) return;
+
+    expect(result.current.query.data).toEqual({
+      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: [],
+      },
+    });
+
+    act(async () => {
+      await result.current?.api.informTransfer({
+        wtid: '3',
+        credit_amount: 'EUR:1',
+        exchange_url: 'exchange.url',
+        payto_uri: 'payto://'
+      });
+    });
+
+    await waitForNextUpdate({ timeout: 1 });
+    assertJustExpectedRequestWereMade(env);
+
+    expect(result.current.query.loading).toBeFalsy();
+    expect(result.current.query.ok).toBeTruthy();
+    if (!result.current?.query.ok) return;
+
+    expect(result.current.query.data).toEqual({
+      transfers: [{ wtid: "3" }, { wtid: "2" }],
+    });
+  });
+
+});
+
+describe("transfer listing pagination", () => {
+
+  it("should not load more if has reach the end", async () => {
+    const env = new AxiosMockEnvironment();
+    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 { result, waitForNextUpdate } = renderHook(() => {
+      const moveCursor = (d: string) => {
+        console.log("new position", d);
+      };
+      const query = useInstanceTransfers({ payto_uri: 'payto://' }, moveCursor)
+      return { query }
+    }, { wrapper: TestingContext });
+
+    assertJustExpectedRequestWereMade(env);
+
+    await waitForNextUpdate();
+
+    expect(result.current?.query.ok).toBeTruthy();
+    if (!result.current?.query.ok) return;
+
+    expect(result.current.query.data).toEqual({
+      transfers: [{ wtid: "2" }, { wtid: "1" }],
+    });
+
+    expect(result.current.query.isReachingEnd).toBeTruthy()
+    expect(result.current.query.isReachingStart).toBeTruthy()
+
+    await act(() => {
+      if (!result.current?.query.ok) throw Error("not ok");
+      result.current.query.loadMore();
+    });
+    assertNoMoreRequestWereMade(env);
+
+    await act(() => {
+      if (!result.current?.query.ok) throw Error("not ok");
+      result.current.query.loadMorePrev();
+    });
+    assertNoMoreRequestWereMade(env);
+
+    expect(result.current.query.data).toEqual({
+      transfers: [
+        { wtid: "2" },
+        { wtid: "1" },
+      ],
+    });
+  });
+
+  it("should load more if result brings more that PAGE_SIZE", async () => {
+    const env = new AxiosMockEnvironment();
+
+    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://' },
+      response: {
+        transfers: transfersFrom0to20,
+      },
+    });
+
+    env.addRequestExpectation(API_LIST_TRANSFERS, {
+      qparam: { limit: -20, payto_uri: 'payto://' },
+      response: {
+        transfers: transfersFrom20to40,
+      },
+    });
+
+    const { result, waitForNextUpdate } = renderHook(() => {
+      const moveCursor = (d: string) => {
+        console.log("new position", d);
+      };
+      const query = useInstanceTransfers({ payto_uri: 'payto://', position: 
'1' }, moveCursor)
+      return { query }
+    }, { wrapper: TestingContext });
+
+    assertJustExpectedRequestWereMade(env);
+
+    await waitForNextUpdate({ timeout: 1 });
+
+    expect(result.current?.query.ok).toBeTruthy();
+    if (!result.current?.query.ok) return;
+
+    expect(result.current.query.data).toEqual({
+      transfers: [...transfersFrom20to0, ...transfersFrom20to40],
+    });
+
+    expect(result.current.query.isReachingEnd).toBeFalsy()
+    expect(result.current.query.isReachingStart).toBeFalsy()
+
+    env.addRequestExpectation(API_LIST_TRANSFERS, {
+      qparam: { limit: -40, payto_uri: 'payto://', offset: "1" },
+      response: {
+        transfers: [...transfersFrom20to40, { wtid: '41' }],
+      },
+    });
+
+    await act(() => {
+      if (!result.current?.query.ok) throw Error("not ok");
+      result.current.query.loadMore();
+    });
+    await waitForNextUpdate({ timeout: 1 });
+
+    assertJustExpectedRequestWereMade(env);
+
+    env.addRequestExpectation(API_LIST_TRANSFERS, {
+      qparam: { limit: 40, payto_uri: 'payto://', offset: "1" },
+      response: {
+        transfers: [...transfersFrom0to20, { wtid: '-1' }],
+      },
+    });
+
+    await act(() => {
+      if (!result.current?.query.ok) throw Error("not ok");
+      result.current.query.loadMorePrev();
+    });
+    await waitForNextUpdate({ timeout: 1 });
+    assertJustExpectedRequestWereMade(env);
+
+    expect(result.current.query.data).toEqual({
+      transfers: [{ wtid: '-1' }, ...transfersFrom20to0, 
...transfersFrom20to40, { wtid: '41' }],
+    });
+  });
+
+
+});

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]