/* 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, mutate } 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 revalidateConversionInfo() { 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, 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 revalidateBusinessAccounts() { mutate(key => Array.isArray(key) && key[key.length - 1] === "getAccounts") } 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 revalidateOnePendingCashouts() { mutate(key => Array.isArray(key) && key[key.length - 1] === "useOnePendingCashouts") } 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, "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() { 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(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, "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() { mutate(key => Array.isArray(key) && key[key.length - 1] === "getCashoutById") } 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 revalidateLastMonitorInfo() { mutate(key => Array.isArray(key) && key[key.length - 1] === "useLastMonitorInfo") } 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; }