/* 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 { useSessionState } from "./session.js"; import { AccessToken, AmountJson, Amounts, HttpStatusCode, OperationOk, TalerBankConversionResultByMethod, TalerCoreBankErrorsByMethod, TalerCoreBankResultByMethod, TalerCorebankApi, TalerError, TalerHttpError, opFixedSuccess, } from "@gnu-taler/taler-util"; import { useState } from "preact/hooks"; import _useSWR, { SWRHook, mutate } from "swr"; import { useBankCoreApiContext } from "@gnu-taler/web-util/browser"; // 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; } | "amount-is-too-small"; 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 { lib: { conversion }, config } = useBankCoreApiContext(); async function fetcher() { return await conversion.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 { lib: { conversion } } = useBankCoreApiContext(); return { estimateByCredit: async (fiatAmount, fee) => { const resp = await conversion.getCashinRate({ credit: fiatAmount, }); if (resp.type === "fail") { switch (resp.case) { case HttpStatusCode.Conflict: { return "amount-is-too-small"; } // this below can't happen case HttpStatusCode.NotImplemented: //it should not be able to call this function case HttpStatusCode.BadRequest: //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 conversion.getCashinRate({ debit: regionalAmount, }); if (resp.type === "fail") { switch (resp.case) { case HttpStatusCode.Conflict: { return "amount-is-too-small"; } // this below can't happen case HttpStatusCode.NotImplemented: //it should not be able to call this function case HttpStatusCode.BadRequest: //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 { lib: { conversion } } = useBankCoreApiContext(); return { estimateByCredit: async (fiatAmount, fee) => { const resp = await conversion.getCashoutRate({ credit: fiatAmount, }); if (resp.type === "fail") { switch (resp.case) { case HttpStatusCode.Conflict: { return "amount-is-too-small"; } // this below can't happen case HttpStatusCode.NotImplemented: //it should not be able to call this function case HttpStatusCode.BadRequest: //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 conversion.getCashoutRate({ debit: regionalAmount, }); if (resp.type === "fail") { switch (resp.case) { case HttpStatusCode.Conflict: { return "amount-is-too-small"; } // this below can't happen case HttpStatusCode.NotImplemented: //it should not be able to call this function case HttpStatusCode.BadRequest: //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 async function revalidateBusinessAccounts() { return mutate( (key) => Array.isArray(key) && key[key.length - 1] === "getAccounts", undefined, { revalidate: true }, ); } export function useBusinessAccounts() { const { state: credentials } = useSessionState(); const token = credentials.status !== "loggedIn" ? undefined : credentials.token; const { lib: { bank: 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 } = useSessionState(); const { lib: { bank: 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(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< | 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 } = useSessionState(); const { lib: { bank: 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 } }; } 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 } = useSessionState(); const creds = credentials.status !== "loggedIn" ? undefined : credentials; const { lib: { bank: 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 { lib: { bank: api } } = useBankCoreApiContext(); const { state: credentials } = useSessionState(); 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; }