taler-typescript-core

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

commit b895fdd3ecee933a9a5a69dd419ffdc9ea264c64
parent a3317d6a37f3b90b76026e9a058ae08d39b88766
Author: Sebastian <sebasjm@gmail.com>
Date:   Thu, 26 Jun 2025 11:41:10 -0300

wip api integration

Diffstat:
Apackages/bank-ui/src/hooks/conversion-rate.ts | 0
Apackages/bank-ui/src/pages/admin/ConversionClassList.tsx | 237+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-util/src/http-client/bank-core.ts | 269+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
Mpackages/taler-util/src/types-taler-bank-conversion.ts | 4++--
Mpackages/taler-util/src/types-taler-common.ts | 9++++++---
Mpackages/taler-util/src/types-taler-corebank.ts | 176++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
6 files changed, 629 insertions(+), 66 deletions(-)

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/admin/ConversionClassList.tsx b/packages/bank-ui/src/pages/admin/ConversionClassList.tsx @@ -0,0 +1,237 @@ +/* + This file is part of GNU Taler + (C) 2022-2024 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +import { + Amounts, + HttpStatusCode, + TalerError, + assertUnreachable, +} from "@gnu-taler/taler-util"; +import { + Loading, + RouteDefinition, + useBankCoreApiContext, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; +import { Fragment, VNode, h } from "preact"; +import { ErrorLoadingWithDebug } from "../../components/ErrorLoadingWithDebug.js"; +import { useBusinessAccounts } from "../../hooks/regional.js"; +import { RenderAmount } from "../PaytoWireTransferForm.js"; + +const TALER_SCREEN_ID = 121; + +interface Props { + routeCreate: RouteDefinition; + + routeShowAccount: RouteDefinition<{ account: string }>; + routeRemoveAccount: RouteDefinition<{ account: string }>; + routeUpdatePasswordAccount: RouteDefinition<{ account: string }>; +} + +export function AccountList({ + routeCreate, + routeRemoveAccount, + routeShowAccount, + routeUpdatePasswordAccount, +}: Props): VNode { + const result = useBusinessAccounts(); + const { i18n } = useTranslationContext(); + const { config } = useBankCoreApiContext(); + + if (!result) { + return <Loading />; + } + if (result instanceof TalerError) { + return <ErrorLoadingWithDebug error={result} />; + } + switch (result.case) { + case "ok": + break; + case HttpStatusCode.Unauthorized: + return <Fragment />; + default: + assertUnreachable(result); + } + + const onGoStart = result.isFirstPage ? undefined : result.loadFirst; + const onGoNext = result.isLastPage ? undefined : result.loadNext; + + const accounts = result.body; + 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>Accounts</i18n.Translate> + </h1> + </div> + <div class="mt-4 sm:ml-16 sm:mt-0 sm:flex-none"> + <a + href={routeCreate.url({})} + name="create account" + 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>Create account</i18n.Translate> + </a> + </div> + </div> + <div class="mt-4 flow-root"> + <div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"> + <div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"> + {!accounts.length ? ( + <div>{/* FIXME: ADD empty list */}</div> + ) : ( + <table class="min-w-full divide-y divide-gray-300"> + <thead> + <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`Balance`}</th> + <th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-0"> + <span class="sr-only">{i18n.str`Actions`}</span> + </th> + </tr> + </thead> + <tbody class="divide-y divide-gray-200"> + {accounts.map((item, idx) => { + const balance = !item.balance + ? undefined + : Amounts.parse(item.balance.amount); + const noBalance = Amounts.isZero(item.balance.amount); + const balanceIsDebit = + item.balance && + item.balance.credit_debit_indicator == "debit"; + + return ( + <tr + key={idx} + 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"> + <a + name={`show account ${item.username}`} + href={routeShowAccount.url({ + account: item.username, + })} + class="text-indigo-600 hover:text-indigo-900" + > + {item.username} + </a> + </td> + <td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500"> + {item.name} + </td> + <td + data-negative={ + noBalance + ? undefined + : balanceIsDebit + ? "true" + : "false" + } + class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 data-[negative=false]:text-green-600 data-[negative=true]:text-red-600 " + > + {!balance ? ( + i18n.str`Unknown` + ) : ( + <span class="amount"> + <RenderAmount + value={balance} + negative={balanceIsDebit} + spec={config.currency_specification} + /> + </span> + )} + </td> + <td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-0"> + {item.status === "deleted" ? ( + <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({ + account: item.username, + })} + class="text-indigo-600 hover:text-indigo-900" + > + <i18n.Translate>Remove</i18n.Translate> + </a> + ) : undefined} + </Fragment> + )} + </td> + </tr> + ); + })} + </tbody> + </table> + )} + </div> + <nav + class="flex items-center justify-between border-t border-gray-200 bg-white px-4 py-3 sm:px-6 rounded-lg" + aria-label="Pagination" + > + <div class="flex flex-1 justify-between sm:justify-end"> + <button + name="first page" + class="relative disabled:bg-gray-100 disabled:text-gray-500 inline-flex items-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus-visible:outline-offset-0" + disabled={!onGoStart} + onClick={onGoStart} + > + <i18n.Translate>First page</i18n.Translate> + </button> + <button + name="next page" + class="relative disabled:bg-gray-100 disabled:text-gray-500 ml-3 inline-flex items-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus-visible:outline-offset-0" + disabled={!onGoNext} + onClick={onGoNext} + > + <i18n.Translate>Next</i18n.Translate> + </button> + </div> + </nav> + </div> + </div> + </div> + </Fragment> + ); +} diff --git a/packages/taler-util/src/http-client/bank-core.ts b/packages/taler-util/src/http-client/bank-core.ts @@ -53,6 +53,20 @@ import { } from "../operation.js"; import { WithdrawalOperationStatusFlag } from "../types-taler-bank-integration.js"; import { + AccountPasswordChange, + AccountReconfiguration, + BankAccountConfirmWithdrawalRequest, + BankAccountCreateWithdrawalRequest, + CashoutRequest, + Challenge, + ChallengeSolve, + ConversionRateClassInput, + CreateTransactionRequest, + CreateTransactionResponse, + MonitorTimeframeParam, + RegisterAccountRequest, + TalerCorebankConfigResponse, + codecForAccountConversionRateClass, codecForAccountData, codecForBankAccountCreateWithdrawalResponse, codecForBankAccountTransactionInfo, @@ -61,6 +75,7 @@ import { codecForCashoutStatusResponse, codecForCashouts, codecForChallenge, + codecForConversionRateClassResponse, codecForCoreBankConfig, codecForCreateTransactionResponse, codecForGlobalCashouts, @@ -80,8 +95,6 @@ import { nullEvictor, } from "./utils.js"; -import * as TalerCorebankApi from "../types-taler-corebank.js"; - export type TalerCoreBankResultByMethod< prop extends keyof TalerCoreBankHttpClient, > = ResultByMethod<TalerCoreBankHttpClient, prop>; @@ -99,6 +112,9 @@ export enum TalerCoreBankCacheEviction { ABORT_WITHDRAWAL, CREATE_WITHDRAWAL, CREATE_CASHOUT, + CREATE_CONVERSION_RATE_CLASS, + UPDATE_CONVERSION_RATE_CLASS, + DELETE_CONVERSION_RATE_CLASS, } export type Credentials = BasicCredentials | BearerCredentials; @@ -120,7 +136,7 @@ export type BearerCredentials = { * Uses libtool's current:revision:age versioning. */ export class TalerCoreBankHttpClient { - public readonly PROTOCOL_VERSION = "8:0:0"; + public readonly PROTOCOL_VERSION = "9:0:0"; httpLib: HttpRequestLibrary; cacheEvictor: CacheEvictor<TalerCoreBankCacheEviction>; @@ -262,7 +278,7 @@ export class TalerCoreBankHttpClient { */ async getConfig(): Promise< | OperationFail<HttpStatusCode.NotFound> - | OperationOk<TalerCorebankApi.TalerCorebankConfigResponse> + | OperationOk<TalerCorebankConfigResponse> > { const url = new URL(`config`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { @@ -316,23 +332,8 @@ export class TalerCoreBankHttpClient { */ async createAccount( auth: AccessToken | undefined, - body: TalerCorebankApi.RegisterAccountRequest, - ): Promise< - | OperationOk<TalerCorebankApi.RegisterAccountResponse> - | OperationFail<HttpStatusCode.BadRequest> - | OperationFail<HttpStatusCode.Unauthorized> - | OperationFail<TalerErrorCode.BANK_REGISTER_USERNAME_REUSE> - | OperationFail<TalerErrorCode.BANK_REGISTER_PAYTO_URI_REUSE> - | OperationFail<TalerErrorCode.BANK_UNALLOWED_DEBIT> - | OperationFail<TalerErrorCode.BANK_RESERVED_USERNAME_CONFLICT> - | OperationFail<TalerErrorCode.BANK_NON_ADMIN_PATCH_DEBT_LIMIT> - | OperationFail<TalerErrorCode.BANK_NON_ADMIN_SET_MIN_CASHOUT> - | OperationFail<TalerErrorCode.BANK_NON_ADMIN_SET_TAN_CHANNEL> - | OperationFail<TalerErrorCode.BANK_TAN_CHANNEL_NOT_SUPPORTED> - | OperationFail<TalerErrorCode.BANK_MISSING_TAN_INFO> - | OperationFail<TalerErrorCode.BANK_PASSWORD_TOO_SHORT> - | OperationFail<TalerErrorCode.BANK_PASSWORD_TOO_LONG> - > { + body: RegisterAccountRequest, + ) { const url = new URL(`accounts`, this.baseUrl); const headers: Record<string, string> = {}; if (auth) { @@ -367,7 +368,7 @@ export class TalerCoreBankHttpClient { return opKnownTalerFailure(details.code, details); case TalerErrorCode.BANK_NON_ADMIN_PATCH_DEBT_LIMIT: return opKnownTalerFailure(details.code, details); - case TalerErrorCode.BANK_NON_ADMIN_SET_MIN_CASHOUT: + case TalerErrorCode.BANK_NON_ADMIN_SET_CONVERSION_RATE_CLASS: return opKnownTalerFailure(details.code, details); case TalerErrorCode.BANK_NON_ADMIN_SET_TAN_CHANNEL: return opKnownTalerFailure(details.code, details); @@ -379,6 +380,8 @@ export class TalerCoreBankHttpClient { return opKnownTalerFailure(details.code, details); case TalerErrorCode.BANK_PASSWORD_TOO_LONG: return opKnownTalerFailure(details.code, details); + case TalerErrorCode.BANK_CONVERSION_RATE_CLASS_UNKNOWN: + return opKnownTalerFailure(details.code, details); default: return opUnknownHttpFailure(resp, details); } @@ -438,7 +441,7 @@ export class TalerCoreBankHttpClient { */ async updateAccount( auth: UserAndToken, - body: TalerCorebankApi.AccountReconfiguration, + body: AccountReconfiguration, cid?: string, ) { const url = new URL(`accounts/${auth.username}`, this.baseUrl); @@ -475,7 +478,7 @@ export class TalerCoreBankHttpClient { return opKnownTalerFailure(details.code, details); case TalerErrorCode.BANK_NON_ADMIN_PATCH_DEBT_LIMIT: return opKnownTalerFailure(details.code, details); - case TalerErrorCode.BANK_NON_ADMIN_SET_MIN_CASHOUT: + case TalerErrorCode.BANK_NON_ADMIN_SET_CONVERSION_RATE_CLASS: return opKnownTalerFailure(details.code, details); case TalerErrorCode.BANK_TAN_CHANNEL_NOT_SUPPORTED: return opKnownTalerFailure(details.code, details); @@ -485,6 +488,8 @@ export class TalerCoreBankHttpClient { return opKnownTalerFailure(details.code, details); case TalerErrorCode.BANK_PASSWORD_TOO_LONG: return opKnownTalerFailure(details.code, details); + case TalerErrorCode.BANK_CONVERSION_RATE_CLASS_UNKNOWN: + return opKnownTalerFailure(details.code, details); default: return opUnknownHttpFailure(resp, details); } @@ -500,7 +505,7 @@ export class TalerCoreBankHttpClient { */ async updatePassword( auth: UserAndToken, - body: TalerCorebankApi.AccountPasswordChange, + body: AccountPasswordChange, cid?: string, ) { const url = new URL(`accounts/${auth.username}/auth`, this.baseUrl); @@ -581,7 +586,7 @@ export class TalerCoreBankHttpClient { */ async getAccounts( auth: AccessToken, - filter: { account?: string } = {}, + filter: { account?: string; conversionRateId?: number } = {}, pagination?: PaginationParams, ) { const url = new URL(`accounts`, this.baseUrl); @@ -589,6 +594,12 @@ export class TalerCoreBankHttpClient { if (filter.account !== undefined) { url.searchParams.set("filter_name", filter.account); } + if (filter.conversionRateId !== undefined) { + url.searchParams.set( + "conversion_rate_class_id", + String(filter.conversionRateId), + ); + } const resp = await this.httpLib.fetch(url.href, { method: "GET", headers: { @@ -702,12 +713,12 @@ export class TalerCoreBankHttpClient { */ async createTransaction( auth: UserAndToken, - body: TalerCorebankApi.CreateTransactionRequest, + body: CreateTransactionRequest, cid?: string, ): Promise< //manually definition all return types because of recursion - | OperationOk<TalerCorebankApi.CreateTransactionResponse> - | OperationAlternative<HttpStatusCode.Accepted, TalerCorebankApi.Challenge> + | OperationOk<CreateTransactionResponse> + | OperationAlternative<HttpStatusCode.Accepted, Challenge> | OperationFail<HttpStatusCode.NotFound> | OperationFail<HttpStatusCode.BadRequest> | OperationFail<HttpStatusCode.Unauthorized> @@ -776,7 +787,7 @@ export class TalerCoreBankHttpClient { */ async createWithdrawal( auth: UserAndToken, - body: TalerCorebankApi.BankAccountCreateWithdrawalRequest, + body: BankAccountCreateWithdrawalRequest, ) { const url = new URL(`accounts/${auth.username}/withdrawals`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { @@ -813,7 +824,7 @@ export class TalerCoreBankHttpClient { */ async confirmWithdrawalById( auth: UserAndToken, - body: TalerCorebankApi.BankAccountConfirmWithdrawalRequest, + body: BankAccountConfirmWithdrawalRequest, wid: string, cid?: string, ) { @@ -943,11 +954,7 @@ export class TalerCoreBankHttpClient { * https://docs.taler.net/core/api-corebank.html#post--accounts-$USERNAME-cashouts * */ - async createCashout( - auth: UserAndToken, - body: TalerCorebankApi.CashoutRequest, - cid?: string, - ) { + async createCashout(auth: UserAndToken, body: CashoutRequest, cid?: string) { const url = new URL(`accounts/${auth.username}/cashouts`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { method: "POST", @@ -1088,6 +1095,186 @@ export class TalerCoreBankHttpClient { } // + // CONVERSION RATE CLASS + // + + /** + * https://docs.taler.net/core/api-corebank.html#post--conversion-rate-classes + * + */ + async createConversionRateClass( + auth: AccessToken, + body: ConversionRateClassInput, + ) { + const url = new URL(`conversion-rate-classes`, this.baseUrl); + + const resp = await this.httpLib.fetch(url.href, { + method: "POST", + headers: { + Authorization: makeBearerTokenAuthHeader(auth), + }, + body, + }); + + switch (resp.status) { + case HttpStatusCode.Ok: + await this.cacheEvictor.notifySuccess( + TalerCoreBankCacheEviction.CREATE_CONVERSION_RATE_CLASS, + ); + return opSuccessFromHttp(resp, codecForConversionRateClassResponse()); + case HttpStatusCode.Unauthorized: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Forbidden: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Conflict: { + const details = await readTalerErrorResponse(resp); + switch (details.code) { + case TalerErrorCode.BANK_NAME_REUSE: + return opKnownTalerFailure(details.code, details); + default: + return opUnknownHttpFailure(resp, details); + } + } + case HttpStatusCode.NotImplemented: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownHttpFailure(resp); + } + } + + /** + * https://docs.taler.net/core/api-corebank.html#patch--conversion-rate-classes-CLASS_ID + * + */ + async updateConversionRateClass(auth: AccessToken, cid: number) { + const url = new URL(`conversion-rate-classes/${cid}`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + method: "PATCH", + headers: { + Authorization: makeBearerTokenAuthHeader(auth), + }, + }); + switch (resp.status) { + case HttpStatusCode.NoContent: + return opEmptySuccess(); + case HttpStatusCode.Unauthorized: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Forbidden: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Conflict: { + const details = await readTalerErrorResponse(resp); + switch (details.code) { + case TalerErrorCode.BANK_NAME_REUSE: + return opKnownTalerFailure(details.code, details); + default: + return opUnknownHttpFailure(resp, details); + } + } + case HttpStatusCode.NotImplemented: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownHttpFailure(resp); + } + } + + /** + * https://docs.taler.net/core/api-corebank.html#get--accounts-$USERNAME-cashouts + * + */ + async deleteConversionRateClass(auth: AccessToken, cid: number) { + const url = new URL(`conversion-rate-classes/${cid}`, this.baseUrl); + + const resp = await this.httpLib.fetch(url.href, { + method: "DELETE", + headers: { + Authorization: makeBearerTokenAuthHeader(auth), + }, + }); + switch (resp.status) { + case HttpStatusCode.NoContent: + return opEmptySuccess(); + case HttpStatusCode.Unauthorized: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Forbidden: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + // case HttpStatusCode.Conflict: { + // const details = await readTalerErrorResponse(resp); + // switch (details.code) { + // case TalerErrorCode.BANK_LI: + // return opKnownTalerFailure(details.code, details); + // default: + // return opUnknownHttpFailure(resp, details); + // } + // } + case HttpStatusCode.NotImplemented: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownHttpFailure(resp); + } + } + + /** + * https://docs.taler.net/core/api-corebank.html#get--conversion-rate-classes-CLASS_ID + * + */ + async getConversionRateClass(auth: AccessToken, cid: number) { + const url = new URL(`conversion-rate-classes/${cid}`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + headers: { + Authorization: makeBearerTokenAuthHeader(auth), + }, + }); + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForAccountConversionRateClass()); + case HttpStatusCode.Unauthorized: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Forbidden: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotImplemented: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownHttpFailure(resp); + } + } + + /** + * https://docs.taler.net/core/api-corebank.html#get--conversion-rate-classes-CLASS_ID + * + */ + async listConversionRateClasses(auth: AccessToken, cid: number) { + const url = new URL(`conversion-rate-classes`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + headers: { + Authorization: makeBearerTokenAuthHeader(auth), + }, + }); + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForAccountConversionRateClass()); + case HttpStatusCode.Unauthorized: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Forbidden: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotImplemented: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownHttpFailure(resp); + } + } + // // 2FA // @@ -1161,7 +1348,7 @@ export class TalerCoreBankHttpClient { async confirmChallenge( auth: UserAndToken, cid: string, - body: TalerCorebankApi.ChallengeSolve, + body: ChallengeSolve, ) { return this.__interal_confirmChallenge( auth.username, @@ -1178,7 +1365,7 @@ export class TalerCoreBankHttpClient { async confirmLoginChallenge( auth: UserAndPassword, cid: string, - body: TalerCorebankApi.ChallengeSolve, + body: ChallengeSolve, ) { return this.__interal_confirmChallenge( auth.username, @@ -1192,7 +1379,7 @@ export class TalerCoreBankHttpClient { username: string, Authorization: string | undefined, cid: string, - body: TalerCorebankApi.ChallengeSolve, + body: ChallengeSolve, ) { const url = new URL( `accounts/${username}/challenge/${cid}/confirm`, @@ -1239,7 +1426,7 @@ export class TalerCoreBankHttpClient { async getMonitor( auth: AccessToken, params: { - timeframe?: TalerCorebankApi.MonitorTimeframeParam; + timeframe?: MonitorTimeframeParam; date?: AbsoluteTime; } = {}, ) { @@ -1247,7 +1434,7 @@ export class TalerCoreBankHttpClient { if (params.timeframe) { url.searchParams.set( "timeframe", - TalerCorebankApi.MonitorTimeframeParam[params.timeframe], + MonitorTimeframeParam[params.timeframe], ); } if (params.date) { diff --git a/packages/taler-util/src/types-taler-bank-conversion.ts b/packages/taler-util/src/types-taler-bank-conversion.ts @@ -58,10 +58,10 @@ export interface ConversionInfo { cashout_tiny_amount: AmountString; // Rounding mode used during cashin conversion - cashin_rounding_mode: "zero" | "up" | "nearest"; + cashin_rounding_mode: RoundingMode; // Rounding mode used during cashout conversion - cashout_rounding_mode: "zero" | "up" | "nearest"; + cashout_rounding_mode: RoundingMode; } export interface TalerConversionInfoConfig { diff --git a/packages/taler-util/src/types-taler-common.ts b/packages/taler-util/src/types-taler-common.ts @@ -437,8 +437,11 @@ export interface TokenInfo { // Time when the token was last used. last_access: Timestamp; - // Opaque unique ID used for pagination. - row_id: Integer; + // ID identifying the token + token_id: Integer; + + // deprecated since **v9**. Use *token_id* instead. + // row_id?: Integer; } export const codecForTokenInfo = (): Codec<TokenInfo> => @@ -457,7 +460,7 @@ export const codecForTokenInfo = (): Codec<TokenInfo> => .property("refreshable", codecForBoolean()) .property("description", codecOptional(codecForString())) .property("last_access", codecForTimestamp) - .property("row_id", codecForNumber()) + .property("token_id", codecForNumber()) .build("TokenInfo"); export const codecForTokenInfoList = (): Codec<TokenInfos> => diff --git a/packages/taler-util/src/types-taler-corebank.ts b/packages/taler-util/src/types-taler-corebank.ts @@ -33,6 +33,7 @@ import { codecForTalerUriString, codecForTimestamp, codecOptionalDefault, + RoundingMode, } from "./index.js"; import { PaytoString, codecForPaytoString } from "./payto.js"; import { TalerUriString } from "./taleruri.js"; @@ -258,6 +259,7 @@ export interface RegisterAccountResponse { export interface RegisterAccountRequest { // Username + // Must match [a-zA-Z0-9\-\._~]{1, 126} username: string; // Password. @@ -295,15 +297,18 @@ export interface RegisterAccountRequest { // Only admin can set this property. debit_threshold?: AmountString; - // If present, set a custom minimum cashout amount for this account. - // Only admin can set this property - // @since v4 - min_cashout?: AmountString; + // If present, set the user conversion rate class + // Only admin can set this property. + // @since **v9** + conversion_rate_class_id?: Integer; // If present, enables 2FA and set the TAN channel used for challenges // Only admin can set this property, other user can reconfig their account // after creation. tan_channel?: TanChannel; + + // @deprecated in **v9**, use conversion_rate_class_id instead + // min_cashout?: Amount; } export type EmailAddress = string; @@ -344,13 +349,16 @@ export interface AccountReconfiguration { // Only admin can change this property. debit_threshold?: AmountString; - // If present, change the custom minimum cashout amount for this account. - // Only admin can set this property - // @since v4 - min_cashout?: AmountString; + // If present, set the user conversion rate class + // Only admin can set this property. + // @since **v9** + conversion_rate_class_id?: Integer; // If present, enables 2FA and set the TAN channel used for challenges tan_channel?: TanChannel | null; + + // @deprecated in **v9**, user conversion rate classes instead + // min_cashout?: Amount; } export interface AccountPasswordChange { @@ -428,16 +436,90 @@ export interface AccountMinimalData { // Is the account locked. // Defaults to false. - // @since **v7** - is_locked?: boolean; + // @deprecated since **v7** + // is_locked?: boolean; // Current status of the account // active: the account can be used + // locked: the account can be used but cannot create new tokens + // @since **v7** // deleted: the account has been deleted but is retained for compliance // reasons, only the administrator can access it - // Default to 'active' is missing - // @since v4, will become mandatory in the next version. - status?: "active" | "deleted"; + // 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 AccountConversionRateClass { + // Class unique ID + conversion_rate_class_id: Integer; + + // 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 ConversionRateClassInput { + // The name of this class + name: string; + + // A description of the class + description?: string; + + // 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 ConversionRateClassResponse { + // ID identifying the conversion rate class being created + conversion_rate_class_id: Integer; } export interface AccountData { @@ -476,19 +558,25 @@ export interface AccountData { // Is the account locked. // Defaults to false. - // @since **v7** - is_locked?: boolean; + // @deprecated since **v7** + // is_locked?: boolean; // Is 2FA enabled and what channel is used for challenges? tan_channel?: TanChannel; // Current status of the account // active: the account can be used + // locked: the account can be used but cannot create new tokens + // @since **v7** // deleted: the account has been deleted but is retained for compliance // reasons, only the administrator can access it - // Default to 'active' is missing - // @since v4, will become mandatory in the next version. - status?: "active" | "deleted"; + // 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 interface CashoutRequest { @@ -761,14 +849,19 @@ export const codecForAccountMinimalData = (): Codec<AccountMinimalData> => .property("row_id", codecForNumber()) .property("debit_threshold", codecForAmountString()) .property("min_cashout", codecOptional(codecForAmountString())) - .property("is_locked", codecOptional(codecForBoolean())) + // .property("is_locked", codecOptional(codecForBoolean())) .property("is_public", codecForBoolean()) .property("is_taler_exchange", codecForBoolean()) .property( + "conversion_rate_class", + codecOptional(codecForAccountConversionRateClass()), + ) + .property( "status", codecOptional( codecForEither( codecForConstString("active"), + codecForConstString("locked"), codecForConstString("deleted"), ), ), @@ -791,9 +884,12 @@ export const codecForAccountData = (): Codec<AccountData> => .property("contact_data", codecOptional(codecForChallengeContactData())) .property("cashout_payto_uri", codecOptional(codecForPaytoString())) .property("is_public", codecForBoolean()) - .property("is_locked", codecOptional(codecForBoolean())) .property("is_taler_exchange", codecForBoolean()) .property( + "conversion_rate_class", + codecOptional(codecForAccountConversionRateClass()), + ) + .property( "tan_channel", codecOptional( codecForEither( @@ -807,12 +903,52 @@ export const codecForAccountData = (): Codec<AccountData> => codecOptional( codecForEither( codecForConstString("active"), + codecForConstString("locked"), codecForConstString("deleted"), ), ), ) .build("TalerCorebankApi.AccountData"); +export const codecForConversionRateClassResponse = + (): Codec<ConversionRateClassResponse> => + buildCodecForObject<ConversionRateClassResponse>() + .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"), + ), + ), + ) + // .property("cashout_tiny_amount", codecForAmountString()) + .build("ConversionBankConfig.AccountConversionRateClass"); + export const codecForChallengeContactData = (): Codec<ChallengeContactData> => buildCodecForObject<ChallengeContactData>() .property("email", codecOptional(codecForString()))