/*
This file is part of GNU Taler
(C) 2022-2024 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 { PAGE_SIZE } from "../utils.js";
import { useBackendState } from "./backend.js";
import {
AccessToken,
AmountJson,
Amounts,
OperationOk,
TalerBankConversionResultByMethod,
TalerCoreBankErrorsByMethod,
TalerCoreBankResultByMethod,
TalerCorebankApi,
TalerError,
TalerHttpError,
opFixedSuccess,
} from "@gnu-taler/taler-util";
import _useSWR, { SWRHook, mutate } from "swr";
import { useBankCoreApiContext } from "../context/config.js";
import { useState } from "preact/hooks";
// 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 ConversionEstimators = {
estimateByCredit: EstimatorFunction;
estimateByDebit: EstimatorFunction;
};
export function revalidateConversionInfo() {
return mutate(
(key) =>
Array.isArray(key) && key[key.length - 1] === "getConversionInfoAPI",
);
}
export function useConversionInfo() {
const { api, config } = useBankCoreApiContext();
async function fetcher() {
return await api.getConversionInfoAPI().getConfig();
}
const { data, error } = useSWR<
TalerBankConversionResultByMethod<"getConfig">,
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 useCashinEstimator(): ConversionEstimators {
const { api } = useBankCoreApiContext();
return {
estimateByCredit: async (fiatAmount, fee) => {
const resp = await api.getConversionInfoAPI().getCashinRate({
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().getCashinRate({
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 useCashoutEstimator(): ConversionEstimators {
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,
};
},
};
}
/**
* @deprecated use useCashoutEstimator
*/
export function useEstimator(): ConversionEstimators {
return useCashoutEstimator()
}
export function revalidateBusinessAccounts() {
return mutate((key) => Array.isArray(key) && key[key.length - 1] === "getAccounts", undefined, { revalidate: true });
}
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, number]) {
// FIXME: add account name filter
return api.getAccounts(
token,
{},
{
limit: PAGE_SIZE + 1,
offset: String(offset),
order: "asc",
},
);
}
const { data, error } = useSWR<
TalerCoreBankResultByMethod<"getAccounts">,
TalerHttpError
>([token, offset ?? 0, "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 = !offset;
const result = data && data.type == "ok" ? structuredClone(data.body.accounts) : []
if (result.length == PAGE_SIZE + 1) {
result.pop()
}
const pagination = {
result,
isLastPage,
isFirstPage,
loadNext: () => {
if (!result.length) return;
setOffset(result[result.length - 1].row_id);
},
loadFirst: () => {
setOffset(0);
},
};
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 revalidateOnePendingCashouts() {
return mutate(
(key) =>
Array.isArray(key) && key[key.length - 1] === "useOnePendingCashouts", undefined, { revalidate: true }
);
}
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.length > 0 ? list.body.cashouts[0] : undefined;
if (!pendingCashout) return opFixedSuccess(list.httpResp, undefined);
const cashoutInfo = await api.getCashoutById(
{ username, token },
pendingCashout.cashout_id,
);
if (cashoutInfo.type !== "ok") {
return cashoutInfo;
}
return opFixedSuccess(list.httpResp, {
...cashoutInfo.body,
id: pendingCashout.cashout_id,
});
}
const { data, error } = useSWR<
| OperationOk
| TalerCoreBankErrorsByMethod<"getAccountCashouts">
| TalerCoreBankErrorsByMethod<"getCashoutById">,
TalerHttpError
>(
!config.allow_conversion
? undefined
: [account, token, "useOnePendingCashouts"],
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 revalidateCashouts() {
return mutate((key) => Array.isArray(key) && key[key.length - 1] === "useCashouts");
}
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(async (c) => {
const r = await api.getCashoutById({ username, token }, c.cashout_id);
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 }, httpResp: list.httpResp };
}
const { data, error } = useSWR<
| OperationOk<{ cashouts: CashoutWithId[] }>
| TalerCoreBankErrorsByMethod<"getAccountCashouts">,
TalerHttpError
>(
!config.allow_conversion ? undefined : [account, token, "useCashouts"],
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 revalidateCashoutDetails() {
return mutate(
(key) => Array.isArray(key) && key[key.length - 1] === "getCashoutById", undefined, { revalidate: true }
);
}
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<
TalerCoreBankResultByMethod<"getCashoutById">,
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 revalidateLastMonitorInfo() {
return mutate(
(key) => Array.isArray(key) && key[key.length - 1] === "useLastMonitorInfo", undefined, { revalidate: true }
);
}
export function useLastMonitorInfo(
currentMoment: number,
previousMoment: number,
timeframe: TalerCorebankApi.MonitorTimeframeParam,
) {
const { api } = 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;
}