/*
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 {
Amounts,
HttpStatusCode,
TalerCorebankApi,
TalerError,
TalerErrorCode,
TranslatedString,
encodeCrock,
getRandomBytes,
parsePaytoUri
} from "@gnu-taler/taler-util";
import {
Attention,
Loading,
LocalNotificationBanner,
ShowInputErrorLabel,
notifyInfo,
useLocalNotification,
useTranslationContext
} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useEffect, useState } from "preact/hooks";
import { ErrorLoadingWithDebug } from "../../components/ErrorLoadingWithDebug.js";
import { useBankCoreApiContext } from "../../context/config.js";
import { useAccountDetails } from "../../hooks/access.js";
import { useBackendState } from "../../hooks/backend.js";
import {
useConversionInfo,
useEstimator
} from "../../hooks/circuit.js";
import {
TanChannel,
undefinedIfEmpty
} from "../../utils.js";
import { LoginForm } from "../LoginForm.js";
import { InputAmount, RenderAmount, doAutoFocus } from "../PaytoWireTransferForm.js";
import { assertUnreachable } from "../WithdrawalOperationPage.js";
import { useBankState } from "../../hooks/bank-state.js";
interface Props {
account: string;
focus?: boolean,
onComplete: (id: string) => void;
onCancel?: () => void;
}
type FormType = {
isDebit: boolean;
amount: string;
subject: string;
channel: TanChannel;
};
type ErrorFrom = {
[P in keyof T]+?: string;
};
export function CreateCashout({
account: accountName,
onComplete,
focus,
onCancel,
}: Props): VNode {
const { i18n } = useTranslationContext();
const resultAccount = useAccountDetails(accountName);
const {
estimateByCredit: calculateFromCredit,
estimateByDebit: calculateFromDebit,
} = useEstimator();
const { state: credentials } = useBackendState();
const creds = credentials.status !== "loggedIn" ? undefined : credentials
const [, updateBankState] = useBankState()
const { api, config } = useBankCoreApiContext()
const [form, setForm] = useState>({ isDebit: true, });
const [notification, notify, handleError] = useLocalNotification()
const info = useConversionInfo();
if (!config.allow_conversion) {
return The bank configuration does not support cashout operations.
}
if (!resultAccount) {
return
}
if (resultAccount instanceof TalerError) {
return
}
if (resultAccount.type === "fail") {
switch (resultAccount.case) {
case HttpStatusCode.Unauthorized: return
case HttpStatusCode.NotFound: return
default: assertUnreachable(resultAccount)
}
}
if (!info) {
return
}
if (info instanceof TalerError) {
return
}
const conversionInfo = info.body.conversion_rate
if (!conversionInfo) {
return
conversion enabled but server replied without conversion_rate
}
const account = {
balance: Amounts.parseOrThrow(resultAccount.body.balance.amount),
balanceIsDebit: resultAccount.body.balance.credit_debit_indicator == "debit",
debitThreshold: Amounts.parseOrThrow(resultAccount.body.debit_threshold)
}
const { fiat_currency, regional_currency, fiat_currency_specification, regional_currency_specification } = info.body
const regionalZero = Amounts.zeroOfCurrency(regional_currency);
const fiatZero = Amounts.zeroOfCurrency(fiat_currency);
const limit = account.balanceIsDebit
? Amounts.sub(account.debitThreshold, account.balance).amount
: Amounts.add(account.balance, account.debitThreshold).amount;
const zeroCalc = { debit: regionalZero, credit: fiatZero, beforeFee: fiatZero };
const [calc, setCalc] = useState(zeroCalc);
const sellFee = Amounts.parseOrThrow(conversionInfo.cashout_fee);
const sellRate = conversionInfo.cashout_ratio
/**
* can be in regional currency or fiat currency
* depending on the isDebit flag
*/
const inputAmount = Amounts.parseOrThrow(
`${form.isDebit ? regional_currency : fiat_currency}:${!form.amount ? "0" : form.amount}`,
);
useEffect(() => {
async function doAsync() {
await handleError(async () => {
if (Amounts.isNonZero(inputAmount)) {
const resp = await (form.isDebit ?
calculateFromDebit(inputAmount, sellFee) :
calculateFromCredit(inputAmount, sellFee));
setCalc(resp)
}
})
}
doAsync()
}, [form.amount, form.isDebit]);
const balanceAfter = Amounts.sub(account.balance, calc.debit).amount;
function updateForm(newForm: typeof form): void {
setForm(newForm);
}
const errors = undefinedIfEmpty>({
subject: !form.subject ? i18n.str`required` : undefined,
amount: !form.amount
? i18n.str`required`
: !inputAmount
? i18n.str`could not be parsed`
: Amounts.cmp(limit, calc.debit) === -1
? i18n.str`balance is not enough`
: Amounts.cmp(calc.credit, sellFee) === -1
? i18n.str`need to be higher due to fees`
: Amounts.isZero(calc.credit)
? i18n.str`the total transfer at destination will be zero`
: undefined,
channel: !form.channel ? i18n.str`required` : undefined,
});
const trimmedAmountStr = form.amount?.trim();
async function createCashout() {
const request_uid = encodeCrock(getRandomBytes(32))
await handleError(async () => {
const validChannel = config.supported_tan_channels.length === 0 || form.channel
if (!creds || !form.subject || !validChannel) return;
const resp = await api.createCashout(creds, {
request_uid,
amount_credit: Amounts.stringify(calc.credit),
amount_debit: Amounts.stringify(calc.debit),
subject: form.subject,
tan_channel: form.channel,
})
if (resp.type === "ok") {
notifyInfo(i18n.str`Cashout created`)
} else {
switch (resp.case) {
case HttpStatusCode.Accepted: {
updateBankState("currentChallengeId", resp.body.challenge_id)
return notify({
type: "info",
title: i18n.str`Cashout created but confirmation is required.`,
});
}
case HttpStatusCode.NotFound: return notify({
type: "error",
title: i18n.str`Account not found`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
});
case TalerErrorCode.BANK_TRANSFER_REQUEST_UID_REUSED: return notify({
type: "error",
title: i18n.str`Duplicated request detected, check if the operation succeded or try again.`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
});
case TalerErrorCode.BANK_BAD_CONVERSION: return notify({
type: "error",
title: i18n.str`The exchange rate was incorrectly applied`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
});
case TalerErrorCode.BANK_UNALLOWED_DEBIT: return notify({
type: "error",
title: i18n.str`The account does not have sufficient funds`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
});
case HttpStatusCode.NotImplemented: return notify({
type: "error",
title: i18n.str`Cashouts are not supported`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
});
case TalerErrorCode.BANK_CONFIRM_INCOMPLETE: return notify({
type: "error",
title: i18n.str`Missing cashout URI in the profile`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
});
case TalerErrorCode.BANK_TAN_CHANNEL_SCRIPT_FAILED: return notify({
type: "error",
title: i18n.str`Sending the confirmation message failed, retry later or contact the administrator.`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
});
}
assertUnreachable(resp)
}
})
}
const cashoutDisabled = config.supported_tan_channels.length < 1 || !resultAccount.body.cashout_payto_uri
console.log("disab", cashoutDisabled)
const cashoutAccount = !resultAccount.body.cashout_payto_uri ? undefined :
parsePaytoUri(resultAccount.body.cashout_payto_uri);
const cashoutAccountName = !cashoutAccount ? undefined : cashoutAccount.targetPath
return (
Cashout
Convertion rate
{sellRate}
Balance
Fee
{cashoutAccountName ?
To account
{cashoutAccountName}
:
Before doing a cashout you need to complete your profile