commit b895fdd3ecee933a9a5a69dd419ffdc9ea264c64
parent a3317d6a37f3b90b76026e9a058ae08d39b88766
Author: Sebastian <sebasjm@gmail.com>
Date: Thu, 26 Jun 2025 11:41:10 -0300
wip api integration
Diffstat:
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()))