diff options
author | Sebastian <sebasjm@gmail.com> | 2024-04-05 13:01:45 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2024-04-05 13:01:45 -0300 |
commit | 4759ceae7014771a8a23df4800b0fbd016870621 (patch) | |
tree | 1e27323313560e15a35b0122630a846e468e7c28 | |
parent | 89dde053665d39be8367c25691efc008fc2a5cc7 (diff) | |
download | wallet-core-4759ceae7014771a8a23df4800b0fbd016870621.tar.xz |
wip #8276
6 files changed, 361 insertions, 89 deletions
diff --git a/packages/bank-ui/src/pages/account/ShowAccountDetails.tsx b/packages/bank-ui/src/pages/account/ShowAccountDetails.tsx index c071a838a..69a186ca1 100644 --- a/packages/bank-ui/src/pages/account/ShowAccountDetails.tsx +++ b/packages/bank-ui/src/pages/account/ShowAccountDetails.tsx @@ -19,27 +19,26 @@ import { TalerCorebankApi, TalerError, TalerErrorCode, - TalerRevenueHttpClient, TranslatedString, assertUnreachable, - parsePaytoUri, + parsePaytoUri } from "@gnu-taler/taler-util"; import { CopyButton, Loading, LocalNotificationBanner, + RouteDefinition, notifyInfo, + useBankCoreApiContext, useLocalNotification, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { ErrorLoadingWithDebug } from "../../components/ErrorLoadingWithDebug.js"; -import { useBankCoreApiContext } from "@gnu-taler/web-util/browser"; import { useAccountDetails } from "../../hooks/account.js"; -import { useSessionState } from "../../hooks/session.js"; import { useBankState } from "../../hooks/bank-state.js"; -import { RouteDefinition } from "@gnu-taler/web-util/browser"; +import { useSessionState } from "../../hooks/session.js"; import { LoginForm } from "../LoginForm.js"; import { ProfileNavigation } from "../ProfileNavigation.js"; import { AccountForm } from "../admin/AccountForm.js"; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx index dd77d609c..255caa375 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx @@ -44,8 +44,8 @@ const accountAuthType = ["none", "basic"]; function isValidURL(s: string): boolean { try { - const u = new URL("/", s) - return true; + const parsed = new URL("/", s); + return parsed instanceof URL; } catch (e) { return false; } @@ -61,43 +61,54 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { credit_facade_credentials: !state.credit_facade_credentials ? undefined : undefinedIfEmpty({ - username: - state.credit_facade_credentials.type === "basic" && !state.credit_facade_credentials.username - ? i18n.str`required` - : undefined, - password: - state.credit_facade_credentials.type === "basic" && !state.credit_facade_credentials.password - ? i18n.str`required` - : undefined, - }), + username: + state.credit_facade_credentials.type === "basic" && + !state.credit_facade_credentials.username + ? i18n.str`required` + : undefined, + password: + state.credit_facade_credentials.type === "basic" && + !state.credit_facade_credentials.password + ? i18n.str`required` + : undefined, + }), credit_facade_url: !state.credit_facade_url ? undefined - : !isValidURL(state.credit_facade_url) ? i18n.str`not valid url` + : !isValidURL(state.credit_facade_url) + ? i18n.str`not valid url` + : undefined, + repeatPassword: !state.credit_facade_credentials + ? undefined + : state.credit_facade_credentials.type === "basic" && + (!state.credit_facade_credentials.password || + state.credit_facade_credentials.password !== state.repeatPassword) + ? i18n.str`is not the same` : undefined, - repeatPassword: - !state.credit_facade_credentials - ? undefined - : state.credit_facade_credentials.type === "basic" && (!state.credit_facade_credentials.password || state.credit_facade_credentials.password !== state.repeatPassword) - ? i18n.str`is not the same` - : undefined, }; const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined, + (k) => (errors as Record<string, unknown>)[k] !== undefined, ); const submitForm = () => { if (hasErrors) return Promise.reject(); - const credit_facade_url = !state.credit_facade_url ? undefined : new URL("/", state.credit_facade_url).href - const credit_facade_credentials: TalerMerchantApi.FacadeCredentials | undefined = - credit_facade_url == undefined ? undefined : - state.credit_facade_credentials?.type === "basic" ? { - type: "basic", - password: state.credit_facade_credentials.password, - username: state.credit_facade_credentials.username, - } : { - type: "none" - } + const credit_facade_url = !state.credit_facade_url + ? undefined + : new URL("/", state.credit_facade_url).href; + const credit_facade_credentials: + | TalerMerchantApi.FacadeCredentials + | undefined = + credit_facade_url == undefined + ? undefined + : state.credit_facade_credentials?.type === "basic" + ? { + type: "basic", + password: state.credit_facade_credentials.password, + username: state.credit_facade_credentials.username, + } + : { + type: "none", + }; return onCreate({ payto_uri: state.payto_uri!, diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx index 3d27b9a1a..96ca8bf5e 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx @@ -19,8 +19,22 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { TalerMerchantApi } from "@gnu-taler/taler-util"; -import { useMerchantApiContext, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { + FacadeCredentials, + HttpStatusCode, + OperationFail, + OperationOk, + TalerError, + TalerMerchantApi, + TalerRevenueHttpClient, + assertUnreachable, + opFixedSuccess, +} from "@gnu-taler/taler-util"; +import { + BrowserFetchHttpLib, + useMerchantApiContext, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; import { useState } from "preact/hooks"; import { NotificationCard } from "../../../../components/menu/index.js"; @@ -45,10 +59,69 @@ export default function CreateValidator({ onConfirm, onBack }: Props): VNode { <NotificationCard notification={notif} /> <CreatePage onBack={onBack} - onCreate={(request: Entity) => { - return api.instance.addBankAccount(state.token, request) + onCreate={async (request: Entity) => { + const revenueAPI = !request.credit_facade_url + ? undefined + : new URL("/", request.credit_facade_url); + + if (revenueAPI) { + const resp = await testRevenueAPI( + revenueAPI, + request.credit_facade_credentials, + ); + if (resp.type === "fail") { + switch (resp.case) { + case "no-config": { + setNotif({ + message: i18n.str`Could not create account`, + type: "ERROR", + description: i18n.str`The endpoint doesn't seems to be a Taler Revenue API`, + }); + return; + } + case "client-bad-request": { + setNotif({ + message: i18n.str`Could not create account`, + type: "ERROR", + description: i18n.str`Server replied with "bad request".`, + }); + return; + } + case "unauthorized": { + setNotif({ + message: i18n.str`Could not create account`, + type: "ERROR", + description: i18n.str`Unauthorized, try with another credentials.`, + }); + return; + } + case "not-found": { + setNotif({ + message: i18n.str`Could not create account`, + type: "ERROR", + description: i18n.str`Check facade URL, server replied with "not found".`, + }); + return; + } + case "error": { + setNotif({ + message: i18n.str`Could not create account`, + type: "ERROR", + description: resp.detail.hint, + }); + return; + } + default: { + assertUnreachable(resp) + } + } + } + } + + return api.instance + .addBankAccount(state.token, request) .then(() => { - onConfirm() + onConfirm(); }) .catch((error) => { setNotif({ @@ -62,3 +135,93 @@ export default function CreateValidator({ onConfirm, onBack }: Props): VNode { </> ); } + +export async function testRevenueAPI( + revenueAPI: URL, + creds: FacadeCredentials | undefined, +): Promise< + | OperationOk<void> + | OperationFail<"no-config"> + | OperationFail<"client-bad-request"> + | OperationFail<"unauthorized"> + | OperationFail<"not-found"> + | OperationFail<"error"> +> { + const api = new TalerRevenueHttpClient( + revenueAPI.href, + new BrowserFetchHttpLib(), + ); + try { + const config = await api.getConfig(); + if (config.type === "fail") { + return { + type: "fail", + case: "no-config", + detail: { + code: 1, + }, + }; + } + } catch (err) { + if (err instanceof TalerError) { + return { + type: "fail", + case: "error", + detail: err.errorDetail, + }; + } + } + if (creds) { + const auth = + creds.type === "basic" + ? { + username: creds.username, + password: creds.password, + } + : undefined; + + try { + const history = await api.getHistory(auth); + if (history.type === "fail") { + switch (history.case) { + case HttpStatusCode.BadRequest: { + return { + type: "fail", + case: "client-bad-request", + detail: { + code: 1, + }, + }; + } + case HttpStatusCode.Unauthorized: { + return { + type: "fail", + case: "unauthorized", + detail: { + code: 1, + }, + }; + } + case HttpStatusCode.NotFound: { + return { + type: "fail", + case: "not-found", + detail: { + code: 1, + }, + }; + } + } + } + } catch (err) { + if (err instanceof TalerError) { + return { + type: "fail", + case: "error", + detail: err.errorDetail, + }; + } + } + } + return opFixedSuccess(undefined); +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/index.tsx index 6b8af50a9..cf1d5b0c7 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/index.tsx @@ -35,6 +35,7 @@ import { Notification } from "../../../../utils/types.js"; import { LoginPage } from "../../../login/index.js"; import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; import { UpdatePage } from "./UpdatePage.js"; +import { testRevenueAPI } from "../create/index.js"; export type Entity = TalerMerchantApi.AccountPatchDetails & WithId; @@ -79,8 +80,65 @@ export default function UpdateValidator({ <UpdatePage account={{ ...result.body, id: bid }} onBack={onBack} - onUpdate={(data) => { - return api.instance.updateBankAccount(state.token, bid, data) + onUpdate={async (request) => { + const revenueAPI = !request.credit_facade_url + ? undefined + : new URL("/", request.credit_facade_url); + + if (revenueAPI) { + const resp = await testRevenueAPI( + revenueAPI, + request.credit_facade_credentials, + ); + if (resp.type === "fail") { + switch (resp.case) { + case "no-config": { + setNotif({ + message: i18n.str`Could not create account`, + type: "ERROR", + description: i18n.str`The endpoint doesn't seems to be a Taler Revenue API`, + }); + return; + } + case "client-bad-request": { + setNotif({ + message: i18n.str`Could not create account`, + type: "ERROR", + description: i18n.str`Server replied with "bad request".`, + }); + return; + } + case "unauthorized": { + setNotif({ + message: i18n.str`Could not create account`, + type: "ERROR", + description: i18n.str`Unauthorized, try with another credentials.`, + }); + return; + } + case "not-found": { + setNotif({ + message: i18n.str`Could not create account`, + type: "ERROR", + description: i18n.str`Check facade URL, server replied with "not found".`, + }); + return; + } + case "error": { + setNotif({ + message: i18n.str`Could not create account`, + type: "ERROR", + description: resp.detail.hint, + }); + return; + } + default: { + assertUnreachable(resp) + } + } + } + } + return api.instance.updateBankAccount(state.token, bid, request) .then(onConfirm) .catch((error) => { setNotif({ diff --git a/packages/taler-util/src/http-client/bank-revenue.ts b/packages/taler-util/src/http-client/bank-revenue.ts index d2f0c7000..b195e8c8f 100644 --- a/packages/taler-util/src/http-client/bank-revenue.ts +++ b/packages/taler-util/src/http-client/bank-revenue.ts @@ -14,9 +14,14 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { HttpRequestLibrary, makeBasicAuthHeader, readTalerErrorResponse } from "../http-common.js"; +import { + HttpRequestLibrary, + makeBasicAuthHeader, + readTalerErrorResponse, +} from "../http-common.js"; import { HttpStatusCode } from "../http-status-codes.js"; import { createPlatformHttpLib } from "../http.js"; +import { LibtoolVersion } from "../libtool-version.js"; import { FailCasesByMethod, ResultByMethod, @@ -27,7 +32,8 @@ import { import { LongPollParams, PaginationParams, - codecForMerchantIncomingHistory, + codecForRevenueConfig, + codecForRevenueIncomingHistory, } from "./types.js"; import { addLongPollingParam, addPaginationParams } from "./utils.js"; @@ -47,50 +53,65 @@ export class TalerRevenueHttpClient { constructor( readonly baseUrl: string, - readonly username: string, httpClient?: HttpRequestLibrary, ) { this.httpLib = httpClient ?? createPlatformHttpLib(); } - // public readonly PROTOCOL_VERSION = "4:0:0"; - // isCompatible(version: string): boolean { - // const compare = LibtoolVersion.compare(this.PROTOCOL_VERSION, version) - // return compare?.compatible ?? false - // } - // /** - // * https://docs.taler.net/core/api-corebank.html#config - // * - // */ - // async getConfig() { - // const url = new URL(`config`, this.baseUrl); - // const resp = await this.httpLib.fetch(url.href, { - // method: "GET" - // }); - // switch (resp.status) { - // case HttpStatusCode.Ok: return opSuccess(resp, codecForCoreBankConfig()) - // default: return opUnknownFailure(resp, await resp.text()) - // } - // } + public readonly PROTOCOL_VERSION = "0:0:0"; + + isCompatible(version: string): boolean { + const compare = LibtoolVersion.compare(this.PROTOCOL_VERSION, version); + return compare?.compatible ?? false; + } + + /** + * https://docs.taler.net/core/api-bank-revenue.html#get--config + * + */ + async getConfig() { + const url = new URL(`config`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + }); + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForRevenueConfig()); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } /** * https://docs.taler.net/core/api-bank-revenue.html#get--history * * @returns */ - async getHistory(auth: string, params?: PaginationParams & LongPollParams) { + async getHistory( + auth: + | { + username: string; + password: string; + } + | undefined, + params?: PaginationParams & LongPollParams, + ) { const url = new URL(`history`, this.baseUrl); addPaginationParams(url, params); addLongPollingParam(url, params); const resp = await this.httpLib.fetch(url.href, { method: "GET", headers: { - Authorization: makeBasicAuthHeader(this.username, auth), + Authorization: auth + ? makeBasicAuthHeader(auth.username, auth.password) + : undefined, }, }); switch (resp.status) { case HttpStatusCode.Ok: - return opSuccessFromHttp(resp, codecForMerchantIncomingHistory()); + return opSuccessFromHttp(resp, codecForRevenueIncomingHistory()); case HttpStatusCode.BadRequest: return opKnownHttpFailure(resp.status, resp); case HttpStatusCode.Unauthorized: diff --git a/packages/taler-util/src/http-client/types.ts b/packages/taler-util/src/http-client/types.ts index ea7ba341b..72189cf0a 100644 --- a/packages/taler-util/src/http-client/types.ts +++ b/packages/taler-util/src/http-client/types.ts @@ -1218,26 +1218,33 @@ export const codecForBankWithdrawalOperationPostResponse = .property("confirm_transfer_url", codecOptional(codecForURL())) .build("TalerBankIntegrationApi.BankWithdrawalOperationPostResponse"); -export const codecForMerchantIncomingHistory = - (): Codec<TalerRevenueApi.MerchantIncomingHistory> => - buildCodecForObject<TalerRevenueApi.MerchantIncomingHistory>() +export const codecForRevenueConfig = (): Codec<TalerRevenueApi.RevenueConfig> => + buildCodecForObject<TalerRevenueApi.RevenueConfig>() + .property("name", codecForConstString("taler-revenue")) + .property("version", codecForString()) + .property("currency", codecForString()) + .property("implementation", codecOptional(codecForString())) + .build("TalerRevenueApi.RevenueConfig"); + +export const codecForRevenueIncomingHistory = + (): Codec<TalerRevenueApi.RevenueIncomingHistory> => + buildCodecForObject<TalerRevenueApi.RevenueIncomingHistory>() .property("credit_account", codecForPaytoString()) .property( "incoming_transactions", - codecForList(codecForMerchantIncomingBankTransaction()), + codecForList(codecForRevenueIncomingBankTransaction()), ) .build("TalerRevenueApi.MerchantIncomingHistory"); -export const codecForMerchantIncomingBankTransaction = - (): Codec<TalerRevenueApi.MerchantIncomingBankTransaction> => - buildCodecForObject<TalerRevenueApi.MerchantIncomingBankTransaction>() - .property("row_id", codecForNumber()) - .property("date", codecForTimestamp) +export const codecForRevenueIncomingBankTransaction = + (): Codec<TalerRevenueApi.RevenueIncomingBankTransaction> => + buildCodecForObject<TalerRevenueApi.RevenueIncomingBankTransaction>() .property("amount", codecForAmountString()) + .property("date", codecForTimestamp) .property("debit_account", codecForPaytoString()) - .property("exchange_url", codecForURL()) - .property("wtid", codecForString()) - .build("TalerRevenueApi.MerchantIncomingBankTransaction"); + .property("row_id", codecForNumber()) + .property("subject", codecForString()) + .build("TalerRevenueApi.RevenueIncomingBankTransaction"); export const codecForTransferResponse = (): Codec<TalerWireGatewayApi.TransferResponse> => @@ -1699,18 +1706,34 @@ export namespace TalerWireGatewayApi { } export namespace TalerRevenueApi { - export interface MerchantIncomingHistory { + export interface RevenueConfig { + // Name of the API. + name: "taler-revenue"; + + // libtool-style representation of the Bank protocol version, see + // https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning + // The format is "current:revision:age". + version: string; + + // Currency used by this gateway. + currency: string; + + // URN of the implementation (needed to interpret 'revision' in version). + // @since v0, may become mandatory in the future. + implementation?: string; + } + + export interface RevenueIncomingHistory { // Array of incoming transactions. - incoming_transactions: MerchantIncomingBankTransaction[]; + incoming_transactions: RevenueIncomingBankTransaction[]; // Payto URI to identify the receiver of funds. - // This must be one of the merchant's bank accounts. // Credit account is shared by all incoming transactions // as per the nature of the request. - credit_account: PaytoString; + credit_account: string; } - export interface MerchantIncomingBankTransaction { + export interface RevenueIncomingBankTransaction { // Opaque identifier of the returned record. row_id: SafeUint64; @@ -1721,13 +1744,10 @@ export namespace TalerRevenueApi { amount: AmountString; // Payto URI to identify the sender of funds. - debit_account: PaytoString; + debit_account: string; - // Base URL of the exchange where the transfer originated form. - exchange_url: string; - - // The wire transfer identifier. - wtid: WireTransferIdentifierRawP; + // The wire transfer subject. + subject: string; } } |