taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit 5d7e2e78b50dac8ee79760e7a52e7a5b405d90a2
parent b895fdd3ecee933a9a5a69dd419ffdc9ea264c64
Author: Sebastian <sebasjm@gmail.com>
Date:   Sun, 29 Jun 2025 23:10:36 -0300

new conversion API

updating bank spa to take info from conversion rate instead of the
deprecated min_cashout

Diffstat:
Mpackages/bank-ui/src/Routing.tsx | 2+-
Mpackages/bank-ui/src/app.tsx | 11+++++++++++
Dpackages/bank-ui/src/hooks/conversion-rate.ts | 0
Mpackages/bank-ui/src/pages/RegistrationPage.tsx | 6++++--
Mpackages/bank-ui/src/pages/account/ShowAccountDetails.tsx | 13+++++++++++--
Mpackages/bank-ui/src/pages/admin/AccountForm.tsx | 54------------------------------------------------------
Mpackages/bank-ui/src/pages/admin/AdminHome.tsx | 16+++++++++++-----
Mpackages/bank-ui/src/pages/admin/ConversionClassList.tsx | 29++++++++---------------------
Mpackages/bank-ui/src/pages/admin/CreateNewAccount.tsx | 13+++++++++++--
Mpackages/bank-ui/src/pages/regional/ConversionConfig.tsx | 83++++++++++++++-----------------------------------------------------------------
Mpackages/bank-ui/src/pages/regional/CreateCashout.tsx | 4++--
Mpackages/taler-harness/src/integrationtests/test-withdrawal-conversion.ts | 2--
Mpackages/taler-util/src/http-client/bank-core.ts | 17++++++++++++-----
Mpackages/taler-util/src/types-taler-bank-conversion.ts | 119+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Mpackages/taler-util/src/types-taler-corebank.ts | 157++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
15 files changed, 253 insertions(+), 273 deletions(-)

diff --git a/packages/bank-ui/src/Routing.tsx b/packages/bank-ui/src/Routing.tsx @@ -533,7 +533,7 @@ function PrivateRouting({ onAuthorizationRequired={() => navigateTo(privatePages.solveSecondFactor.url({})) } - routeCreate={privatePages.accountCreate} + routeCreateAccount={privatePages.accountCreate} routeRemoveAccount={privatePages.accountDelete} routeShowAccount={privatePages.accountDetails} routeShowCashoutsAccount={privatePages.accountCashouts} diff --git a/packages/bank-ui/src/app.tsx b/packages/bank-ui/src/app.tsx @@ -216,6 +216,17 @@ const evictBankSwrCache: CacheEvictor<TalerCoreBankCacheEviction> = { case TalerCoreBankCacheEviction.ABORT_WITHDRAWAL: case TalerCoreBankCacheEviction.CREATE_WITHDRAWAL: return; + case TalerCoreBankCacheEviction.UPDATE_CONVERSION_RATE_CLASS: + case TalerCoreBankCacheEviction.CREATE_CONVERSION_RATE_CLASS: + case TalerCoreBankCacheEviction.DELETE_CONVERSION_RATE_CLASS: { + await Promise.all([ + revalidateConversionInfo(), + revalidateCashouts(), + revalidateTransactions(), + ]); + + } + return; default: assertUnreachable(op); } diff --git a/packages/bank-ui/src/hooks/conversion-rate.ts b/packages/bank-ui/src/hooks/conversion-rate.ts diff --git a/packages/bank-ui/src/pages/RegistrationPage.tsx b/packages/bank-ui/src/pages/RegistrationPage.tsx @@ -150,8 +150,10 @@ function RegistrationForm({ return i18n.str`Authentication channel is not supported.`; case TalerErrorCode.BANK_NON_ADMIN_PATCH_DEBT_LIMIT: return i18n.str`Only an administrator is allowed to set the debt limit.`; - case TalerErrorCode.BANK_NON_ADMIN_SET_MIN_CASHOUT: - return i18n.str`Only the administrator can change the minimum cashout limit.`; + case TalerErrorCode.BANK_NON_ADMIN_SET_CONVERSION_RATE_CLASS: + return i18n.str`Only the administrator can change the conversion rate.`; + case TalerErrorCode.BANK_CONVERSION_RATE_CLASS_UNKNOWN: + return i18n.str`The conversion rate class doesn't exist.`; case TalerErrorCode.BANK_NON_ADMIN_SET_TAN_CHANNEL: return i18n.str`Only admin can create accounts with second factor authentication.`; case TalerErrorCode.BANK_PASSWORD_TOO_SHORT: diff --git a/packages/bank-ui/src/pages/account/ShowAccountDetails.tsx b/packages/bank-ui/src/pages/account/ShowAccountDetails.tsx @@ -202,10 +202,19 @@ export function ShowAccountDetails({ when: AbsoluteTime.now(), }); } - case TalerErrorCode.BANK_NON_ADMIN_SET_MIN_CASHOUT: { + case TalerErrorCode.BANK_NON_ADMIN_SET_CONVERSION_RATE_CLASS: { return notify({ type: "error", - title: i18n.str`Only the administrator can change the minimum cashout limit.`, + title: i18n.str`Only the administrator can change the conversion rate.`, + description: resp.detail?.hint as TranslatedString, + debug: resp.detail, + when: AbsoluteTime.now(), + }); + } + case TalerErrorCode.BANK_CONVERSION_RATE_CLASS_UNKNOWN: { + return notify({ + type: "error", + title: i18n.str`The conversion rate class doesn't exist.`, description: resp.detail?.hint as TranslatedString, debug: resp.detail, when: AbsoluteTime.now(), diff --git a/packages/bank-ui/src/pages/admin/AccountForm.tsx b/packages/bank-ui/src/pages/admin/AccountForm.tsx @@ -54,7 +54,6 @@ const REGEX_JUST_NUMBERS_REGEX = /^\+[0-9 ]*$/; export type AccountFormData = { debit_threshold?: string; - min_cashout?: string; isExchange?: boolean; isPublic?: boolean; name?: string; @@ -116,9 +115,6 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({ config.default_debit_threshold ?? `${config.currency}:0`, ), - min_cashout: Amounts.stringifyValue( - template?.min_cashout ?? `${config.currency}:0`, - ), isExchange: template?.is_taler_exchange, isPublic: template?.is_public, name: template?.name ?? "", @@ -148,18 +144,12 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({ (config.allow_edit_cashout_payto_uri || userIsAdmin)); const editableThreshold = userIsAdmin && (purpose === "create" || purpose === "update"); - const editableMinCashout = - userIsAdmin && (purpose === "create" || purpose === "update"); const editableAccount = purpose === "create" && userIsAdmin; const hasPhone = !!defaultValue.phone || !!form.phone; const hasEmail = !!defaultValue.email || !!form.email; function updateForm(newForm: typeof defaultValue): void { - const trimmedMinCashoutStr = newForm.min_cashout?.trim(); - const parsedMinCashout = Amounts.parse( - `${config.currency}:${trimmedMinCashoutStr}`, - ); const trimmedDebitThresholdStr = newForm.debit_threshold?.trim(); const parsedDebitThreshold = Amounts.parse( `${config.currency}:${trimmedDebitThresholdStr}`, @@ -211,13 +201,6 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({ : !parsedDebitThreshold ? i18n.str`Not valid` : undefined, - min_cashout: !editableMinCashout - ? undefined - : !trimmedMinCashoutStr - ? undefined - : !parsedMinCashout - ? i18n.str`Not valid` - : undefined, name: !editableName ? undefined // disabled : purpose === "update" && newForm.name === undefined @@ -277,9 +260,6 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({ const threshold = !parsedDebitThreshold ? undefined : Amounts.stringify(parsedDebitThreshold); - const minCashout = !parsedMinCashout - ? undefined - : Amounts.stringify(parsedMinCashout); switch (purpose) { case "create": { @@ -294,7 +274,6 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({ phone: !newForm.phone ? undefined : newForm.phone, }), debit_threshold: threshold ?? config.default_debit_threshold, - min_cashout: minCashout, cashout_payto_uri: cashoutURI === null ? undefined : cashoutURI, payto_uri: internalURI, is_public: newForm.isPublic, @@ -318,7 +297,6 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({ phone: !newForm.phone ? undefined : newForm.phone, }), debit_threshold: threshold, - min_cashout: minCashout, is_public: newForm.isPublic, name: newForm.name, tan_channel: @@ -695,38 +673,6 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({ </div> <div class="sm:col-span-5"> - <label - for="minCashout" - class="block text-sm font-medium leading-6 text-gray-900" - >{i18n.str`Minimum cashout`}</label> - <InputAmount - name="minCashout" - left - currency={config.currency} - value={form.min_cashout ?? defaultValue.min_cashout} - onChange={ - !editableMinCashout - ? undefined - : (e) => { - form.min_cashout = e as AmountString; - updateForm(structuredClone(form)); - } - } - /> - <ShowInputErrorLabel - message={ - errors?.min_cashout ? String(errors?.min_cashout) : undefined - } - isDirty={form.min_cashout !== undefined} - /> - <p class="mt-2 text-sm text-gray-500"> - <i18n.Translate> - Custom minimum cashout amount for this account. - </i18n.Translate> - </p> - </div> - - <div class="sm:col-span-5"> <div class="flex items-center justify-between"> <span class="flex flex-grow flex-col"> <span diff --git a/packages/bank-ui/src/pages/admin/AdminHome.tsx b/packages/bank-ui/src/pages/admin/AdminHome.tsx @@ -39,6 +39,7 @@ import { useConversionInfo, useLastMonitorInfo } from "../../hooks/regional.js"; import { RenderAmount } from "../PaytoWireTransferForm.js"; import { WireTransfer } from "../WireTransfer.js"; import { AccountList } from "./AccountList.js"; +import { ConversionClassList } from "./ConversionClassList.js"; const TALER_SCREEN_ID = 122; @@ -46,22 +47,22 @@ const TALER_SCREEN_ID = 122; * Query account information and show QR code if there is pending withdrawal */ interface Props { - routeCreate: RouteDefinition; routeDownloadStats: RouteDefinition; routeCreateWireTransfer: RouteDefinition<{ account?: string; subject?: string; amount?: string; }>; - - routeShowAccount: RouteDefinition<{ account: string }>; + + routeCreateAccount: RouteDefinition; routeRemoveAccount: RouteDefinition<{ account: string }>; + routeShowAccount: RouteDefinition<{ account: string }>; routeUpdatePasswordAccount: RouteDefinition<{ account: string }>; routeShowCashoutsAccount: RouteDefinition<{ account: string }>; onAuthorizationRequired: () => void; } export function AdminHome({ - routeCreate, + routeCreateAccount, routeRemoveAccount, routeShowAccount, routeUpdatePasswordAccount, @@ -81,11 +82,16 @@ export function AdminHome({ routeCreateWireTransfer={routeCreateWireTransfer} /> <AccountList - routeCreate={routeCreate} + routeCreate={routeCreateAccount} routeRemoveAccount={routeRemoveAccount} routeShowAccount={routeShowAccount} routeUpdatePasswordAccount={routeUpdatePasswordAccount} /> + <ConversionClassList + routeCreate={routeCreateAccount} + routeRemove={routeRemoveAccount} + routeShowDetails={routeShowAccount} + /> </Fragment> ); } diff --git a/packages/bank-ui/src/pages/admin/ConversionClassList.tsx b/packages/bank-ui/src/pages/admin/ConversionClassList.tsx @@ -34,17 +34,16 @@ const TALER_SCREEN_ID = 121; interface Props { routeCreate: RouteDefinition; + routeRemove: RouteDefinition<{ account: string }>; - routeShowAccount: RouteDefinition<{ account: string }>; - routeRemoveAccount: RouteDefinition<{ account: string }>; - routeUpdatePasswordAccount: RouteDefinition<{ account: string }>; + routeShowDetails: RouteDefinition<{ account: string }>; + // routeUpdatePasswordAccount: RouteDefinition<{ account: string }>; } -export function AccountList({ +export function ConversionClassList({ routeCreate, - routeRemoveAccount, - routeShowAccount, - routeUpdatePasswordAccount, + routeRemove, + routeShowDetails, }: Props): VNode { const result = useBusinessAccounts(); const { i18n } = useTranslationContext(); @@ -134,7 +133,7 @@ export function AccountList({ <td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-0"> <a name={`show account ${item.username}`} - href={routeShowAccount.url({ + href={routeShowDetails.url({ account: item.username, })} class="text-indigo-600 hover:text-indigo-900" @@ -172,23 +171,11 @@ export function AccountList({ <p class="text-gray-600">removed</p> ) : ( <Fragment> - <a - name={`update password ${item.username}`} - href={routeUpdatePasswordAccount.url({ - account: item.username, - })} - class="text-indigo-600 hover:text-indigo-900" - > - <i18n.Translate> - Change password - </i18n.Translate> - </a> - <br /> {noBalance ? ( <a name={`remove account ${item.username}`} - href={routeRemoveAccount.url({ + href={routeRemove.url({ account: item.username, })} class="text-indigo-600 hover:text-indigo-900" diff --git a/packages/bank-ui/src/pages/admin/CreateNewAccount.tsx b/packages/bank-ui/src/pages/admin/CreateNewAccount.tsx @@ -148,10 +148,19 @@ export function CreateNewAccount({ debug: resp.detail, when: AbsoluteTime.now(), }); - case TalerErrorCode.BANK_NON_ADMIN_SET_MIN_CASHOUT: { + case TalerErrorCode.BANK_NON_ADMIN_SET_CONVERSION_RATE_CLASS: { return notify({ type: "error", - title: i18n.str`Only the administrator can change the minimum cashout limit.`, + title: i18n.str`Only the administrator can change the conversion rate.`, + description: resp.detail?.hint as TranslatedString, + debug: resp.detail, + when: AbsoluteTime.now(), + }); + } + case TalerErrorCode.BANK_CONVERSION_RATE_CLASS_UNKNOWN: { + return notify({ + type: "error", + title: i18n.str`The conversion rate class doesn't exist.`, description: resp.detail?.hint as TranslatedString, debug: resp.detail, when: AbsoluteTime.now(), diff --git a/packages/bank-ui/src/pages/regional/ConversionConfig.tsx b/packages/bank-ui/src/pages/regional/ConversionConfig.tsx @@ -28,33 +28,33 @@ import { Attention, InternationalizationAPI, LocalNotificationBanner, + RouteDefinition, ShowInputErrorLabel, + useBankCoreApiContext, useLocalNotification, useTranslationContext, utils, } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useEffect, useState } from "preact/hooks"; -import { useBankCoreApiContext } from "@gnu-taler/web-util/browser"; -import { useSessionState } from "../../hooks/session.js"; +import { + FormErrors, + FormStatus, + FormValues, + RecursivePartial, + UIField, + useFormState, +} from "../../hooks/form.js"; import { TransferCalculation, useCashinEstimator, useCashoutEstimator, useConversionInfo, } from "../../hooks/regional.js"; -import { RouteDefinition } from "@gnu-taler/web-util/browser"; +import { useSessionState } from "../../hooks/session.js"; import { undefinedIfEmpty } from "../../utils.js"; import { InputAmount, RenderAmount } from "../PaytoWireTransferForm.js"; import { ProfileNavigation } from "../ProfileNavigation.js"; -import { - FormErrors, - FormStatus, - FormValues, - RecursivePartial, - UIField, - useFormState, -} from "../../hooks/form.js"; const TALER_SCREEN_ID = 126; @@ -116,15 +116,11 @@ function useComponentState({ amount: "100", conv: { cashin_min_amount: info.conversion_rate.cashin_min_amount.split(":")[1], - cashin_tiny_amount: - info.conversion_rate.cashin_tiny_amount.split(":")[1], cashin_fee: info.conversion_rate.cashin_fee.split(":")[1], cashin_ratio: info.conversion_rate.cashin_ratio, cashin_rounding_mode: info.conversion_rate.cashin_rounding_mode, cashout_min_amount: info.conversion_rate.cashout_min_amount.split(":")[1], - cashout_tiny_amount: - info.conversion_rate.cashout_tiny_amount.split(":")[1], cashout_fee: info.conversion_rate.cashout_fee.split(":")[1], cashout_ratio: info.conversion_rate.cashout_ratio, cashout_rounding_mode: info.conversion_rate.cashout_rounding_mode, @@ -342,7 +338,6 @@ function useComponentState({ minimum={form?.conv?.cashin_min_amount} ratio={form?.conv?.cashin_ratio} rounding={form?.conv?.cashin_rounding_mode} - tiny={form?.conv?.cashin_tiny_amount} /> )} @@ -356,7 +351,6 @@ function useComponentState({ minimum={form?.conv?.cashout_min_amount} ratio={form?.conv?.cashout_ratio} rounding={form?.conv?.cashout_rounding_mode} - tiny={form?.conv?.cashout_tiny_amount} /> </Fragment> )} @@ -593,17 +587,14 @@ function createFormValidator( const cashin_min_amount = Amounts.parse( `${fiat}:${state.conv.cashin_min_amount}`, ); - const cashin_tiny_amount = Amounts.parse( - `${regional}:${state.conv.cashin_tiny_amount}`, - ); + // const cashin_tiny_amount = Amounts.parse( + // `${regional}:${state.conv.cashin_tiny_amount}`, + // ); const cashin_fee = Amounts.parse(`${regional}:${state.conv.cashin_fee}`); const cashout_min_amount = Amounts.parse( `${regional}:${state.conv.cashout_min_amount}`, ); - const cashout_tiny_amount = Amounts.parse( - `${fiat}:${state.conv.cashout_tiny_amount}`, - ); const cashout_fee = Amounts.parse(`${fiat}:${state.conv.cashout_fee}`); const am = Amounts.parse(`${fiat}:${state.amount}`); @@ -618,11 +609,6 @@ function createFormValidator( : !cashin_min_amount ? i18n.str`Invalid` : undefined, - cashin_tiny_amount: !state.conv.cashin_tiny_amount - ? i18n.str`Required` - : !cashin_tiny_amount - ? i18n.str`Invalid` - : undefined, cashin_fee: !state.conv.cashin_fee ? i18n.str`Required` : !cashin_fee @@ -634,11 +620,6 @@ function createFormValidator( : !cashout_min_amount ? i18n.str`Invalid` : undefined, - cashout_tiny_amount: !state.conv.cashin_tiny_amount - ? i18n.str`Required` - : !cashout_tiny_amount - ? i18n.str`Invalid` - : undefined, cashout_fee: !state.conv.cashin_fee ? i18n.str`Required` : !cashout_fee @@ -686,9 +667,6 @@ function createFormValidator( cashin_rounding_mode: !errors?.conv?.cashin_rounding_mode ? state.conv.cashin_rounding_mode! : undefined, - cashin_tiny_amount: !errors?.conv?.cashin_tiny_amount - ? Amounts.stringify(cashin_tiny_amount!) - : undefined, cashout_fee: !errors?.conv?.cashout_fee ? Amounts.stringify(cashout_fee!) : undefined, @@ -701,9 +679,6 @@ function createFormValidator( cashout_rounding_mode: !errors?.conv?.cashout_rounding_mode ? state.conv.cashout_rounding_mode! : undefined, - cashout_tiny_amount: !errors?.conv?.cashout_tiny_amount - ? Amounts.stringify(cashout_tiny_amount!) - : undefined, }, }; return errors === undefined @@ -720,12 +695,10 @@ function ConversionForm({ minimum, ratio, rounding, - tiny, }: { inputCurrency: string; outputCurrency: string; minimum: UIField | undefined; - tiny: UIField | undefined; fee: UIField | undefined; rounding: UIField | undefined; ratio: UIField | undefined; @@ -804,34 +777,6 @@ function ConversionForm({ <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={`${id}_tiny_amount`} - class="block text-sm font-medium leading-6 text-gray-900" - >{i18n.str`Rounding value`}</label> - <InputAmount - name={`${id}_tiny_amount`} - left - currency={outputCurrency} - value={tiny?.value ?? ""} - onChange={tiny?.onUpdate} - /> - <ShowInputErrorLabel - message={tiny?.error} - isDirty={tiny?.value !== undefined} - /> - <p class="mt-2 text-sm text-gray-500"> - <i18n.Translate> - Smallest difference between two amounts after the ratio is - applied. - </i18n.Translate> - </p> - </div> - </div> - </div> - - <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 class="block text-sm font-medium leading-6 text-gray-900" for={`${id}_channel`} > diff --git a/packages/bank-ui/src/pages/regional/CreateCashout.tsx b/packages/bank-ui/src/pages/regional/CreateCashout.tsx @@ -195,9 +195,9 @@ export function CreateCashout({ resultAccount.body.balance.credit_debit_indicator == "debit", debitThreshold: Amounts.parseOrThrow(resultAccount.body.debit_threshold), minCashout: - resultAccount.body.min_cashout === undefined + resultAccount.body.conversion_rate === undefined ? regionalZero - : Amounts.parseOrThrow(resultAccount.body.min_cashout), + : Amounts.parseOrThrow(resultAccount.body.conversion_rate.cashin_min_amount), }; const limit = account.balanceIsDebit diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-conversion.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-conversion.ts @@ -96,12 +96,10 @@ async function runTestfakeConversionService(): Promise<TestfakeConversionService cashin_min_amount: "A:0.1" as AmountString, cashin_ratio: "1", cashin_rounding_mode: "zero", - cashin_tiny_amount: "A:1" as AmountString, cashout_fee: "A:1" as AmountString, cashout_min_amount: "A:0.1" as AmountString, cashout_ratio: "1", cashout_rounding_mode: "zero", - cashout_tiny_amount: "A:1" as AmountString, }, } satisfies TalerBankConversionApi.TalerConversionInfoConfig), ); diff --git a/packages/taler-util/src/http-client/bank-core.ts b/packages/taler-util/src/http-client/bank-core.ts @@ -66,7 +66,6 @@ import { MonitorTimeframeParam, RegisterAccountRequest, TalerCorebankConfigResponse, - codecForAccountConversionRateClass, codecForAccountData, codecForBankAccountCreateWithdrawalResponse, codecForBankAccountTransactionInfo, @@ -75,7 +74,9 @@ import { codecForCashoutStatusResponse, codecForCashouts, codecForChallenge, + codecForConversionRateClass, codecForConversionRateClassResponse, + codecForConversionRateClasses, codecForCoreBankConfig, codecForCreateTransactionResponse, codecForGlobalCashouts, @@ -1158,6 +1159,9 @@ export class TalerCoreBankHttpClient { }); switch (resp.status) { case HttpStatusCode.NoContent: + await this.cacheEvictor.notifySuccess( + TalerCoreBankCacheEviction.UPDATE_CONVERSION_RATE_CLASS, + ); return opEmptySuccess(); case HttpStatusCode.Unauthorized: return opKnownHttpFailure(resp.status, resp); @@ -1196,6 +1200,9 @@ export class TalerCoreBankHttpClient { }); switch (resp.status) { case HttpStatusCode.NoContent: + await this.cacheEvictor.notifySuccess( + TalerCoreBankCacheEviction.DELETE_CONVERSION_RATE_CLASS, + ); return opEmptySuccess(); case HttpStatusCode.Unauthorized: return opKnownHttpFailure(resp.status, resp); @@ -1233,7 +1240,7 @@ export class TalerCoreBankHttpClient { }); switch (resp.status) { case HttpStatusCode.Ok: - return opSuccessFromHttp(resp, codecForAccountConversionRateClass()); + return opSuccessFromHttp(resp, codecForConversionRateClass()); case HttpStatusCode.Unauthorized: return opKnownHttpFailure(resp.status, resp); case HttpStatusCode.Forbidden: @@ -1248,10 +1255,10 @@ export class TalerCoreBankHttpClient { } /** - * https://docs.taler.net/core/api-corebank.html#get--conversion-rate-classes-CLASS_ID + * https://docs.taler.net/core/api-corebank.html#get--conversion-rate-classes * */ - async listConversionRateClasses(auth: AccessToken, cid: number) { + async listConversionRateClasses(auth: AccessToken) { const url = new URL(`conversion-rate-classes`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { method: "GET", @@ -1261,7 +1268,7 @@ export class TalerCoreBankHttpClient { }); switch (resp.status) { case HttpStatusCode.Ok: - return opSuccessFromHttp(resp, codecForAccountConversionRateClass()); + return opSuccessFromHttp(resp, codecForConversionRateClasses()); case HttpStatusCode.Unauthorized: return opKnownHttpFailure(resp.status, resp); case HttpStatusCode.Forbidden: diff --git a/packages/taler-util/src/types-taler-bank-conversion.ts b/packages/taler-util/src/types-taler-bank-conversion.ts @@ -32,37 +32,37 @@ import { codecForDecimalNumber, } from "./types-taler-common.js"; -export interface ConversionInfo { - // Exchange rate to buy regional currency from fiat - cashin_ratio: DecimalNumber; +// export interface ConversionInfo { +// // Exchange rate to buy regional currency from fiat +// cashin_ratio: DecimalNumber; - // Exchange rate to sell regional currency for fiat - cashout_ratio: DecimalNumber; +// // Exchange rate to sell regional currency for fiat +// cashout_ratio: DecimalNumber; - // Fee to subtract after applying the cashin ratio. - cashin_fee: AmountString; +// // Fee to subtract after applying the cashin ratio. +// cashin_fee: AmountString; - // Fee to subtract after applying the cashout ratio. - cashout_fee: AmountString; +// // Fee to subtract after applying the cashout ratio. +// cashout_fee: AmountString; - // Minimum amount authorised for cashin, in fiat before conversion - cashin_min_amount: AmountString; +// // Minimum amount authorised for cashin, in fiat before conversion +// cashin_min_amount: AmountString; - // Minimum amount authorised for cashout, in regional before conversion - cashout_min_amount: AmountString; +// // Minimum amount authorised for cashout, in regional before conversion +// cashout_min_amount: AmountString; - // Smallest possible regional amount, converted amount is rounded to this amount - cashin_tiny_amount: AmountString; +// // Smallest possible regional amount, converted amount is rounded to this amount +// cashin_tiny_amount: AmountString; - // Smallest possible fiat amount, converted amount is rounded to this amount - cashout_tiny_amount: AmountString; +// // Smallest possible fiat amount, converted amount is rounded to this amount +// cashout_tiny_amount: AmountString; - // Rounding mode used during cashin conversion - cashin_rounding_mode: RoundingMode; +// // Rounding mode used during cashin conversion +// cashin_rounding_mode: RoundingMode; - // Rounding mode used during cashout conversion - cashout_rounding_mode: RoundingMode; -} +// // Rounding mode used during cashout conversion +// cashout_rounding_mode: RoundingMode; +// } export interface TalerConversionInfoConfig { // libtool-style representation of the Bank protocol version, see @@ -73,6 +73,10 @@ export interface TalerConversionInfoConfig { // Name of the API. name: "taler-conversion-info"; + // URN of the implementation (needed to interpret 'revision' in version). + // @since v4, may become mandatory in the future. + implementation?: string; + // Currency used by this bank. regional_currency: string; @@ -85,9 +89,11 @@ export interface TalerConversionInfoConfig { // How the bank SPA should render this currency. fiat_currency_specification: CurrencySpecification; - // Extra conversion rate information. - // Only present if server opts in to report the static conversion rate. - conversion_rate: ConversionInfo; + // Global exchange rate between the regional currency and the fiat + // currency of the banking system. Use /rate to get the user specific + // rate. + // FIXME spec: changed on v2, breaking change insteand of deprecating + conversion_rate: ConversionRate; } export interface CashinConversionResponse { @@ -111,20 +117,24 @@ export interface CashoutConversionResponse { export type RoundingMode = "zero" | "up" | "nearest"; export interface ConversionRate { + // Minimum amount authorised for cashin, in fiat before conversion + cashin_min_amount: AmountString; + // Exchange rate to buy regional currency from fiat cashin_ratio: DecimalNumber; // Fee to subtract after applying the cashin ratio. cashin_fee: AmountString; - // Minimum amount authorised for cashin, in fiat before conversion - cashin_min_amount: AmountString; + // Rounding mode used during cashin conversion + cashin_rounding_mode: RoundingMode; // Smallest possible regional amount, converted amount is rounded to this amount - cashin_tiny_amount: AmountString; + // FIXME spec: is this depreacted? + // cashin_tiny_amount: AmountString; - // Rounding mode used during cashin conversion - cashin_rounding_mode: RoundingMode; + // Minimum amount authorised for cashout, in regional before conversion + cashout_min_amount: AmountString; // Exchange rate to sell regional currency for fiat cashout_ratio: DecimalNumber; @@ -132,14 +142,12 @@ export interface ConversionRate { // Fee to subtract after applying the cashout ratio. cashout_fee: AmountString; - // Minimum amount authorised for cashout, in regional before conversion - cashout_min_amount: AmountString; - - // Smallest possible fiat amount, converted amount is rounded to this amount - cashout_tiny_amount: AmountString; - // Rounding mode used during cashout conversion cashout_rounding_mode: RoundingMode; + + // Smallest possible fiat amount, converted amount is rounded to this amount + // FIXME spec: is this depreacted? + // cashout_tiny_amount: AmountString; } export const codecForCashoutConversionResponse = @@ -156,8 +164,8 @@ export const codecForCashinConversionResponse = .property("amount_debit", codecForAmountString()) .build("TalerCorebankApi.CashinConversionResponse"); -export const codecForConversionInfo = (): Codec<ConversionInfo> => - buildCodecForObject<ConversionInfo>() +export const codecForConversionRate = (): Codec<ConversionRate> => + buildCodecForObject<ConversionRate>() .property("cashin_fee", codecForAmountString()) .property("cashin_min_amount", codecForAmountString()) .property("cashin_ratio", codecForDecimalNumber()) @@ -169,7 +177,8 @@ export const codecForConversionInfo = (): Codec<ConversionInfo> => codecForConstString("nearest"), ), ) - .property("cashin_tiny_amount", codecForAmountString()) + // FIXME spec: depreacted? + // .property("cashin_tiny_amount", codecForAmountString()) .property("cashout_fee", codecForAmountString()) .property("cashout_min_amount", codecForAmountString()) .property("cashout_ratio", codecForDecimalNumber()) @@ -181,20 +190,22 @@ export const codecForConversionInfo = (): Codec<ConversionInfo> => codecForConstString("nearest"), ), ) - .property("cashout_tiny_amount", codecForAmountString()) + // FIXME spec: depreacted? + // .property("cashout_tiny_amount", codecForAmountString()) .build("ConversionBankConfig.ConversionInfo"); -export const codecForConversionBankConfig = (): Codec<TalerConversionInfoConfig> => - buildCodecForObject<TalerConversionInfoConfig>() - .property("name", codecForConstString("taler-conversion-info")) - .property("version", codecForString()) - .property("regional_currency", codecForString()) - .property( - "regional_currency_specification", - codecForCurrencySpecificiation(), - ) - .property("fiat_currency", codecForString()) - .property("fiat_currency_specification", codecForCurrencySpecificiation()) - - .property("conversion_rate", codecForConversionInfo()) - .build("ConversionBankConfig.IntegrationConfig"); +export const codecForConversionBankConfig = + (): Codec<TalerConversionInfoConfig> => + buildCodecForObject<TalerConversionInfoConfig>() + .property("name", codecForConstString("taler-conversion-info")) + .property("version", codecForString()) + .property("regional_currency", codecForString()) + .property( + "regional_currency_specification", + codecForCurrencySpecificiation(), + ) + .property("fiat_currency", codecForString()) + .property("fiat_currency_specification", codecForCurrencySpecificiation()) + + .property("conversion_rate", codecForConversionRate()) + .build("ConversionBankConfig.IntegrationConfig"); diff --git a/packages/taler-util/src/types-taler-corebank.ts b/packages/taler-util/src/types-taler-corebank.ts @@ -27,12 +27,14 @@ import { import { buildCodecForUnion, codecForAmountString, + codecForConversionRate, codecForEither, codecForList, codecForNumber, codecForTalerUriString, codecForTimestamp, codecOptionalDefault, + ConversionRate, RoundingMode, } from "./index.js"; import { PaytoString, codecForPaytoString } from "./payto.js"; @@ -421,8 +423,9 @@ export interface AccountMinimalData { // Custom minimum cashout amount for this account. // If null or absent, the global conversion fee is used. - // @since v4 - min_cashout?: AmountString; + // @since v6 + // @deprecated in **v9**, use conversion_rate_class_id instead + // min_cashout?: AmountString; // Is this account visible to anyone? is_public: boolean; @@ -448,14 +451,56 @@ export interface AccountMinimalData { // Defaults to 'active' is missing // @since **v4**, will become mandatory in the next version. status?: AccountStatus; - - // Conversion rate class to which the user belongs - // @since **v9** - conversion_rate_class?: AccountConversionRateClass; } export type AccountStatus = "active" | "locked" | "deleted"; +export interface ConversionRateClass { + // The name of this class + name: string; + + // A description of the class + description?: string; + + // Class unique ID + conversion_rate_class_id: Integer; + + // Number of users affected to this class + num_users: Integer; + + // Applied conversion rate + conversion_rate: ConversionRate; + + // Minimum fiat amount authorised for cashin before conversion + cashin_min_amount?: AmountString; + + // Exchange rate to buy regional currency from fiat + cashin_ratio?: DecimalNumber; + + // Regional amount fee to subtract after applying the cashin ratio. + cashin_fee?: AmountString; + + // Rounding mode used during cashin conversion + cashin_rounding_mode?: RoundingMode; + + // Minimum regional amount authorised for cashout before conversion + cashout_min_amount?: AmountString; + + // Exchange rate to sell regional currency for fiat + cashout_ratio?: DecimalNumber; + + // Fiat amount fee to subtract after applying the cashout ratio. + cashout_fee?: AmountString; + + // Rounding mode used during cashout conversion + cashout_rounding_mode?: RoundingMode; +} + +export interface ConversionRateClasses { + default: ConversionRate; + classes: ConversionRateClass[]; +} + export interface AccountConversionRateClass { // Class unique ID conversion_rate_class_id: Integer; @@ -537,13 +582,20 @@ export interface AccountData { // Custom minimum cashout amount for this account. // If null or absent, the global conversion fee is used. - // @since v4 - min_cashout?: AmountString; + // @since v6 + // @deprecated in **v9**, use conversion_rate_class_id instead + // min_cashout?: AmountString; + // Addresses where to send the TAN for transactions. + // Currently only used for cashouts. + // If missing, cashouts will fail. + // In the future, might be used for other transactions + // as well. contact_data?: ChallengeContactData; - // 'payto' address pointing the bank account - // where to send cashouts. This field is optional + // Full 'payto' URI of a fiat bank account where to send cashouts with + // ``name`` as the 'receiver-name'. + // This field is optional // because not all the accounts are required to participate // in the merchants' circuit. One example is the exchange: // that never cashouts. Registering these accounts can @@ -574,9 +626,10 @@ export interface AccountData { // @since **v4**, will become mandatory in the next version. status?: AccountStatus; - // Conversion rate class to which the user belongs + // Conversion rate available to the user + // Only present if conversion is activated on the server // @since **v9** - conversion_rate_class?: AccountConversionRateClass; + conversion_rate?: ConversionRate; } export interface CashoutRequest { @@ -848,15 +901,9 @@ export const codecForAccountMinimalData = (): Codec<AccountMinimalData> => .property("balance", codecForBalance()) .property("row_id", codecForNumber()) .property("debit_threshold", codecForAmountString()) - .property("min_cashout", codecOptional(codecForAmountString())) - // .property("is_locked", codecOptional(codecForBoolean())) .property("is_public", codecForBoolean()) .property("is_taler_exchange", codecForBoolean()) .property( - "conversion_rate_class", - codecOptional(codecForAccountConversionRateClass()), - ) - .property( "status", codecOptional( codecForEither( @@ -880,15 +927,11 @@ export const codecForAccountData = (): Codec<AccountData> => .property("balance", codecForBalance()) .property("payto_uri", codecForPaytoString()) .property("debit_threshold", codecForAmountString()) - .property("min_cashout", codecOptional(codecForAmountString())) .property("contact_data", codecOptional(codecForChallengeContactData())) .property("cashout_payto_uri", codecOptional(codecForPaytoString())) .property("is_public", codecForBoolean()) .property("is_taler_exchange", codecForBoolean()) - .property( - "conversion_rate_class", - codecOptional(codecForAccountConversionRateClass()), - ) + .property("conversion_rate", codecOptional(codecForConversionRate())) .property( "tan_channel", codecOptional( @@ -916,38 +959,44 @@ export const codecForConversionRateClassResponse = .property("conversion_rate_class_id", codecForNumber()) .build("TalerCorebankApi.ConversionRateClassResponse"); -export const codecForAccountConversionRateClass = - (): Codec<AccountConversionRateClass> => - buildCodecForObject<AccountConversionRateClass>() - .property("conversion_rate_class_id", codecForNumber()) - .property("cashin_fee", codecOptional(codecForAmountString())) - .property("cashin_min_amount", codecOptional(codecForAmountString())) - .property("cashin_ratio", codecOptional(codecForDecimalNumber())) - .property( - "cashin_rounding_mode", - codecOptional( - codecForEither( - codecForConstString("zero"), - codecForConstString("up"), - codecForConstString("nearest"), - ), - ), - ) - .property("cashout_fee", codecOptional(codecForAmountString())) - .property("cashout_min_amount", codecOptional(codecForAmountString())) - .property("cashout_ratio", codecOptional(codecForDecimalNumber())) - .property( - "cashout_rounding_mode", - codecOptional( - codecForEither( - codecForConstString("zero"), - codecForConstString("up"), - codecForConstString("nearest"), - ), +export const codecForConversionRateClass = (): Codec<ConversionRateClass> => + buildCodecForObject<ConversionRateClass>() + .property("cashin_fee", codecOptional(codecForAmountString())) + .property("cashin_min_amount", codecOptional(codecForAmountString())) + .property("cashin_ratio", codecOptional(codecForDecimalNumber())) + .property( + "cashin_rounding_mode", + codecOptional( + codecForEither( + codecForConstString("zero"), + codecForConstString("up"), + codecForConstString("nearest"), ), - ) - // .property("cashout_tiny_amount", codecForAmountString()) - .build("ConversionBankConfig.AccountConversionRateClass"); + ), + ) + .property("cashout_fee", codecOptional(codecForAmountString())) + .property("cashout_min_amount", codecOptional(codecForAmountString())) + .property("cashout_ratio", codecOptional(codecForDecimalNumber())) + .property( + "cashout_rounding_mode", + codecForEither( + codecForConstString("zero"), + codecForConstString("up"), + codecForConstString("nearest"), + ), + ) + .property("conversion_rate", codecForConversionRate()) + .property("conversion_rate_class_id", codecForNumber()) + .property("description", codecOptional(codecForString())) + .property("name", codecForString()) + .property("num_users", codecForNumber()) + .build("TalerCorebankApi.ConversionRateClass"); + +export const codecForConversionRateClasses = (): Codec<ConversionRateClasses> => + buildCodecForObject<ConversionRateClasses>() + .property("classes", codecForList(codecForConversionRateClass())) + .property("default", codecForConversionRate()) + .build("TalerCorebankApi.ConversionRateClasses"); export const codecForChallengeContactData = (): Codec<ChallengeContactData> => buildCodecForObject<ChallengeContactData>()