commit 3f7e8ab611d02cc2a4b66d1e8619b74c01544b0d
parent 6adddedd819be7514de3448066c95b8eff2314f3
Author: Sebastian <sebasjm@gmail.com>
Date: Thu, 3 Jul 2025 11:08:11 -0300
test conversion for conversion group
Diffstat:
10 files changed, 890 insertions(+), 373 deletions(-)
diff --git a/packages/bank-ui/src/Routing.tsx b/packages/bank-ui/src/Routing.tsx
@@ -325,12 +325,12 @@ const privatePages = {
({ wopid }) => `#/operation/${wopid}`,
),
conversionRateClassCreate: urlPattern(
- /\/new-conversion-rate-class/,
- () => "#/new-conversion-rate-class",
+ /\/new-conversion-rate-group/,
+ () => "#/new-conversion-rate-group",
),
conversionRateClassDetails: urlPattern<{ classId: string }>(
- /\/conversion-rate-class\/(?<classId>[0-9]+)\/details/,
- ({ classId }) => `#/conversion-rate-class/${classId}/details`,
+ /\/conversion-rate-group\/(?<classId>[0-9]+)\/details/,
+ ({ classId }) => `#/conversion-rate-group/${classId}/details`,
),
};
@@ -684,7 +684,7 @@ function PrivateRouting({
case "conversionRateClassCreate": {
return (
<NewConversionRateClass
- onCreated={() => navigateTo(privatePages.home.url({}))}
+ onCreated={(id) => navigateTo(privatePages.conversionRateClassDetails.url({classId: String(id)}))}
routeCancel={privatePages.home}
/>
);
@@ -694,7 +694,6 @@ function PrivateRouting({
if (Number.isNaN(id)) {
return <div>class id is not a number "{location.values.classId}"</div>;
}
- // return <div>2222</div>
return (
<ConversionRateClassDetails
classId={id}
diff --git a/packages/bank-ui/src/hooks/regional.ts b/packages/bank-ui/src/hooks/regional.ts
@@ -36,6 +36,8 @@ import { useState } from "preact/hooks";
import _useSWR, { SWRHook, mutate } from "swr";
import { PAGINATED_LIST_REQUEST } from "../utils.js";
import { buildPaginatedResult } from "./account.js";
+import { TalerBankConversionHttpClient } from "@gnu-taler/taler-util";
+import { assertUnreachable } from "@gnu-taler/taler-util";
// FIX default import https://github.com/microsoft/TypeScript/issues/49189
const useSWR = _useSWR as unknown as SWRHook;
@@ -92,73 +94,157 @@ export function useConversionInfo() {
return undefined;
}
-export function useCashinEstimator(): ConversionEstimators {
+export function useConversionInfoForUser(username: string) {
const {
- lib: { conversion },
+ lib: { conversionForUser },
+ config,
} = 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
- if (resp.detail) {
- throw TalerError.fromUncheckedDetail(resp.detail);
- } else {
- throw TalerError.fromException(
- new Error("failed to get conversion cashin rate"),
- );
- }
- }
- }
- 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,
- };
+
+ async function fetcher() {
+ return await conversionForUser(username).getConfig();
+ }
+ const { data, error } = useSWR<
+ TalerBankConversionResultByMethod<"getConfig">,
+ TalerHttpError
+ >(
+ !config.allow_conversion ? undefined : ["useConversionInfoForUser"],
+ fetcher,
+ {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ errorRetryCount: 0,
+ errorRetryInterval: 1,
+ shouldRetryOnError: false,
+ keepPreviousData: true,
},
- 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
- if (resp.detail) {
- throw TalerError.fromUncheckedDetail(resp.detail);
- } else {
- throw TalerError.fromException(
- new Error("failed to get conversion cashin rate"),
- );
- }
+ );
+
+ if (data) return data;
+ if (error) return error;
+ return undefined;
+}
+
+export function useConversionInfoForClass(classId: number) {
+ const {
+ lib: { conversionForClass },
+ config,
+ } = useBankCoreApiContext();
+
+ async function fetcher() {
+ return await conversionForClass(classId).getConfig();
+ }
+ const { data, error } = useSWR<
+ TalerBankConversionResultByMethod<"getConfig">,
+ TalerHttpError
+ >(
+ !config.allow_conversion ? undefined : ["useConversionInfoForClass"],
+ 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;
+}
+
+function buildEstimatorWithTheBackend(
+ conversion: TalerBankConversionHttpClient,
+ token: AccessToken | undefined,
+ estimation:
+ | "cashin-rate-from-credit"
+ | "cashin-rate-from-debit"
+ | "cashout-rate-from-credit"
+ | "cashout-rate-from-debit",
+): EstimatorFunction {
+ return async (amount, fee) => {
+ let resp;
+ switch (estimation) {
+ case "cashin-rate-from-credit": {
+ resp = await conversion.getCashinRate(token, {
+ credit: amount,
+ });
+ break;
+ }
+ case "cashin-rate-from-debit": {
+ resp = await conversion.getCashinRate(token, {
+ debit: amount,
+ });
+ break;
+ }
+ case "cashout-rate-from-credit": {
+ resp = await conversion.getCashoutRate(token, {
+ credit: amount,
+ });
+ break;
+ }
+ case "cashout-rate-from-debit": {
+ resp = await conversion.getCashoutRate(token, {
+ debit: amount,
+ });
+ break;
+ }
+ default: {
+ assertUnreachable(estimation);
+ }
+ }
+ 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
+ if (resp.detail) {
+ throw TalerError.fromUncheckedDetail(resp.detail);
+ } else {
+ throw TalerError.fromException(
+ new Error("failed to get conversion cashin rate"),
+ );
+ }
}
- 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,
- };
- },
+ }
+ 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,
+ };
+ };
+}
+
+export function useCashinEstimator(): ConversionEstimators {
+ const {
+ lib: { conversion },
+ } = useBankCoreApiContext();
+
+ return {
+ estimateByCredit: buildEstimatorWithTheBackend(
+ conversion,
+ undefined,
+ "cashin-rate-from-credit",
+ ),
+ estimateByDebit: buildEstimatorWithTheBackend(
+ conversion,
+ undefined,
+ "cashin-rate-from-debit",
+ ),
};
}
@@ -166,77 +252,109 @@ 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
- if (resp.detail) {
- throw TalerError.fromUncheckedDetail(resp.detail);
- } else {
- throw TalerError.fromException(
- new Error("failed to get conversion cashout rate"),
- );
- }
- }
- }
- 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
- if (resp.detail) {
- throw TalerError.fromUncheckedDetail(resp.detail);
- } else {
- throw TalerError.fromException(
- new Error("failed to get conversion cashout rate"),
- );
- }
- }
- }
- 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,
- };
- },
+ estimateByCredit: buildEstimatorWithTheBackend(
+ conversion,
+ undefined,
+ "cashout-rate-from-credit",
+ ),
+ estimateByDebit: buildEstimatorWithTheBackend(
+ conversion,
+ undefined,
+ "cashout-rate-from-debit",
+ ),
};
}
-/**
- * @deprecated use useCashoutEstimator
- */
-export function useEstimator(): ConversionEstimators {
- return useCashoutEstimator();
+export function useCashinEstimatorForClass(
+ classId: number,
+): ConversionEstimators {
+ const {
+ lib: { conversionForClass },
+ } = useBankCoreApiContext();
+ const { state } = useSessionState();
+ const token = state.status === "loggedIn" ? state.token : undefined;
+
+ return {
+ estimateByCredit: buildEstimatorWithTheBackend(
+ conversionForClass(classId),
+ token,
+ "cashin-rate-from-credit",
+ ),
+ estimateByDebit: buildEstimatorWithTheBackend(
+ conversionForClass(classId),
+ token,
+ "cashin-rate-from-debit",
+ ),
+ };
+}
+
+export function useCashoutEstimatorForClass(
+ classId: number,
+): ConversionEstimators {
+ const {
+ lib: { conversionForClass },
+ } = useBankCoreApiContext();
+ const { state } = useSessionState();
+ const token = state.status === "loggedIn" ? state.token : undefined;
+ return {
+ estimateByCredit: buildEstimatorWithTheBackend(
+ conversionForClass(classId),
+ token,
+ "cashout-rate-from-credit",
+ ),
+ estimateByDebit: buildEstimatorWithTheBackend(
+ conversionForClass(classId),
+ token,
+ "cashout-rate-from-debit",
+ ),
+ };
+}
+
+export function useCashinEstimatorByUser(
+ username: string,
+): ConversionEstimators {
+ const {
+ lib: { conversionForUser },
+ } = useBankCoreApiContext();
+ const { state } = useSessionState();
+ const token = state.status === "loggedIn" ? state.token : undefined;
+
+ return {
+ estimateByCredit: buildEstimatorWithTheBackend(
+ conversionForUser(username),
+ token,
+ "cashin-rate-from-credit",
+ ),
+ estimateByDebit: buildEstimatorWithTheBackend(
+ conversionForUser(username),
+ token,
+ "cashin-rate-from-debit",
+ ),
+ };
+}
+
+export function useCashoutEstimatorByUser(
+ username: string,
+): ConversionEstimators {
+ const {
+ lib: { conversionForUser },
+ } = useBankCoreApiContext();
+ const { state } = useSessionState();
+ const token = state.status === "loggedIn" ? state.token : undefined;
+ return {
+ estimateByCredit: buildEstimatorWithTheBackend(
+ conversionForUser(username),
+ token,
+ "cashout-rate-from-credit",
+ ),
+ estimateByDebit: buildEstimatorWithTheBackend(
+ conversionForUser(username),
+ token,
+ "cashout-rate-from-debit",
+ ),
+ };
}
export async function revalidateBusinessAccounts() {
@@ -257,14 +375,11 @@ export function useBusinessAccounts() {
const [offset, setOffset] = useState<number | undefined>();
function fetcher([token, aid]: [AccessToken, number]) {
- return api.listAccounts(
- token,
- {
- limit: PAGINATED_LIST_REQUEST,
- offset: aid ? String(aid) : undefined,
- order: "asc",
- },
- );
+ return api.listAccounts(token, {
+ limit: PAGINATED_LIST_REQUEST,
+ offset: aid ? String(aid) : undefined,
+ order: "asc",
+ });
}
const { data, error } = useSWR<
@@ -585,7 +700,8 @@ export function useConversionRateClasses() {
export function revalidateConversionRateClassDetails() {
return mutate(
(key) =>
- Array.isArray(key) && key[key.length - 1] === "useConversionRateClassDetails",
+ Array.isArray(key) &&
+ key[key.length - 1] === "useConversionRateClassDetails",
undefined,
{ revalidate: true },
);
@@ -611,10 +727,12 @@ export function useConversionRateClassDetails(classId: number) {
if (data) return data;
if (error) return error;
return undefined;
-
}
-export function useConversionRateClassUsers(classId: number | undefined, username?: string) {
+export function useConversionRateClassUsers(
+ classId: number | undefined,
+ username?: string,
+) {
const { state: credentials } = useSessionState();
const token =
credentials.status !== "loggedIn" ? undefined : credentials.token;
@@ -624,7 +742,12 @@ export function useConversionRateClassUsers(classId: number | undefined, usernam
const [offset, setOffset] = useState<number | undefined>();
- function fetcher([token, aid, username, classId]: [AccessToken, number, string, number]) {
+ function fetcher([token, aid, username, classId]: [
+ AccessToken,
+ number,
+ string,
+ number,
+ ]) {
return api.listAccounts(token, {
limit: PAGINATED_LIST_REQUEST,
offset: aid ? String(aid) : undefined,
@@ -637,17 +760,21 @@ export function useConversionRateClassUsers(classId: number | undefined, usernam
const { data, error } = useSWR<
TalerCoreBankResultByMethod<"listAccounts">,
TalerHttpError
- >([token, offset ?? 0, username, classId, "useConversionRateClassUsers"], fetcher, {
- refreshInterval: 0,
- refreshWhenHidden: false,
- revalidateOnFocus: false,
- revalidateOnReconnect: false,
- refreshWhenOffline: false,
- errorRetryCount: 0,
- errorRetryInterval: 1,
- shouldRetryOnError: false,
- keepPreviousData: true,
- });
+ >(
+ [token, offset ?? 0, username, classId, "useConversionRateClassUsers"],
+ fetcher,
+ {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ errorRetryCount: 0,
+ errorRetryInterval: 1,
+ shouldRetryOnError: false,
+ keepPreviousData: true,
+ },
+ );
if (error) return error;
if (data === undefined) return undefined;
diff --git a/packages/bank-ui/src/pages/ConversionRateClassDetails.tsx b/packages/bank-ui/src/pages/ConversionRateClassDetails.tsx
@@ -10,6 +10,7 @@ import {
} from "@gnu-taler/taler-util";
import {
Attention,
+ InputText,
InputToggle,
Loading,
LocalNotificationBanner,
@@ -21,7 +22,7 @@ import {
useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { Fragment, h, VNode } from "preact";
-import { useState } from "preact/hooks";
+import { useState, useEffect } from "preact/hooks";
import { ErrorLoadingWithDebug } from "../components/ErrorLoadingWithDebug.js";
import {
FormErrors,
@@ -30,14 +31,21 @@ import {
useFormState,
} from "../hooks/form.js";
import {
+ TransferCalculation,
+ useCashinEstimator,
+ useCashinEstimatorForClass,
+ useCashoutEstimator,
+ useCashoutEstimatorForClass,
useConversionInfo,
+ useConversionInfoForClass,
useConversionRateClassDetails,
useConversionRateClassUsers,
} from "../hooks/regional.js";
import { useSessionState } from "../hooks/session.js";
import { RecursivePartial, undefinedIfEmpty } from "../utils.js";
-import { doAutoFocus } from "./PaytoWireTransferForm.js";
+import { doAutoFocus, InputAmount } from "./PaytoWireTransferForm.js";
import { ConversionForm } from "./regional/ConversionConfig.js";
+import { AmountJson } from "@gnu-taler/taler-util";
interface Props {
classId: number;
@@ -123,8 +131,8 @@ function Form({
const [notification, notify, handleError] = useLocalNotification();
const [section, setSection] = useState<
- "detail" | "cashout" | "cashin" | "users"
- >("detail");
+ "detail" | "cashout" | "cashin" | "users" | "test"
+ >("test");
const initalState: FormValues<FormType> = {
name: detailsResult.name,
@@ -212,7 +220,6 @@ function Form({
? undefined
: doUpdateClass;
- console.log("ERROR", status.errors);
const default_rate = conversionInfo.conversion_rate;
const final_cashin_ratio =
@@ -244,7 +251,7 @@ function Form({
<div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-6 md:grid-cols-3 bg-gray-100 my-4 px-4 pb-4 rounded-lg">
<div class="px-4 sm:px-0">
<h2 class="text-base font-semibold leading-7 text-gray-900">
- <i18n.Translate>Conversion</i18n.Translate>
+ <i18n.Translate>Conversion rate group</i18n.Translate>
</h2>
<div class="px-2 mt-2 grid grid-cols-1 gap-y-4 sm:gap-x-4">
<label
@@ -341,6 +348,29 @@ function Form({
</span>
</span>
</label>
+ <label
+ data-enabled={section === "test"}
+ class="relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-none border-gray-300 data-[enabled=true]:border-indigo-600 data-[enabled=true]:ring-2 data-[enabled=true]:ring-indigo-600"
+ >
+ <input
+ type="radio"
+ name="project-type"
+ value="Newsletter"
+ class="sr-only"
+ aria-labelledby="project-type-0-label"
+ aria-describedby="project-type-0-description-0 project-type-0-description-1"
+ onChange={() => {
+ setSection("test");
+ }}
+ />
+ <span class="flex flex-1">
+ <span class="flex flex-col">
+ <span class="block text-sm font-medium text-gray-900">
+ <i18n.Translate>Test</i18n.Translate>
+ </span>
+ </span>
+ </span>
+ </label>
</div>
</div>
@@ -536,6 +566,8 @@ function Form({
<AccountsOnConversionClass classId={classId} />
)}
+ {section == "test" && <TestConversionClass classId={classId} />}
+
<div class="flex items-center justify-between mt-4 gap-x-6 border-t border-gray-900/10 px-4 py-4">
<a
name="cancel"
@@ -706,10 +738,216 @@ export function createFormValidator(
};
}
+function TestConversionClass({ classId }: { classId: number }): VNode {
+ const { i18n } = useTranslationContext();
+ const [notification, notify, handleError] = useLocalNotification();
+
+ const result = useConversionInfoForClass(classId);
+ const info =
+ result && !(result instanceof TalerError) && result.type === "ok"
+ ? result.body
+ : undefined;
+
+ const { estimateByDebit: calculateCashoutFromDebit } = useCashoutEstimatorForClass(classId);
+ const { estimateByDebit: calculateCashinFromDebit } = useCashinEstimatorForClass(classId);
+
+ const [amount, setAmount] = useState<string>("100");
+ const [error, setError] = useState<string>();
+
+ const [calculationResult, setCalc] = useState<{
+ cashin: TransferCalculation;
+ cashout: TransferCalculation;
+ }>();
+
+ useEffect(() => {
+ async function doAsync() {
+ await handleError(async () => {
+ if (!info) return;
+ if (!amount || error) return;
+ const in_amount = Amounts.parseOrThrow(
+ `${info.fiat_currency}:${amount}`,
+ );
+ const in_fee = Amounts.parseOrThrow(info.conversion_rate.cashin_fee);
+ const cashin = await calculateCashinFromDebit(in_amount, in_fee);
+
+ if (cashin === "amount-is-too-small") {
+ setCalc(undefined);
+ return;
+ }
+
+ const out_fee = Amounts.parseOrThrow(info.conversion_rate.cashout_fee);
+ const cashout = await calculateCashoutFromDebit(cashin.credit, out_fee);
+
+ setCalc({ cashin, cashout });
+ });
+ }
+ doAsync();
+ }, [amount]);
+
+ if (!info) {
+ return <Loading />;
+ }
+
+ const cashinCalc =
+ calculationResult?.cashin === "amount-is-too-small"
+ ? undefined
+ : calculationResult?.cashin;
+ const cashoutCalc =
+ calculationResult?.cashout === "amount-is-too-small"
+ ? undefined
+ : calculationResult?.cashout;
+
+ return (
+ <Fragment>
+ <div class="px-6 pt-6">
+ <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
+ <div class="sm:col-span-5">
+ <label
+ for="amount"
+ class="block text-sm font-medium leading-6 text-gray-900"
+ >{i18n.str`Initial amount`}</label>
+ <InputAmount
+ name="amount"
+ left
+ currency={info.fiat_currency}
+ value={amount ?? ""}
+ onChange={(d) => {
+ setAmount(d);
+ }}
+ />
+ <ShowInputErrorLabel
+ message={error}
+ isDirty={amount !== undefined}
+ />
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ Use it to test how the conversion will affect the amount.
+ </i18n.Translate>
+ </p>
+ </div>
+ </div>
+ </div>
+
+ {!cashoutCalc || !cashinCalc ? undefined : (
+ <div class="px-6 pt-6">
+ <div class="sm:col-span-5">
+ <dl class="mt-4 space-y-4">
+ <div class="justify-between items-center flex ">
+ <dt class="text-sm text-gray-600">
+ <i18n.Translate>Sending to this bank</i18n.Translate>
+ </dt>
+ <dd class="text-sm text-gray-900">
+ <RenderAmount
+ value={cashinCalc.debit}
+ negative
+ withColor
+ spec={info.fiat_currency_specification}
+ />
+ </dd>
+ </div>
+
+ {Amounts.isZero(cashinCalc.beforeFee) ? undefined : (
+ <div class="flex items-center justify-between afu ">
+ <dt class="flex items-center text-sm text-gray-600">
+ <span>
+ <i18n.Translate>Converted</i18n.Translate>
+ </span>
+ </dt>
+ <dd class="text-sm text-gray-900">
+ <RenderAmount
+ value={cashinCalc.beforeFee}
+ spec={info.regional_currency_specification}
+ />
+ </dd>
+ </div>
+ )}
+ <div class="flex justify-between items-center border-t-2 afu pt-4">
+ <dt class="text-lg text-gray-900 font-medium">
+ <i18n.Translate>Cashin after fee</i18n.Translate>
+ </dt>
+ <dd class="text-lg text-gray-900 font-medium">
+ <RenderAmount
+ value={cashinCalc.credit}
+ withColor
+ spec={info.regional_currency_specification}
+ />
+ </dd>
+ </div>
+ </dl>
+ </div>
+
+ <div class="sm:col-span-5">
+ <dl class="mt-4 space-y-4">
+ <div class="justify-between items-center flex ">
+ <dt class="text-sm text-gray-600">
+ <i18n.Translate>Sending from this bank</i18n.Translate>
+ </dt>
+ <dd class="text-sm text-gray-900">
+ <RenderAmount
+ value={cashoutCalc.debit}
+ negative
+ withColor
+ spec={info.regional_currency_specification}
+ />
+ </dd>
+ </div>
+
+ {Amounts.isZero(cashoutCalc.beforeFee) ? undefined : (
+ <div class="flex items-center justify-between afu">
+ <dt class="flex items-center text-sm text-gray-600">
+ <span>
+ <i18n.Translate>Converted</i18n.Translate>
+ </span>
+ </dt>
+ <dd class="text-sm text-gray-900">
+ <RenderAmount
+ value={cashoutCalc.beforeFee}
+ spec={info.fiat_currency_specification}
+ />
+ </dd>
+ </div>
+ )}
+ <div class="flex justify-between items-center border-t-2 afu pt-4">
+ <dt class="text-lg text-gray-900 font-medium">
+ <i18n.Translate>Cashout after fee</i18n.Translate>
+ </dt>
+ <dd class="text-lg text-gray-900 font-medium">
+ <RenderAmount
+ value={cashoutCalc.credit}
+ withColor
+ spec={info.fiat_currency_specification}
+ />
+ </dd>
+ </div>
+ </dl>
+ </div>
+
+ {/* {cashoutCalc &&
+ error === undefined &&
+ Amounts.cmp(status.result.amount, cashoutCalc.credit) < 0 ? (
+ <div class="p-4">
+ <Attention title={i18n.str`Bad configuration`} type="warning">
+ <i18n.Translate>
+ This configuration allows users to cash out more of what has
+ been cashed in.
+ </i18n.Translate>
+ </Attention>
+ </div>
+ ) : undefined} */}
+ </div>
+ )}
+ </Fragment>
+ );
+}
function AccountsOnConversionClass({ classId }: { classId: number }): VNode {
const { i18n } = useTranslationContext();
- const [filter, setFilter] = useState<{ classId?: number; account?: string }>({
+ const [filter, setFilter] = useState<{
+ showAll?: boolean;
+ classId?: number;
+ account?: string;
+ }>({
+ showAll: classId === undefined,
classId,
});
const userListResult = useConversionRateClassUsers(
@@ -739,29 +977,59 @@ function AccountsOnConversionClass({ classId }: { classId: number }): VNode {
}
return (
<Fragment>
- <div class="px-4 mt-8">
+ <div class="px-4 mt-4">
<div class="sm:flex sm:items-center">
<div class="sm:flex-auto">
<h1 class="text-base font-semibold leading-6 text-gray-900">
- <i18n.Translate>Accounts</i18n.Translate>
+ <i18n.Translate>Filters</i18n.Translate>
</h1>
</div>
</div>
</div>
- <div class="px-4 mt-8">
+ <div class="px-4 mt-2">
<InputToggle
- label={i18n.str`Show all`}
+ label={i18n.str`Show from other groups`}
name="show_all"
threeState={false}
handler={{
- value: filter.classId === undefined,
+ value: filter.showAll,
onChange(v) {
- filter.classId = !v ? classId : undefined;
+ filter.showAll = !!v;
+ if (!v) {
+ filter.classId = classId;
+ }
setFilter(structuredClone(filter));
},
name: "show_all",
}}
/>
+ <InputText
+ label={i18n.str`Account`}
+ name="account"
+ handler={{
+ value: filter.account,
+ onChange(v) {
+ filter.account = v;
+ setFilter(structuredClone(filter));
+ },
+ name: "account",
+ }}
+ />
+ {filter.showAll ? (
+ <InputText
+ label={i18n.str`Group ID`}
+ name="crcid"
+ handler={{
+ value: String(filter.classId),
+ onChange(v) {
+ const id = !v ? undefined : Number.parseInt(v, 10);
+ filter.classId = id;
+ setFilter(structuredClone(filter));
+ },
+ name: "crcid",
+ }}
+ />
+ ) : undefined}
</div>
<div class="mt-4 flow-root">
<div class="overflow-x-auto">
@@ -778,16 +1046,16 @@ function AccountsOnConversionClass({ classId }: { classId: number }): VNode {
<tr>
<th
scope="col"
- class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-0"
- >{i18n.str`Username`}</th>
- <th
- scope="col"
class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
>{i18n.str`Name`}</th>
<th
scope="col"
class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
>{i18n.str`Conversion rate`}</th>
+ <th
+ scope="col"
+ class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
+ >{i18n.str`Action`}</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
@@ -798,15 +1066,22 @@ function AccountsOnConversionClass({ classId }: { classId: number }): VNode {
class="data-[status=deleted]:bg-gray-100"
data-status={item.status}
>
- <td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-0">
- {item.username}
- </td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
{item.name}
</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
{"<pending>"}
</td>
+ <td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
+ <a
+ href=""
+ class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600"
+ >
+ <i18n.Translate>
+ Quit from this group
+ </i18n.Translate>
+ </a>
+ </td>
</tr>
);
})}
diff --git a/packages/bank-ui/src/pages/NewConversionRateClass.tsx b/packages/bank-ui/src/pages/NewConversionRateClass.tsx
@@ -17,7 +17,7 @@ import { TalerErrorCode } from "@gnu-taler/taler-util";
interface Props {
routeCancel: RouteDefinition;
- onCreated: () => void;
+ onCreated: (id: number) => void;
}
export function NewConversionRateClass({
routeCancel,
@@ -43,7 +43,7 @@ export function NewConversionRateClass({
const resp = await api.createConversionRateClass(token, submitData);
if (resp.type === "ok") {
notifyInfo(i18n.str`Conversion rate class created.`);
- onCreated();
+ onCreated(resp.body.conversion_rate_class_id);
return;
}
switch (resp.case) {
@@ -75,17 +75,11 @@ export function NewConversionRateClass({
<div class="px-4 sm:px-0">
<h2 class="text-base font-semibold leading-7 text-gray-900">
- <i18n.Translate>New conversion rate class</i18n.Translate>
+ <i18n.Translate>New conversion rate group</i18n.Translate>
</h2>
</div>
- <ConversionRateClassForm
- template={undefined}
- purpose="create"
- onChange={(a) => {
- setSubmitData(a);
- }}
- >
+ <ConversionRateClassForm onChange={setSubmitData}>
<div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8">
<a
href={routeCancel.url({})}
diff --git a/packages/bank-ui/src/pages/admin/ConversionClassList.tsx b/packages/bank-ui/src/pages/admin/ConversionClassList.tsx
@@ -31,8 +31,12 @@ import {
} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { ErrorLoadingWithDebug } from "../../components/ErrorLoadingWithDebug.js";
-import { useConversionRateClasses } from "../../hooks/regional.js";
+import {
+ useConversionInfo,
+ useConversionRateClasses,
+} from "../../hooks/regional.js";
import { RenderAmount } from "../PaytoWireTransferForm.js";
+import { CurrencySpecification } from "@gnu-taler/taler-util";
const TALER_SCREEN_ID = 121;
@@ -47,8 +51,16 @@ export function ConversionClassList({
}: Props): VNode {
const result = useConversionRateClasses();
const { i18n } = useTranslationContext();
- const { config } = useBankCoreApiContext();
+ const resultInfo = useConversionInfo();
+
+ const convInfo =
+ !resultInfo || resultInfo instanceof Error || resultInfo.type === "fail"
+ ? undefined
+ : resultInfo.body;
+ if (!convInfo) {
+ return <Fragment>-</Fragment>;
+ }
if (!result) {
return <Loading />;
}
@@ -93,42 +105,13 @@ export function ConversionClassList({
const classes = result.body;
- function DescribeRatio({
- fee,
- min,
- ratio,
- rounding,
- }: {
- min?: AmountString;
- ratio?: DecimalNumber;
- fee?: AmountString;
- rounding?: RoundingMode;
- }): VNode {
- const { config } = useBankCoreApiContext();
-
- if (!rounding || !ratio) {
- return <Fragment>-</Fragment>;
- }
- return (
- <Fragment>
- {ratio}
- {!min ? undefined : (
- <RenderAmount
- spec={config.currency_specification}
- value={Amounts.parseOrThrow(min)}
- />
- )}
- </Fragment>
- );
- }
-
return (
<Fragment>
<div class="px-4 sm:px-6 lg:px-8 mt-8">
<div class="sm:flex sm:items-center">
<div class="sm:flex-auto">
<h1 class="text-base font-semibold leading-6 text-gray-900">
- <i18n.Translate>Conversion classes</i18n.Translate>
+ <i18n.Translate>Conversion rate groups</i18n.Translate>
</h1>
</div>
<div class="mt-4 sm:ml-16 sm:mt-0 sm:flex-none">
@@ -138,7 +121,7 @@ export function ConversionClassList({
type="button"
class="block rounded-md bg-indigo-600 px-3 py-2 text-center text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
>
- <i18n.Translate>New conversion class</i18n.Translate>
+ <i18n.Translate>Create conversion rate group</i18n.Translate>
</a>
</div>
</div>
@@ -147,7 +130,7 @@ export function ConversionClassList({
<div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
{!classes.length ? (
<div>
- <i18n.Translate>No conversion classes</i18n.Translate>
+ <i18n.Translate>No conversion rate group</i18n.Translate>
</div>
) : (
<table class="min-w-full divide-y divide-gray-300">
@@ -175,7 +158,7 @@ export function ConversionClassList({
{classes.map((row, idx) => {
return (
<tr key={idx} class="">
- <td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-0">
+ <td class="whitespace-nowrap py-3 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-0">
<a
href={routeShowDetails.url({
classId: String(row.conversion_rate_class_id),
@@ -185,23 +168,73 @@ export function ConversionClassList({
</a>
</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
- {row.description}
+ <a
+ href={routeShowDetails.url({
+ classId: String(row.conversion_rate_class_id),
+ })}
+ >
+ {row.description}
+ </a>
</td>
- <td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
- <DescribeRatio
- ratio={row.cashin_ratio}
- fee={row.cashin_fee}
- min={row.cashin_min_amount}
- rounding={row.cashin_rounding_mode}
- />
+ <td class="whitespace-nowrap px-3 py-2 text-sm text-gray-500">
+ <a
+ href={routeShowDetails.url({
+ classId: String(row.conversion_rate_class_id),
+ })}
+ >
+ <DescribeRatio
+ ratio={
+ row.cashin_ratio ??
+ convInfo.conversion_rate.cashin_ratio
+ }
+ fee={
+ row.cashin_fee ??
+ convInfo.conversion_rate.cashin_fee
+ }
+ min={
+ row.cashin_min_amount ??
+ convInfo.conversion_rate.cashin_min_amount
+ }
+ rounding={
+ row.cashin_rounding_mode ??
+ convInfo.conversion_rate.cashin_rounding_mode
+ }
+ minSpec={convInfo.fiat_currency_specification}
+ feeSpec={
+ convInfo.regional_currency_specification
+ }
+ />
+ </a>
</td>
- <td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
- <DescribeRatio
- ratio={row.cashout_ratio}
- fee={row.cashout_fee}
- min={row.cashout_min_amount}
- rounding={row.cashout_rounding_mode}
- />
+ <td class="whitespace-nowrap px-3 py-2 text-sm text-gray-500">
+ <a
+ href={routeShowDetails.url({
+ classId: String(row.conversion_rate_class_id),
+ })}
+ >
+ <DescribeRatio
+ ratio={
+ row.cashout_ratio ??
+ convInfo.conversion_rate.cashout_ratio
+ }
+ fee={
+ row.cashout_fee ??
+ convInfo.conversion_rate.cashout_fee
+ }
+ min={
+ row.cashout_min_amount ??
+ convInfo.conversion_rate.cashout_min_amount
+ }
+ rounding={
+ row.cashout_rounding_mode ??
+ convInfo.conversion_rate.cashout_rounding_mode
+ }
+ minSpec={
+ convInfo.regional_currency_specification
+ }
+ feeSpec={convInfo.fiat_currency_specification}
+ />
+ </a>
</td>
</tr>
);
@@ -239,3 +272,41 @@ export function ConversionClassList({
</Fragment>
);
}
+
+function DescribeRatio({
+ fee,
+ min,
+ ratio,
+ rounding,
+ feeSpec,
+ minSpec,
+}: {
+ min: AmountString;
+ ratio: DecimalNumber;
+ fee: AmountString;
+ rounding: RoundingMode;
+ minSpec: CurrencySpecification;
+ feeSpec: CurrencySpecification;
+}): VNode {
+ const { i18n } = useTranslationContext();
+
+ return (
+ <Fragment>
+ 1:{ratio}
+ {Amounts.isZero(min) ? undefined : (
+ <Fragment>
+ <br />
+ <i18n.Translate>min:</i18n.Translate>
+ <RenderAmount spec={minSpec} value={Amounts.parseOrThrow(min)} />
+ </Fragment>
+ )}
+ {Amounts.isZero(fee) ? undefined : (
+ <Fragment>
+ <br />
+ <i18n.Translate>fee:</i18n.Translate>
+ <RenderAmount spec={feeSpec} value={Amounts.parseOrThrow(fee)} />
+ </Fragment>
+ )}
+ </Fragment>
+ );
+}
diff --git a/packages/bank-ui/src/pages/admin/ConversionRateClassForm.tsx b/packages/bank-ui/src/pages/admin/ConversionRateClassForm.tsx
@@ -20,26 +20,20 @@ import {
RoundingMode,
TalerCorebankApi,
TranslatedString,
- assertUnreachable
+ assertUnreachable,
} from "@gnu-taler/taler-util";
import {
InputToggle,
ShowInputErrorLabel,
useBankCoreApiContext,
- useTranslationContext
+ useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { ComponentChildren, VNode, h } from "preact";
import { useState } from "preact/hooks";
import { Fragment } from "preact/jsx-runtime";
import { useSessionState } from "../../hooks/session.js";
-import {
- ErrorMessageMappingFor,
- undefinedIfEmpty
-} from "../../utils.js";
-import {
- InputAmount,
- doAutoFocus
-} from "../PaytoWireTransferForm.js";
+import { ErrorMessageMappingFor, undefinedIfEmpty } from "../../utils.js";
+import { InputAmount, doAutoFocus } from "../PaytoWireTransferForm.js";
const TALER_SCREEN_ID = 120;
@@ -64,114 +58,111 @@ export type ConversionRateClassFormData = {
cashout_rounding_mode?: RoundingMode;
};
-type ChangeByPurposeType = {
- create: (a: TalerCorebankApi.ConversionRateClassInput | undefined) => void;
- update: (a: TalerCorebankApi.ConversionRateClassInput | undefined) => void;
- show: undefined;
-};
+// type ChangeByPurposeType = {
+// create: (a: TalerCorebankApi.ConversionRateClassInput | undefined) => void;
+// update: (a: TalerCorebankApi.ConversionRateClassInput | undefined) => void;
+// show: undefined;
+// };
/**
*
* @param param0
* @returns
*/
-export function ConversionRateClassForm<
- PurposeType extends keyof ChangeByPurposeType,
->({
- template,
- username,
- purpose,
- onChange,
- focus,
- children,
-}: {
- focus?: boolean;
- children: ComponentChildren;
- username?: string;
- template: TalerCorebankApi.ConversionRateClass | undefined;
- onChange: ChangeByPurposeType[PurposeType];
- purpose: PurposeType;
-}): VNode {
- const { config, url } = useBankCoreApiContext();
+export function ConversionRateClassForm(
+ // <
+ // PurposeType extends keyof ChangeByPurposeType,
+ // >
+ {
+ onChange,
+ focus,
+ children,
+ }: {
+ focus?: boolean;
+ children: ComponentChildren;
+ // onChange: ChangeByPurposeType[PurposeType];
+ onChange: (
+ a: TalerCorebankApi.ConversionRateClassInput | undefined,
+ ) => void;
+ },
+): VNode {
+ // const { config, url } = useBankCoreApiContext();
const { i18n } = useTranslationContext();
const { state: credentials } = useSessionState();
const [form, setForm] = useState<ConversionRateClassFormData>({});
const [errors, setErrors] = useState<
- ErrorMessageMappingFor<typeof defaultValue> | undefined
+ ErrorMessageMappingFor<ConversionRateClassFormData> | undefined
>(undefined);
- const defaultValue: ConversionRateClassFormData = {
- cashin_fee: Amounts.stringifyValue(
- template?.cashin_fee ?? `${config.currency}:0`,
- ),
- cashin_min_amount: Amounts.stringifyValue(
- template?.cashin_min_amount ?? `${config.currency}:0`,
- ),
- cashout_fee: Amounts.stringifyValue(
- template?.cashout_fee ?? `${config.currency}:0`,
- ),
- cashin_ratio: template?.cashin_ratio ?? "0",
- cashin_rounding_mode: template?.cashin_rounding_mode,
+ // const defaultValue: ConversionRateClassFormData = {
+ // // cashin_fee: Amounts.stringifyValue(
+ // // template?.cashin_fee ?? `${config.currency}:0`,
+ // // ),
+ // // cashin_min_amount: Amounts.stringifyValue(
+ // // template?.cashin_min_amount ?? `${config.currency}:0`,
+ // // ),
+ // // cashout_fee: Amounts.stringifyValue(
+ // // template?.cashout_fee ?? `${config.currency}:0`,
+ // // ),
+ // // cashin_ratio: template?.cashin_ratio ?? "0",
+ // // cashin_rounding_mode: template?.cashin_rounding_mode,
- cashout_min_amount: Amounts.stringifyValue(
- template?.cashout_min_amount ?? `${config.currency}:0`,
- ),
- cashout_ratio: template?.cashout_ratio ?? "0",
- cashout_rounding_mode: template?.cashout_rounding_mode,
+ // // cashout_min_amount: Amounts.stringifyValue(
+ // // template?.cashout_min_amount ?? `${config.currency}:0`,
+ // // ),
+ // // cashout_ratio: template?.cashout_ratio ?? "0",
+ // // cashout_rounding_mode: template?.cashout_rounding_mode,
- cashin_enabled:
- template?.cashin_ratio !== undefined &&
- Number.parseInt(template.cashin_ratio, 10) > 0,
+ // // cashin_enabled:
+ // // template?.cashin_ratio !== undefined &&
+ // // Number.parseInt(template.cashin_ratio, 10) > 0,
- cashout_enabled:
- template?.cashout_ratio !== undefined &&
- Number.parseInt(template.cashout_ratio, 10) > 0,
+ // // cashout_enabled:
+ // // template?.cashout_ratio !== undefined &&
+ // // Number.parseInt(template.cashout_ratio, 10) > 0,
- name: template?.name,
- description: template?.description,
- };
+ // name: template?.name,
+ // description: template?.description,
+ // };
const userIsAdmin =
credentials.status !== "loggedIn" ? false : credentials.isUserAdministrator;
- const editableForm =
- userIsAdmin && (purpose === "create" || purpose === "update");
+ const editableForm = userIsAdmin;
- function updateForm(newForm: typeof defaultValue): void {
+ function updateForm(newForm: ConversionRateClassFormData): void {
const errors = undefinedIfEmpty<
- ErrorMessageMappingFor<typeof defaultValue>
+ ErrorMessageMappingFor<ConversionRateClassFormData>
>({
name: !editableForm
? undefined // disabled
- : purpose === "update" && newForm.name === undefined
- ? undefined // the field hasn't been changed
- : !newForm.name
- ? i18n.str`Required`
- : undefined,
- cashin_fee:
- !editableForm || !newForm.cashin_enabled
- ? undefined
- : !newForm.cashin_fee
- ? i18n.str`Required`
- : undefined,
- cashout_fee:
- !editableForm || !newForm.cashout_fee
- ? undefined
- : !newForm.cashout_fee
- ? i18n.str`Required`
- : undefined,
- cashin_min_amount:
- !editableForm || !newForm.cashin_min_amount
- ? undefined
- : !newForm.cashin_min_amount
- ? i18n.str`Required`
- : undefined,
- cashout_min_amount:
- !editableForm || !newForm.cashout_min_amount
- ? undefined
- : !newForm.cashout_min_amount
- ? i18n.str`Required`
- : undefined,
+ : !newForm.name
+ ? i18n.str`Required`
+ : undefined,
+ // cashin_fee:
+ // !editableForm || !newForm.cashin_enabled
+ // ? undefined
+ // : !newForm.cashin_fee
+ // ? i18n.str`Required`
+ // : undefined,
+ // cashout_fee:
+ // !editableForm || !newForm.cashout_fee
+ // ? undefined
+ // : !newForm.cashout_fee
+ // ? i18n.str`Required`
+ // : undefined,
+ // cashin_min_amount:
+ // !editableForm || !newForm.cashin_min_amount
+ // ? undefined
+ // : !newForm.cashin_min_amount
+ // ? i18n.str`Required`
+ // : undefined,
+ // cashout_min_amount:
+ // !editableForm || !newForm.cashout_min_amount
+ // ? undefined
+ // : !newForm.cashout_min_amount
+ // ? i18n.str`Required`
+ // : undefined,
});
setErrors(errors);
@@ -181,33 +172,39 @@ export function ConversionRateClassForm<
if (errors) {
onChange(undefined);
} else {
- switch (purpose) {
- case "create": {
- // typescript doesn't correctly narrow a generic type
- const callback = onChange as ChangeByPurposeType["create"];
- const result: TalerCorebankApi.ConversionRateClassInput = {
- name: newForm.name!,
- };
- callback(result);
- return;
- }
- case "update": {
- // typescript doesn't correctly narrow a generic type
- const callback = onChange as ChangeByPurposeType["update"];
+ const result: TalerCorebankApi.ConversionRateClassInput = {
+ name: newForm.name!,
+ description: newForm.description,
+ };
+ onChange(result);
+ // switch (purpose) {
+ // case "create": {
+ // // typescript doesn't correctly narrow a generic type
+ // const callback = onChange as ChangeByPurposeType["create"];
+ // const result: TalerCorebankApi.ConversionRateClassInput = {
+ // name: newForm.name!,
+ // description: newForm.description,
+ // };
+ // callback(result);
+ // return;
+ // }
+ // case "update": {
+ // // typescript doesn't correctly narrow a generic type
+ // const callback = onChange as ChangeByPurposeType["update"];
- const result: TalerCorebankApi.ConversionRateClassInput = {
- name: newForm.name!,
- };
- callback(result);
- return;
- }
- case "show": {
- return;
- }
- default: {
- assertUnreachable(purpose);
- }
- }
+ // const result: TalerCorebankApi.ConversionRateClassInput = {
+ // name: newForm.name!,
+ // };
+ // callback(result);
+ // return;
+ // }
+ // case "show": {
+ // return;
+ // }
+ // default: {
+ // assertUnreachable(purpose);
+ // }
+ // }
}
}
return (
@@ -219,7 +216,6 @@ export function ConversionRateClassForm<
e.preventDefault();
}}
>
- <pre>{JSON.stringify(errors, undefined, 2)}</pre>
<div class="px-4 py-6 sm:p-8">
<div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
<div class="sm:col-span-5">
@@ -232,14 +228,14 @@ export function ConversionRateClassForm<
</label>
<div class="mt-2">
<input
- ref={focus && purpose === "create" ? doAutoFocus : undefined}
+ ref={focus ? doAutoFocus : undefined}
type="text"
class="block w-full disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
name="username"
id="username"
data-error={!!errors?.name && form.name !== undefined}
disabled={!editableForm}
- value={form.name ?? defaultValue.name}
+ value={form.name ?? ""}
onChange={(e) => {
form.name = e.currentTarget.value;
updateForm(structuredClone(form));
@@ -266,7 +262,7 @@ export function ConversionRateClassForm<
</label>
<div class="mt-2">
<input
- ref={focus && purpose === "create" ? doAutoFocus : undefined}
+ ref={focus ? doAutoFocus : undefined}
type="text"
class="block w-full disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
name="username"
@@ -275,7 +271,7 @@ export function ConversionRateClassForm<
!!errors?.description && form.description !== undefined
}
disabled={!editableForm}
- value={form.description ?? defaultValue.description}
+ value={form.description ?? ""}
onChange={(e) => {
form.description = e.currentTarget.value;
updateForm(structuredClone(form));
@@ -293,7 +289,7 @@ export function ConversionRateClassForm<
</p>
</div>
- <InputToggle
+ {/* <InputToggle
label={i18n.str`Enable cashin`}
name="cashin"
threeState={false}
@@ -320,9 +316,9 @@ export function ConversionRateClassForm<
},
name: "cashout",
}}
- />
+ /> */}
- {!form.cashin_enabled ? undefined : (
+ {/* {!form.cashin_enabled ? undefined : (
<Fragment>
<div class="sm:col-span-5">
<label
@@ -627,7 +623,7 @@ export function ConversionRateClassForm<
</p>
</div>
</Fragment>
- )}
+ )} */}
</div>
</div>
{children}
diff --git a/packages/taler-util/src/http-client/bank-conversion.ts b/packages/taler-util/src/http-client/bank-conversion.ts
@@ -103,7 +103,10 @@ export class TalerBankConversionHttpClient {
* https://docs.taler.net/core/api-bank-conversion-info.html#get--cashin-rate
*
*/
- async getCashinRate(conversion: { debit?: AmountJson; credit?: AmountJson }) {
+ async getCashinRate(
+ auth: AccessToken | undefined,
+ conversion: { debit?: AmountJson; credit?: AmountJson },
+ ) {
const url = new URL(`cashin-rate`, this.baseUrl);
if (conversion.debit) {
url.searchParams.set("amount_debit", Amounts.stringify(conversion.debit));
@@ -114,8 +117,13 @@ export class TalerBankConversionHttpClient {
Amounts.stringify(conversion.credit),
);
}
+ const headers: Record<string, string> = {};
+ if (auth) {
+ headers.Authorization = makeBearerTokenAuthHeader(auth);
+ }
const resp = await this.httpLib.fetch(url.href, {
method: "GET",
+ headers,
});
switch (resp.status) {
case HttpStatusCode.Ok:
@@ -147,10 +155,13 @@ export class TalerBankConversionHttpClient {
* https://docs.taler.net/core/api-bank-conversion-info.html#get--cashout-rate
*
*/
- async getCashoutRate(conversion: {
- debit?: AmountJson;
- credit?: AmountJson;
- }) {
+ async getCashoutRate(
+ auth: AccessToken | undefined,
+ conversion: {
+ debit?: AmountJson;
+ credit?: AmountJson;
+ },
+ ) {
const url = new URL(`cashout-rate`, this.baseUrl);
if (conversion.debit) {
url.searchParams.set("amount_debit", Amounts.stringify(conversion.debit));
@@ -161,8 +172,13 @@ export class TalerBankConversionHttpClient {
Amounts.stringify(conversion.credit),
);
}
+ const headers: Record<string, string> = {};
+ if (auth) {
+ headers.Authorization = makeBearerTokenAuthHeader(auth);
+ }
const resp = await this.httpLib.fetch(url.href, {
method: "GET",
+ headers,
});
switch (resp.status) {
case HttpStatusCode.Ok:
diff --git a/packages/taler-util/src/http-client/bank-core.ts b/packages/taler-util/src/http-client/bank-core.ts
@@ -1148,7 +1148,11 @@ export class TalerCoreBankHttpClient {
* https://docs.taler.net/core/api-corebank.html#patch--conversion-rate-classes-CLASS_ID
*
*/
- async updateConversionRateClass(auth: AccessToken, cid: number, body: ConversionRateClassInput) {
+ async updateConversionRateClass(
+ auth: AccessToken,
+ cid: number,
+ body: ConversionRateClassInput,
+ ) {
const url = new URL(`conversion-rate-classes/${cid}`, this.baseUrl);
const resp = await this.httpLib.fetch(url.href, {
method: "PATCH",
@@ -1506,7 +1510,26 @@ export class TalerCoreBankHttpClient {
}
/**
- * https://docs.taler.net/core/api-corebank.html#post--accounts-$USERNAME-token
+ * https://docs.taler.net/core/api-corebank.html#any--accounts-$USERNAME-conversion-info-*
+ *
+ */
+ getConversionInfoAPIForUser(username: string): URL {
+ return new URL(`accounts/${username}/conversion-info/`, this.baseUrl);
+ }
+
+ /**
+ * https://docs.taler.net/core/api-corebank.html#any--conversion-rate-classes-$CLASS_ID-conversion-info-*
+ *
+ */
+ getConversionInfoAPIForClass(classId: number): URL {
+ return new URL(
+ `conversion-rate-classes/${String(classId)}/conversion-info/`,
+ this.baseUrl,
+ );
+ }
+
+ /**
+ * https://docs.taler.net/core/api-corebank.html#any--conversion-info-*
*
*/
getConversionInfoAPI(): URL {
diff --git a/packages/web-util/src/context/activity.ts b/packages/web-util/src/context/activity.ts
@@ -73,6 +73,8 @@ export interface ExchangeLib {
export interface BankLib {
bank: TalerCoreBankHttpClient;
conversion: TalerBankConversionHttpClient;
+ conversionForUser(username:string): TalerBankConversionHttpClient;
+ conversionForClass(classId: number): TalerBankConversionHttpClient;
}
export interface ChallengerLib {
diff --git a/packages/web-util/src/context/bank-api.ts b/packages/web-util/src/context/bank-api.ts
@@ -203,6 +203,20 @@ function buildBankApiClient(
lib: {
bank,
conversion,
+ conversionForClass(classId) {
+ return new TalerBankConversionHttpClient(
+ bank.getConversionInfoAPIForClass(classId).href,
+ httpLib,
+ evictors.conversion,
+ );
+ },
+ conversionForUser(username) {
+ return new TalerBankConversionHttpClient(
+ bank.getConversionInfoAPIForUser(username).href,
+ httpLib,
+ evictors.conversion,
+ );
+ },
},
onActivity: tracker.subscribe,
cancelRequest: httpLib.cancelRequest,