taler-typescript-core

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

commit 88dd48fb841c5303e5eb9c83ae61fe3eb16957e1
parent 2262e53bf60270736dc7583923898074065ed76b
Author: Sebastian <sebasjm@gmail.com>
Date:   Mon, 17 Feb 2025 15:16:57 -0300

fix #9539

Diffstat:
Mpackages/aml-backoffice-ui/src/hooks/server-info.ts | 23+++++++++++++++++++----
Mpackages/aml-backoffice-ui/src/pages/CaseDetails.tsx | 62++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Mpackages/aml-backoffice-ui/src/pages/Cases.tsx | 80++++++++++++++++++++++++++++++++++++++-----------------------------------------
Mpackages/aml-backoffice-ui/src/pages/CreateAccount.tsx | 2+-
Mpackages/aml-backoffice-ui/src/pages/Dashboard.tsx | 93+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Mpackages/aml-backoffice-ui/src/pages/MeasureList.tsx | 31+++++++++++++++++++++++++++----
Mpackages/aml-backoffice-ui/src/pages/Search.tsx | 40+++++++++++++++++++++++-----------------
7 files changed, 251 insertions(+), 80 deletions(-)

diff --git a/packages/aml-backoffice-ui/src/hooks/server-info.ts b/packages/aml-backoffice-ui/src/hooks/server-info.ts @@ -23,6 +23,7 @@ import { opFixedSuccess, PaytoString, TalerCorebankApi, + TalerExchangeErrorsByMethod, TalerExchangeResultByMethod, TalerHttpError, } from "@gnu-taler/taler-util"; @@ -95,12 +96,27 @@ export function useServerStatistics( .map(([name]) => name as AmlEventsName); }, [dialect]); + type Success = TalerExchangeResultByMethod<"getAmlKycStatistics">; + type Error = TalerExchangeErrorsByMethod<"getAmlKycStatistics">; + type Response = { + key: AmlEventsName; + current: Success | Error; + previous: Success | Error | undefined; + }; + async function fetcher([officer, keys, current, previous]: [ OfficerAccount, AmlEventsName[], Timeframe, Timeframe | undefined, ]) { + const firstQuery = await api.getAmlKycStatistics(officer, "test", { + since: current.start, + until: current.end, + }); + if (firstQuery.type === "fail") { + return firstQuery; + } const queries = keys.map((key) => { return Promise.all([ api.getAmlKycStatistics(officer, key, { @@ -114,7 +130,8 @@ export function useServerStatistics( until: previous.end, }), ]).then(([c, p]) => { - return { key, current: c.body, previous: p?.body }; + const ret: Response = { key, current: c, previous: p }; + return ret; }); }); @@ -124,9 +141,7 @@ export function useServerStatistics( } const { data, error } = useSWR< - OperationOk< - { key: AmlEventsName; current: EventCounter; previous?: EventCounter }[] - >, + OperationOk<Response[]> | Error, TalerHttpError >(!session ? undefined : [session, keys, current, previous], fetcher, { refreshInterval: 0, diff --git a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx @@ -532,21 +532,44 @@ function ShowMesaureInfo({ nextMeasures }: { nextMeasures: string[] }): VNode { } if (measures.type === "fail") { switch (measures.case) { - // case HttpStatusCode.Unauthorized: case HttpStatusCode.Forbidden: return ( <Fragment> <Attention type="danger" title={i18n.str`Operation denied`}> <i18n.Translate> - This account signature is wrong, contact administrator or create - a new one. + This account signature is invalid, contact administrator or + create a new one. + </i18n.Translate> + </Attention> + <Officer /> + </Fragment> + ); + case HttpStatusCode.NotFound: + return ( + <Fragment> + <Attention type="danger" title={i18n.str`Operation denied`}> + <i18n.Translate> + The designated AML account is not known, contact administrator + or create a new one. + </i18n.Translate> + </Attention> + <Officer /> + </Fragment> + ); + case HttpStatusCode.Conflict: + return ( + <Fragment> + <Attention type="danger" title={i18n.str`Operation denied`}> + <i18n.Translate> + The designated AML account is not enabled, contact administrator + or create a new one. </i18n.Translate> </Attention> <Officer /> </Fragment> ); default: - assertUnreachable(measures.case); + assertUnreachable(measures); } } const filteredMeasures = nextMeasures.filter((n) => !!n && !!n.trim()); @@ -1233,21 +1256,44 @@ export function ShowMeasuresToSelect({ } if (measures.type === "fail") { switch (measures.case) { - // case HttpStatusCode.Unauthorized: case HttpStatusCode.Forbidden: return ( <Fragment> <Attention type="danger" title={i18n.str`Operation denied`}> <i18n.Translate> - This account signature is wrong, contact administrator or create - a new one. + This account signature is invalid, contact administrator or + create a new one. + </i18n.Translate> + </Attention> + <Officer /> + </Fragment> + ); + case HttpStatusCode.NotFound: + return ( + <Fragment> + <Attention type="danger" title={i18n.str`Operation denied`}> + <i18n.Translate> + The designated AML account is not known, contact administrator + or create a new one. + </i18n.Translate> + </Attention> + <Officer /> + </Fragment> + ); + case HttpStatusCode.Conflict: + return ( + <Fragment> + <Attention type="danger" title={i18n.str`Operation denied`}> + <i18n.Translate> + The designated AML account is not enabled, contact administrator + or create a new one. </i18n.Translate> </Attention> <Officer /> </Fragment> ); default: - assertUnreachable(measures.case); + assertUnreachable(measures); } } diff --git a/packages/aml-backoffice-ui/src/pages/Cases.tsx b/packages/aml-backoffice-ui/src/pages/Cases.tsx @@ -207,44 +207,42 @@ export function Cases({ if (list.type === "fail") { switch (list.case) { - case HttpStatusCode.Forbidden: { + case HttpStatusCode.Forbidden: return ( <Fragment> <Attention type="danger" title={i18n.str`Operation denied`}> <i18n.Translate> - This account signature is wrong, contact administrator or create - a new one. + This account signature is invalid, contact administrator or + create a new one. </i18n.Translate> </Attention> <Officer /> </Fragment> ); - } - case HttpStatusCode.NotFound: { + case HttpStatusCode.NotFound: return ( <Fragment> <Attention type="danger" title={i18n.str`Operation denied`}> - <i18n.Translate>This account is not known.</i18n.Translate> + <i18n.Translate> + The designated AML account is not known, contact administrator + or create a new one. + </i18n.Translate> </Attention> <Officer /> </Fragment> ); - } case HttpStatusCode.Conflict: - { - return ( - <Fragment> - <Attention type="danger" title={i18n.str`Operation denied`}> - <i18n.Translate> - This account doesn't have access. Request account activation - sending your public key. - </i18n.Translate> - </Attention> - <Officer /> - </Fragment> - ); - } - return <Officer />; + return ( + <Fragment> + <Attention type="danger" title={i18n.str`Operation denied`}> + <i18n.Translate> + The designated AML account is not enabled, contact administrator + or create a new one. + </i18n.Translate> + </Attention> + <Officer /> + </Fragment> + ); default: assertUnreachable(list); } @@ -281,44 +279,42 @@ export function CasesUnderInvestigation({ if (list.type === "fail") { switch (list.case) { - case HttpStatusCode.Forbidden: { + case HttpStatusCode.Forbidden: return ( <Fragment> <Attention type="danger" title={i18n.str`Operation denied`}> <i18n.Translate> - This account signature is wrong, contact administrator or create - a new one. + This account signature is invalid, contact administrator or + create a new one. </i18n.Translate> </Attention> <Officer /> </Fragment> ); - } - case HttpStatusCode.NotFound: { + case HttpStatusCode.NotFound: return ( <Fragment> <Attention type="danger" title={i18n.str`Operation denied`}> - <i18n.Translate>This account is not known.</i18n.Translate> + <i18n.Translate> + The designated AML account is not known, contact administrator + or create a new one. + </i18n.Translate> </Attention> <Officer /> </Fragment> ); - } case HttpStatusCode.Conflict: - { - return ( - <Fragment> - <Attention type="danger" title={i18n.str`Operation denied`}> - <i18n.Translate> - This account doesn't have access. Request account activation - sending your public key. - </i18n.Translate> - </Attention> - <Officer /> - </Fragment> - ); - } - return <Officer />; + return ( + <Fragment> + <Attention type="danger" title={i18n.str`Operation denied`}> + <i18n.Translate> + The designated AML account is not enabled, contact administrator + or create a new one. + </i18n.Translate> + </Attention> + <Officer /> + </Fragment> + ); default: assertUnreachable(list); } diff --git a/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx b/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx @@ -70,7 +70,7 @@ const createAccountForm = ( label: i18n.str`Repeat password`, required: true, validator(value) { - return value + return !value ? i18n.str`required` : // : state.password !== value // ? i18n.str`doesn't match` diff --git a/packages/aml-backoffice-ui/src/pages/Dashboard.tsx b/packages/aml-backoffice-ui/src/pages/Dashboard.tsx @@ -17,12 +17,19 @@ import { AbsoluteTime, AmlSpaDialect, assertUnreachable, + HttpStatusCode, + OperationFail, + OperationOk, TalerCorebankApi, TalerError, + TalerExchangeErrorsByMethod, + TalerExchangeResultByMethod, TranslatedString, } from "@gnu-taler/taler-util"; import { + Attention, InternationalizationAPI, + Loading, RouteDefinition, useExchangeApiContext, useTranslationContext, @@ -36,6 +43,7 @@ import { usePreferences } from "../hooks/preferences.js"; import { useServerStatistics } from "../hooks/server-info.js"; import { AmlEventsName } from "./decision/aml-events.js"; import { HandleAccountNotReady } from "./HandleAccountNotReady.js"; +import { Officer } from "./Officer.js"; export function Dashboard({ routeToDownloadStats, @@ -85,11 +93,86 @@ function EventMetrics({ const resp = useServerStatistics(dialect, params.current, params.previous); - if (!resp) return <Fragment />; + if (!resp) { + return <Loading />; + } if (resp instanceof TalerError) { return <ErrorLoadingWithDebug error={resp} />; } + // the request test failed? + const failReponse = resp.type === "fail" ? resp : undefined; + + // request test good, but did one of the event metric request failed? + const oneOftheRequestFailed = + resp.type === "ok" + ? resp.body.find( + (r) => + r.current.type === "fail" || + (r.previous && r.previous.type === "fail"), + ) + : undefined; + + // collect error on a single variable + const error = failReponse + ? failReponse + : oneOftheRequestFailed + ? oneOftheRequestFailed.current.type === "fail" + ? oneOftheRequestFailed.current + : (oneOftheRequestFailed.previous as TalerExchangeErrorsByMethod<"getAmlKycStatistics">) + : undefined; + + //check + if (error) { + switch (error.case) { + case HttpStatusCode.Forbidden: + return ( + <Fragment> + <Attention type="danger" title={i18n.str`Operation denied`}> + <i18n.Translate> + This account signature is invalid, contact administrator or + create a new one. + </i18n.Translate> + </Attention> + <Officer /> + </Fragment> + ); + case HttpStatusCode.NotFound: + return ( + <Fragment> + <Attention type="danger" title={i18n.str`Operation denied`}> + <i18n.Translate> + The designated AML account is not known, contact administrator + or create a new one. + </i18n.Translate> + </Attention> + <Officer /> + </Fragment> + ); + case HttpStatusCode.Conflict: + return ( + <Fragment> + <Attention type="danger" title={i18n.str`Operation denied`}> + <i18n.Translate> + The designated AML account is not enabled, contact administrator + or create a new one. + </i18n.Translate> + </Attention> + <Officer /> + </Fragment> + ); + default: + assertUnreachable(error); + } + } + const result = resp as OperationOk< + { + key: AmlEventsName; + current: TalerExchangeResultByMethod<"getAmlKycStatistics">; + previous: TalerExchangeResultByMethod<"getAmlKycStatistics"> | undefined; + }[] + >; + return ( <div class="px-4 mt-4"> <div class="sm:flex sm:items-center mb-4"> @@ -117,9 +200,11 @@ function EventMetrics({ </div> <dl class="mt-5 grid grid-cols-1 md:grid-cols-4 divide-y divide-gray-200 overflow-hidden rounded-lg bg-white shadow-lg md:divide-x md:divide-y-0"> - {resp.body.map((ev) => { + {result.body.map((ev) => { const label = labelForEvent(ev.key, i18n); const desc = descriptionForEvent(ev.key, i18n); + if (ev.current.type === "fail") return undefined; + if (ev.previous?.type === "fail") return undefined; return ( <div class="px-4 py-5 sm:p-6"> <dt class="text-base font-normal text-gray-900"> @@ -129,8 +214,8 @@ function EventMetrics({ )} </dt> <MetricValueNumber - current={ev.current.counter} - previous={ev.previous?.counter} + current={ev.current.body.counter} + previous={ev.previous?.body.counter} /> </div> ); diff --git a/packages/aml-backoffice-ui/src/pages/MeasureList.tsx b/packages/aml-backoffice-ui/src/pages/MeasureList.tsx @@ -47,21 +47,44 @@ export function MeasureList({ routeToNew }: { routeToNew: RouteDefinition }) { if (measures.type === "fail") { switch (measures.case) { - // case HttpStatusCode.Unauthorized: case HttpStatusCode.Forbidden: return ( <Fragment> <Attention type="danger" title={i18n.str`Operation denied`}> <i18n.Translate> - This account signature is wrong, contact administrator or create - a new one. + This account signature is invalid, contact administrator or + create a new one. + </i18n.Translate> + </Attention> + <Officer /> + </Fragment> + ); + case HttpStatusCode.NotFound: + return ( + <Fragment> + <Attention type="danger" title={i18n.str`Operation denied`}> + <i18n.Translate> + The designated AML account is not known, contact administrator + or create a new one. + </i18n.Translate> + </Attention> + <Officer /> + </Fragment> + ); + case HttpStatusCode.Conflict: + return ( + <Fragment> + <Attention type="danger" title={i18n.str`Operation denied`}> + <i18n.Translate> + The designated AML account is not enabled, contact administrator + or create a new one. </i18n.Translate> </Attention> <Officer /> </Fragment> ); default: - assertUnreachable(measures.case); + assertUnreachable(measures); } } diff --git a/packages/aml-backoffice-ui/src/pages/Search.tsx b/packages/aml-backoffice-ui/src/pages/Search.tsx @@ -53,6 +53,7 @@ import { useOfficer } from "../hooks/officer.js"; import { privatePages } from "../Routing.js"; import { Pagination, ToInvestigateIcon } from "./Cases.js"; import { HandleAccountNotReady } from "./HandleAccountNotReady.js"; +import { Officer } from "./Officer.js"; export function Search() { const officer = useOfficer(); @@ -60,6 +61,10 @@ export function Search() { const [paytoUri, setPayto] = useState<PaytoUri | undefined>(undefined); + if (officer.state !== "ready") { + return <HandleAccountNotReady officer={officer} />; + } + const design: FormDesign = { type: "single-column", fields: paytoTypeField(i18n), @@ -70,10 +75,6 @@ export function Search() { // createFormValidator(i18n), ); - if (officer.state !== "ready") { - return <HandleAccountNotReady officer={officer} />; - } - return ( <div> <h1 class="my-2 text-3xl font-bold tracking-tight text-gray-900 "> @@ -130,42 +131,44 @@ function ShowResult({ payto }: { payto: PaytoUri }): VNode { } if (history.type === "fail") { switch (history.case) { - case HttpStatusCode.Forbidden: { + case HttpStatusCode.Forbidden: return ( <Fragment> <Attention type="danger" title={i18n.str`Operation denied`}> <i18n.Translate> - This account signature is wrong, contact administrator or create - a new one. + This account signature is invalid, contact administrator or + create a new one. </i18n.Translate> </Attention> + <Officer /> </Fragment> ); - } - case HttpStatusCode.Conflict: { + case HttpStatusCode.NotFound: return ( <Fragment> <Attention type="danger" title={i18n.str`Operation denied`}> <i18n.Translate> - This account doesn't have access. Request account activation - sending your public key. + The designated AML account is not known, contact administrator + or create a new one. </i18n.Translate> </Attention> + <Officer /> </Fragment> ); - } - case HttpStatusCode.NotFound: { + case HttpStatusCode.Conflict: return ( <Fragment> <Attention type="danger" title={i18n.str`Operation denied`}> - <i18n.Translate>This account is not known.</i18n.Translate> + <i18n.Translate> + The designated AML account is not enabled, contact administrator + or create a new one. + </i18n.Translate> </Attention> + <Officer /> </Fragment> ); - } - default: { + default: assertUnreachable(history); - } } } @@ -610,6 +613,9 @@ function validateIBAN( iban: string, i18n: ReturnType<typeof useTranslationContext>["i18n"], ): TranslatedString | undefined { + if (!iban) { + return i18n.str`required`; + } // Check total length if (iban.length < 4) return i18n.str`IBAN numbers usually have more that 4 digits`;