/*
This file is part of GNU Taler
(C) 2022 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
*/
import { useState } from "preact/hooks";
import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils.js";
import { useBackendState } from "./backend.js";
import { AccessToken, AmountJson, AmountString, Amounts, OperationOk, TalerBankConversionResultByMethod, TalerCoreBankErrorsByMethod, TalerCoreBankResultByMethod, TalerCorebankApi, TalerError, TalerHttpError, opFixedSuccess } from "@gnu-taler/taler-util";
import _useSWR, { SWRHook } from "swr";
import { useBankCoreApiContext } from "../context/config.js";
import { assertUnreachable } from "../pages/WithdrawalOperationPage.js";
import { format, getDate, getDay, getHours, getMonth, getYear, set, sub } from "date-fns";
// FIX default import https://github.com/microsoft/TypeScript/issues/49189
const useSWR = _useSWR as unknown as SWRHook;
export type TransferCalculation = {
debit: AmountJson;
credit: AmountJson;
beforeFee: AmountJson;
};
type EstimatorFunction = (
amount: AmountJson,
fee: AmountJson,
) => Promise;
type CashoutEstimators = {
estimateByCredit: EstimatorFunction;
estimateByDebit: EstimatorFunction;
};
export function useConversionInfo() {
const { api, config } = useBankCoreApiContext()
async function fetcher() {
return await api.getConversionInfoAPI().getConfig()
}
const { data, error } = useSWR, TalerHttpError>(
!config.allow_conversion ? undefined : ["getConversionInfoAPI"], fetcher, {
refreshInterval: 0,
refreshWhenHidden: false,
revalidateOnFocus: false,
revalidateOnReconnect: false,
refreshWhenOffline: false,
errorRetryCount: 0,
errorRetryInterval: 1,
shouldRetryOnError: false,
keepPreviousData: true,
});
if (data) return data
if (error) return error;
return undefined;
}
export function useEstimator(): CashoutEstimators {
const { state } = useBackendState();
const { api } = useBankCoreApiContext();
return {
estimateByCredit: async (fiatAmount, fee) => {
const resp = await api.getConversionInfoAPI().getCashoutRate({
credit: fiatAmount
});
if (resp.type === "fail") {
// can't happen
// not-supported: it should not be able to call this function
// wrong-calculation: we are using just one parameter
throw TalerError.fromDetail(resp.detail.code, {}, resp.detail.hint)
}
const credit = Amounts.parseOrThrow(resp.body.amount_credit);
const debit = Amounts.parseOrThrow(resp.body.amount_debit);
const beforeFee = Amounts.sub(credit, fee).amount;
return {
debit,
beforeFee,
credit,
};
},
estimateByDebit: async (regionalAmount, fee) => {
const resp = await api.getConversionInfoAPI().getCashoutRate({
debit: regionalAmount
});
if (resp.type === "fail") {
// can't happen
// not-supported: it should not be able to call this function
// wrong-calculation: we are using just one parameter
throw TalerError.fromDetail(resp.detail.code, {}, resp.detail.hint)
}
const credit = Amounts.parseOrThrow(resp.body.amount_credit);
const debit = Amounts.parseOrThrow(resp.body.amount_debit);
const beforeFee = Amounts.add(credit, fee).amount;
return {
debit,
beforeFee,
credit,
};
},
};
}
export function useBusinessAccounts() {
const { state: credentials } = useBackendState();
const token = credentials.status !== "loggedIn" ? undefined : credentials.token
const { api } = useBankCoreApiContext();
const [offset, setOffset] = useState();
function fetcher([token, offset]: [AccessToken, string]) {
//FIXME: add account name filter
return api.getAccounts(token, {}, {
limit: MAX_RESULT_SIZE,
offset,
order: "asc"
})
}
const { data, error } = useSWR, TalerHttpError>(
[token, offset, "getAccounts"], fetcher, {
refreshInterval: 0,
refreshWhenHidden: false,
revalidateOnFocus: false,
revalidateOnReconnect: false,
refreshWhenOffline: false,
errorRetryCount: 0,
errorRetryInterval: 1,
shouldRetryOnError: false,
keepPreviousData: true,
},
);
const isLastPage =
data && data.type === "ok" && data.body.accounts.length < PAGE_SIZE;
const isFirstPage = false;
const pagination = {
isLastPage,
isFirstPage,
loadMore: () => {
if (isLastPage || data?.type !== "ok") return;
const list = data.body.accounts
if (list.length < MAX_RESULT_SIZE) {
//FIXME: define pagination
// setOffset(list[list.length - 1].row_id);
}
},
loadMorePrev: () => {
null;
},
};
if (data) return { ok: true, data, ...pagination };
if (error) return error;
return undefined;
}
type CashoutWithId = TalerCorebankApi.CashoutStatusResponse & { id: number }
function notUndefined(c: CashoutWithId | undefined): c is CashoutWithId {
return c !== undefined
}
export function useOnePendingCashouts(account: string) {
const { state: credentials } = useBackendState();
const { api, config } = useBankCoreApiContext();
const token = credentials.status !== "loggedIn" ? undefined : credentials.token
async function fetcher([username, token]: [string, AccessToken]) {
const list = await api.getAccountCashouts({ username, token })
if (list.type !== "ok") {
return list;
}
const pendingCashout = list.body.cashouts.find(c => c.status === "pending")
if (!pendingCashout) return opFixedSuccess(undefined)
const cashoutInfo = await api.getCashoutById({ username, token }, pendingCashout?.cashout_id)
if (cashoutInfo.type !== "ok") {
return cashoutInfo;
}
return opFixedSuccess({ ...cashoutInfo.body, id: pendingCashout.cashout_id })
}
const { data, error } = useSWR | TalerCoreBankErrorsByMethod<"getAccountCashouts"> | TalerCoreBankErrorsByMethod<"getCashoutById">, TalerHttpError>(
!config.allow_conversion ? undefined : [account, token, "getAccountCashouts"], fetcher, {
refreshInterval: 0,
refreshWhenHidden: false,
revalidateOnFocus: false,
revalidateOnReconnect: false,
refreshWhenOffline: false,
errorRetryCount: 0,
errorRetryInterval: 1,
shouldRetryOnError: false,
keepPreviousData: true,
});
if (data) return data;
if (error) return error;
return undefined;
}
export function useCashouts(account: string) {
const { state: credentials } = useBackendState();
const { api, config } = useBankCoreApiContext();
const token = credentials.status !== "loggedIn" ? undefined : credentials.token
async function fetcher([username, token]: [string, AccessToken]) {
const list = await api.getAccountCashouts({ username, token })
if (list.type !== "ok") {
return list;
}
const all: Array = await Promise.all(list.body.cashouts.map(c => {
return api.getCashoutById({ username, token }, c.cashout_id).then(r => {
if (r.type === "fail") {
return undefined
}
return { ...r.body, id: c.cashout_id }
})
}))
const cashouts = all.filter(notUndefined)
return { type: "ok" as const, body: { cashouts } }
}
const { data, error } = useSWR | TalerCoreBankErrorsByMethod<"getAccountCashouts">, TalerHttpError>(
!config.allow_conversion ? undefined : [account, token, "getAccountCashouts"], fetcher, {
refreshInterval: 0,
refreshWhenHidden: false,
revalidateOnFocus: false,
revalidateOnReconnect: false,
refreshWhenOffline: false,
errorRetryCount: 0,
errorRetryInterval: 1,
shouldRetryOnError: false,
keepPreviousData: true,
});
if (data) return data;
if (error) return error;
return undefined;
}
export function useCashoutDetails(cashoutId: number | undefined) {
const { state: credentials } = useBackendState();
const creds = credentials.status !== "loggedIn" ? undefined : credentials
const { api } = useBankCoreApiContext();
async function fetcher([username, token, id]: [string, AccessToken, number]) {
return api.getCashoutById({ username, token }, id)
}
const { data, error } = useSWR, TalerHttpError>(
cashoutId === undefined ? undefined : [creds?.username, creds?.token, cashoutId, "getCashoutById"], fetcher, {
refreshInterval: 0,
refreshWhenHidden: false,
revalidateOnFocus: false,
revalidateOnReconnect: false,
refreshWhenOffline: false,
errorRetryCount: 0,
errorRetryInterval: 1,
shouldRetryOnError: false,
keepPreviousData: true,
});
if (data) return data;
if (error) return error;
return undefined;
}
export type MonitorMetrics = {
lastHour: TalerCoreBankResultByMethod<"getMonitor">,
lastDay: TalerCoreBankResultByMethod<"getMonitor">,
lastMonth: TalerCoreBankResultByMethod<"getMonitor">,
}
export type LastMonitor = { current: TalerCoreBankResultByMethod<"getMonitor">, previous: TalerCoreBankResultByMethod<"getMonitor"> }
export function useLastMonitorInfo(currentMoment: number, previousMoment: number, timeframe: TalerCorebankApi.MonitorTimeframeParam) {
const { api, config } = useBankCoreApiContext();
const { state: credentials } = useBackendState();
const token = credentials.status !== "loggedIn" ? undefined : credentials.token
async function fetcher([token, timeframe]: [AccessToken, TalerCorebankApi.MonitorTimeframeParam]) {
const [current, previous] = await Promise.all([
api.getMonitor(token, { timeframe, which: currentMoment }),
api.getMonitor(token, { timeframe, which: previousMoment }),
])
return {
current,
previous,
}
}
const { data, error } = useSWR(
!token ? undefined : [token, timeframe, "useLastMonitorInfo"], fetcher, {
refreshInterval: 0,
refreshWhenHidden: false,
revalidateOnFocus: false,
revalidateOnReconnect: false,
refreshWhenOffline: false,
errorRetryCount: 0,
errorRetryInterval: 1,
shouldRetryOnError: false,
keepPreviousData: true,
});
if (data) return data;
if (error) return error;
return undefined;
}