taler-typescript-core

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

commit 48f93fe5c61fc1de07a54f284c0b676ced70f90d
parent c9c4b6f06c57d42835c6f4cecb1371f7a884763b
Author: Sebastian <sebasjm@gmail.com>
Date:   Thu, 22 May 2025 11:23:15 -0300

wip #9988

Diffstat:
Mpackages/aml-backoffice-ui/src/hooks/decision-request.ts | 4++++
Mpackages/aml-backoffice-ui/src/hooks/decisions.ts | 12+-----------
Mpackages/aml-backoffice-ui/src/hooks/transfers.ts | 46----------------------------------------------
Mpackages/aml-backoffice-ui/src/pages/CaseDetails.tsx | 10+++++-----
Mpackages/aml-backoffice-ui/src/pages/decision/AmlDecisionRequestWizard.tsx | 8++++----
Mpackages/aml-backoffice-ui/src/pages/decision/Events.tsx | 52+++++++++++++++++++++++++---------------------------
Mpackages/aml-backoffice-ui/src/pages/decision/Justification.tsx | 33+++++----------------------------
Mpackages/aml-backoffice-ui/src/pages/decision/Properties.tsx | 33++++-----------------------------
Mpackages/aml-backoffice-ui/src/pages/decision/Rules.tsx | 33++++++++-------------------------
Mpackages/taler-util/src/aml/events.ts | 3++-
10 files changed, 58 insertions(+), 176 deletions(-)

diff --git a/packages/aml-backoffice-ui/src/hooks/decision-request.ts b/packages/aml-backoffice-ui/src/hooks/decision-request.ts @@ -30,6 +30,7 @@ import { codecOptionalDefault, KycRule, MeasureInformation, + TalerExchangeApi, } from "@gnu-taler/taler-util"; import { buildStorageKey, @@ -51,6 +52,7 @@ export interface AccountAttributes { * With all of this we need to create a AmlDecisionRequest */ export interface DecisionRequest { + original: TalerExchangeApi.AmlDecision | undefined; /** * Next legitimization rules */ @@ -124,6 +126,7 @@ export const codecForAccountAttributes = (): Codec<AccountAttributes> => export const codecForDecisionRequest = (): Codec<DecisionRequest> => buildCodecForObject<DecisionRequest>() + .property("original", codecOptional(codecForAny())) .property("rules", codecOptional(codecForList(codecForKycRules()))) .property("deadline", codecOptional(codecForAbsoluteTime)) .property("properties", codecOptional(codecForMap(codecForAny()))) @@ -143,6 +146,7 @@ export const codecForDecisionRequest = (): Codec<DecisionRequest> => .build("DecisionRequest"); const DECISION_REQUEST_EMPTY: DecisionRequest = { + original: undefined, deadline: undefined, custom_properties: undefined, onExpire_measure: undefined, diff --git a/packages/aml-backoffice-ui/src/hooks/decisions.ts b/packages/aml-backoffice-ui/src/hooks/decisions.ts @@ -144,17 +144,7 @@ export function useAccountDecisions(accountStr: string) { * @param args * @returns */ -export function useAccountActiveDecision(accountStr?: string): - | OperationFail<HttpStatusCode.NotFound> - | OperationFail<HttpStatusCode.Forbidden> - | OperationFail<HttpStatusCode.Conflict> - | TalerError<{ - requestUrl: string; - requestMethod: string; - }> - | OperationOk<undefined> - | OperationOk<AmlDecision> - | undefined { +export function useAccountActiveDecision(accountStr?: string) { const officer = useOfficer(); const session = accountStr !== undefined && officer.state === "ready" diff --git a/packages/aml-backoffice-ui/src/hooks/transfers.ts b/packages/aml-backoffice-ui/src/hooks/transfers.ts @@ -139,52 +139,6 @@ export function useTransferList({ ); } -/** - * @param account - * @param args - * @returns - */ -export function useAccountActiveDecision(accountStr?: string) { - const officer = useOfficer(); - const session = - accountStr !== undefined && officer.state === "ready" - ? officer.account - : undefined; - const { - lib: { exchange: api }, - } = useExchangeApiContext(); - - const [offset, setOffset] = useState<string>(); - - async function fetcher([officer, account, offset]: [ - OfficerAccount, - string, - string | undefined, - ]) { - return await api.getAmlDecisions(officer, { - order: "dec", - offset, - account, - active: true, - limit: PAGINATED_LIST_REQUEST, - }); - } - - const { data, error } = useSWR< - TalerExchangeResultByMethod<"getAmlDecisions">, - TalerHttpError - >( - !session ? undefined : [session, accountStr, offset, "getAmlDecisions"], - fetcher, - ); - - if (error) return error; - if (data === undefined) return undefined; - if (data.type !== "ok") return data; - - if (!data.body.records.length) return opFixedSuccess(undefined); - return opFixedSuccess(data.body.records[0]); -} type PaginatedResult<T> = OperationOk<T> & { isLastPage: boolean; diff --git a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx @@ -129,8 +129,6 @@ export function CaseDetails({ routeToShowCollectedInfo: RouteDefinition<{ cid: string; rowId: string }>; account: string; }) { - const [selected, setSelected] = useState<AbsoluteTime | undefined>(undefined); //AbsoluteTime.now()); - const { i18n } = useTranslationContext(); const details = useAccountInformation(account); const history = useAccountDecisions(account); @@ -174,6 +172,7 @@ export function CaseDetails({ // const events = getEventsFromAmlHistory(accountDetails, i18n); + function ShortcutActionButtons(): VNode { return ( <div> @@ -183,8 +182,9 @@ export function CaseDetails({ // instead here all the values from the current decision should be // loaded into the new decision request, like we are doing with e // custom measures - // FIXME add properties, limits, investigation state + // FIXME-do-this add properties, limits, investigation state onNewDecision({ + original: activeDecision, custom_measures: activeDecision?.limits.custom_measures, }); }} @@ -217,9 +217,9 @@ export function CaseDetails({ <ShortcutActionButtons /> - {selected && ( + {/* {selected && ( <ShowConsolidated history={collectionEvents} until={selected} /> - )} + )} */} <div class="p-4"> <h1 class="text-base font-semibold leading-6 text-black"> <i18n.Translate>Collected information</i18n.Translate> diff --git a/packages/aml-backoffice-ui/src/pages/decision/AmlDecisionRequestWizard.tsx b/packages/aml-backoffice-ui/src/pages/decision/AmlDecisionRequestWizard.tsx @@ -120,15 +120,15 @@ export function AmlDecisionRequestWizard({ const content = (function () { switch (stepOrDefault) { case "rules": - return <Rules account={account} newPayto={newPayto} />; + return <Rules newPayto={newPayto} />; case "properties": - return <Properties account={account} />; + return <Properties />; case "events": - return <Events account={account} />; + return <Events />; case "measures": return <Measures />; case "justification": - return <Justification account={account} newPayto={newPayto} />; + return <Justification newPayto={newPayto} />; case "attributes": return <Attributes formId={formId} />; case "summary": diff --git a/packages/aml-backoffice-ui/src/pages/decision/Events.tsx b/packages/aml-backoffice-ui/src/pages/decision/Events.tsx @@ -4,16 +4,14 @@ import { assertUnreachable, EventsDerivation_TOPS, GLS_AmlEventsName, - HttpStatusCode, MeasureInformation, - TalerError, + TalerFormAttributes, TOPS_AmlEventsName, } from "@gnu-taler/taler-util"; import { FormDesign, FormUI, InternationalizationAPI, - Loading, onComponentUnload, SelectUiChoice, useExchangeApiContext, @@ -21,12 +19,10 @@ import { useTranslationContext, } from "@gnu-taler/web-util/browser"; import { h, VNode } from "preact"; -import { ErrorLoadingWithDebug } from "../../components/ErrorLoadingWithDebug.js"; import { DecisionRequest, useCurrentDecisionRequest, } from "../../hooks/decision-request.js"; -import { useAccountActiveDecision } from "../../hooks/decisions.js"; import { usePreferences } from "../../hooks/preferences.js"; /** @@ -35,8 +31,7 @@ import { usePreferences } from "../../hooks/preferences.js"; * @param param0 * @returns */ -export function Events({ account }: { account: string }): VNode { - const activeDecision = useAccountActiveDecision(account); +export function Events({}: {}): VNode { const { i18n } = useTranslationContext(); const [request, updateRequest] = useCurrentDecisionRequest(); const [pref] = usePreferences(); @@ -46,22 +41,22 @@ export function Events({ account }: { account: string }): VNode { (pref.testingDialect ? undefined : config.config.aml_spa_dialect) ?? AmlSpaDialect.TESTING; - if (!activeDecision) { - return <Loading />; - } - if (activeDecision instanceof TalerError) { - return <ErrorLoadingWithDebug error={activeDecision} />; - } - if (activeDecision.type === "fail") { - switch (activeDecision.case) { - case HttpStatusCode.Forbidden: - case HttpStatusCode.NotFound: - case HttpStatusCode.Conflict: - return <div>couldn't load the last active decision</div>; - default: - assertUnreachable(activeDecision); - } - } + // if (!activeDecision) { + // return <Loading />; + // } + // if (activeDecision instanceof TalerError) { + // return <ErrorLoadingWithDebug error={activeDecision} />; + // } + // if (activeDecision.type === "fail") { + // switch (activeDecision.case) { + // case HttpStatusCode.Forbidden: + // case HttpStatusCode.NotFound: + // case HttpStatusCode.Conflict: + // return <div>couldn't load the last active decision</div>; + // default: + // assertUnreachable(activeDecision); + // } + // } function ShowEventForm({ events: calculatedEvents }: { events: Events }) { const design = formDesign(i18n, calculatedEvents); @@ -86,7 +81,7 @@ export function Events({ account }: { account: string }): VNode { }); onComponentUnload(() => { - updateRequest("onload event",{ + updateRequest("onload event", { custom_events: !form.status.result.custom ? [] : form.status.result.custom, @@ -105,7 +100,7 @@ export function Events({ account }: { account: string }): VNode { } const events = calculateEventsBasedOnState( - activeDecision.body, + request.original, request, i18n, dialect, @@ -223,7 +218,7 @@ function labelForEvent_tops( case TOPS_AmlEventsName.MROS_REPORTED_SUSPICION_SUBSTANTIATED: case TOPS_AmlEventsName.INCR_INVESTIGATION_CONCLUDED: case TOPS_AmlEventsName.DECR_INVESTIGATION_CONCLUDED: - // case TOPS_AmlEventsName.ACCOUNT_OPENED: + // case TOPS_AmlEventsName.ACCOUNT_OPENED: return i18n.str`Account opened`; // case TOPS_AmlEventsName.ACCOUNT_CLOSED: // return i18n.str`Account closed`; @@ -283,14 +278,17 @@ function calculateEventsBasedOnState( dialect: AmlSpaDialect, ): Events { const init: Events = { triggered: [], rest: [] }; + const form = (request.attributes ?? {}) as Record<string, unknown>; + const formId = form[TalerFormAttributes.FORM_ID]! as string; return Object.entries(EventsDerivation_TOPS).reduce((prev, [name, info]) => { if ( info.shouldBeTriggered( + formId, currentState?.limits?.rules, request.rules, currentState?.properties, request.properties, - (request.attributes ?? {}) as Record<string, unknown>, + form, ) ) { prev.triggered.push(name); diff --git a/packages/aml-backoffice-ui/src/pages/decision/Justification.tsx b/packages/aml-backoffice-ui/src/pages/decision/Justification.tsx @@ -1,59 +1,36 @@ -import { AmlDecision, PaytoString, TalerError } from "@gnu-taler/taler-util"; +import { PaytoString } from "@gnu-taler/taler-util"; import { FormDesign, FormUI, InternationalizationAPI, - Loading, onComponentUnload, useForm, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { h, VNode } from "preact"; import { useCurrentDecisionRequest } from "../../hooks/decision-request.js"; -import { useAccountActiveDecision } from "../../hooks/decisions.js"; /** * Mark for further investigation and explain decision * @param param0 * @returns */ -export function Justification({ - account, - newPayto, -}: { - account: string; - newPayto?: PaytoString; -}): VNode { +export function Justification({ newPayto }: { newPayto?: PaytoString }): VNode { const isNewAccount = !!newPayto; - const activeDecision = useAccountActiveDecision(isNewAccount ? undefined :account); - const info = - !activeDecision || - activeDecision instanceof TalerError || - activeDecision.type === "fail" - ? undefined - : activeDecision.body; - - if (!info && !isNewAccount) { - return <Loading />; - } - - return <JustificationForm isNewAccount={isNewAccount} info={info}/> -} - -function JustificationForm({info, isNewAccount}:{info: AmlDecision | undefined, isNewAccount: boolean}):VNode { const { i18n } = useTranslationContext(); const [request, updateRequest] = useCurrentDecisionRequest(); const design = formDesign(i18n, isNewAccount); const form = useForm<FormType>(design, { - investigate: request.keep_investigating ?? info?.to_investigate ?? false, + investigate: + request.keep_investigating ?? request.original?.to_investigate ?? false, justification: request.justification, accountName: request.accountName, }); onComponentUnload(() => { - updateRequest("unload justification",{ + updateRequest("unload justification", { keep_investigating: !!form.status.result.investigate, justification: form.status.result.justification ?? "", accountName: form.status.result.accountName ?? "", diff --git a/packages/aml-backoffice-ui/src/pages/decision/Properties.tsx b/packages/aml-backoffice-ui/src/pages/decision/Properties.tsx @@ -1,36 +1,30 @@ import { AccountProperties, AmlSpaDialect, - assertUnreachable, GLS_AccountProperties, GLS_AML_PROPERTIES, - HttpStatusCode, LegitimizationRuleSet, PropertiesDerivation_TOPS, PropertiesDerivationFunctionByPropertyName, TalerAmlProperties, - TalerError, TOPS_AccountProperties } from "@gnu-taler/taler-util"; import { FormDesign, FormUI, InternationalizationAPI, - Loading, onComponentUnload, UIFormElementConfig, UIHandlerId, useExchangeApiContext, useForm, - useTranslationContext, + useTranslationContext } from "@gnu-taler/web-util/browser"; import { h, VNode } from "preact"; -import { ErrorLoadingWithDebug } from "../../components/ErrorLoadingWithDebug.js"; import { DecisionRequest, useCurrentDecisionRequest, } from "../../hooks/decision-request.js"; -import { useAccountActiveDecision } from "../../hooks/decisions.js"; import { usePreferences } from "../../hooks/preferences.js"; import { DEFAULT_LIMITS_WHEN_NEW_ACCOUNT } from "./Rules.js"; @@ -39,31 +33,12 @@ import { DEFAULT_LIMITS_WHEN_NEW_ACCOUNT } from "./Rules.js"; * @param param0 * @returns */ -export function Properties({ account }: { account: string }): VNode { - const activeDecision = useAccountActiveDecision(account); +export function Properties({}: {}): VNode { const [request] = useCurrentDecisionRequest(); const { config } = useExchangeApiContext(); const [pref] = usePreferences(); - if (!activeDecision) { - return <Loading />; - } - if (activeDecision instanceof TalerError) { - return <ErrorLoadingWithDebug error={activeDecision} />; - } - if (activeDecision.type === "fail") { - switch (activeDecision.case) { - // case HttpStatusCode.Unauthorized: - case HttpStatusCode.Forbidden: - case HttpStatusCode.NotFound: - case HttpStatusCode.Conflict: - return <div />; - default: - assertUnreachable(activeDecision); - } - } - - const lastDecision = activeDecision.body; + const lastDecision = request.original; const dialect = (pref.testingDialect ? undefined : config.config.aml_spa_dialect) ?? @@ -106,7 +81,7 @@ function ReloadForm({ merged }: { merged: any }): VNode { }); onComponentUnload(() => { - updateRequest("unload properties",{ + updateRequest("unload properties", { properties: (form.status.result.defined ?? {}) as Record<string, boolean>, custom_properties: (form.status.result.custom ?? []).reduce( (prev, cur) => { diff --git a/packages/aml-backoffice-ui/src/pages/decision/Rules.tsx b/packages/aml-backoffice-ui/src/pages/decision/Rules.tsx @@ -120,17 +120,13 @@ export function findRuleInconsistency( * @returns */ export function Rules({ - account, newPayto, }: { - account: string; newPayto?: PaytoString; }): VNode { const { i18n } = useTranslationContext(); const { config } = useExchangeApiContext(); - const isNewAccount = !!newPayto; - let newPaytoParsed: PaytoUri | undefined; const isNewAccountAWallet = newPayto === undefined @@ -146,38 +142,25 @@ export function Rules({ !measures || measures instanceof TalerError || measures.type === "fail" ? undefined : measures.body.roots; - - const activeDecision = useAccountActiveDecision( - isNewAccount ? undefined : account, - ); - const info = - !activeDecision || - activeDecision instanceof TalerError || - activeDecision.type === "fail" - ? undefined - : activeDecision.body; - - if (!info && !isNewAccount) { - return <Loading />; - } + const [request] = useCurrentDecisionRequest() // info may be undefined if this is a new account // for which we use the payto:// parameter - const isWallet = info?.is_wallet ?? isNewAccountAWallet; + const isWallet = request.original?.is_wallet ?? isNewAccountAWallet; return ( <div> <UpdateRulesForm rootMeasures={rootMeasures} config={config.config} isWallet={isWallet ?? false} - limits={info?.limits ?? DEFAULT_LIMITS_WHEN_NEW_ACCOUNT} + limits={request.original?.limits ?? DEFAULT_LIMITS_WHEN_NEW_ACCOUNT} /> <div> <h2 class="mt-4 mb-2"> <i18n.Translate>Current active rules</i18n.Translate> </h2> - {info === undefined ? ( + {request.original === undefined ? ( <p> <i18n.Translate> There are no rules for this account. @@ -186,13 +169,13 @@ export function Rules({ ) : ( <ShowDecisionLimitInfo fixed - since={AbsoluteTime.fromProtocolTimestamp(info.decision_time)} + since={AbsoluteTime.fromProtocolTimestamp(request.original.decision_time)} until={AbsoluteTime.fromProtocolTimestamp( - info.limits.expiration_time, + request.original.limits.expiration_time, )} - rules={info.limits.rules} + rules={request.original.limits.rules} startOpen - measure={info.limits.successor_measure ?? ""} + measure={request.original.limits.successor_measure ?? ""} /> )} </div> diff --git a/packages/taler-util/src/aml/events.ts b/packages/taler-util/src/aml/events.ts @@ -1,7 +1,8 @@ import { Amounts } from "../amounts.js"; -import { isOneOf, TalerAmlProperties } from "../index.js"; import { LimitOperationType } from "../types-taler-exchange.js"; import { AccountProperties, KycRule } from "../types-taler-kyc-aml.js"; +import { TalerAmlProperties } from "../taler-account-properties.js"; +import { isOneOf } from "./properties.js"; /** * List of events triggered by TOPS