taler-typescript-core

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

commit 2cbb2d4405890418a612d30619b1eb4a4369a475
parent 9a2cfef15cd3989eaac0691b3a0d3915efa3392d
Author: Sebastian <sebasjm@gmail.com>
Date:   Wed, 26 Mar 2025 18:22:35 -0300

calculate events

Diffstat:
Mpackages/aml-backoffice-ui/src/forms/index.ts | 12++++++------
Mpackages/aml-backoffice-ui/src/forms/simplest.ts | 6+++---
Mpackages/aml-backoffice-ui/src/hooks/decision-request.ts | 6++++++
Mpackages/aml-backoffice-ui/src/pages/CaseDetails.tsx | 1+
Mpackages/aml-backoffice-ui/src/pages/UnlockAccount.tsx | 2+-
Mpackages/aml-backoffice-ui/src/pages/decision/AmlDecisionRequestWizard.tsx | 2+-
Mpackages/aml-backoffice-ui/src/pages/decision/Events.tsx | 199++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Mpackages/aml-backoffice-ui/src/pages/decision/Properties.tsx | 5-----
Mpackages/aml-backoffice-ui/src/pages/decision/Summary.tsx | 10+++++++---
9 files changed, 142 insertions(+), 101 deletions(-)

diff --git a/packages/aml-backoffice-ui/src/forms/index.ts b/packages/aml-backoffice-ui/src/forms/index.ts @@ -29,12 +29,12 @@ import { v1 as simplest } from "./simplest.js"; export const preloadedForms: ( i18n: InternationalizationAPI, ) => Array<FormMetadata> = (i18n) => [ - { - label: i18n.str`Simple comment`, - id: "__simple_comment", - version: 1, - config: simplest(i18n), - }, + // { + // label: i18n.str`Simple comment`, + // id: "__simple_comment", + // version: 1, + // config: simplest(i18n, ), + // }, form_vqf_902_1_customer(i18n), { label: i18n.str`Identification Form (acceptance)`, diff --git a/packages/aml-backoffice-ui/src/forms/simplest.ts b/packages/aml-backoffice-ui/src/forms/simplest.ts @@ -29,7 +29,7 @@ export const v1 = (i18n: InternationalizationAPI): DoubleColumnFormDesign => ({ fields: [ { type: "textArea", - id: "comment", + id: "comment" as UIHandlerId, label: i18n.str`Comment`, }, ], @@ -58,7 +58,7 @@ export function resolutionSection( fields: [ { type: "choiceHorizontal", - id: "state", + id: "state" as UIHandlerId, label: i18n.str`New state`, converterId: "TalerExchangeApi.AmlState", choices: [ @@ -78,7 +78,7 @@ export function resolutionSection( }, { type: "amount", - id: "threshold", + id: "threshold" as UIHandlerId, currency: "NETZBON", converterId: "Taler.Amount", label: i18n.str`New threshold`, diff --git a/packages/aml-backoffice-ui/src/hooks/decision-request.ts b/packages/aml-backoffice-ui/src/hooks/decision-request.ts @@ -90,6 +90,10 @@ export interface DecisionRequest { */ custom_properties: Record<string, any> | undefined; /** + * Supported events to be triggered + */ + triggering_events: string[] | undefined; + /** * Custom unsupported events to be triggered */ custom_events: string[] | undefined; @@ -113,6 +117,7 @@ export const codecForDecisionRequest = (): Codec<DecisionRequest> => .property("custom_properties", codecForAny()) .property("justification", codecOptional(codecForString())) .property("custom_events", codecOptional(codecForList(codecForString()))) + .property("triggering_events", codecOptional(codecForList(codecForString()))) .property( "keep_investigating", codecOptionalDefault(codecForBoolean(), false), @@ -131,6 +136,7 @@ const defaultDecisionRequest: DecisionRequest = { justification: undefined, keep_investigating: false, new_measures: undefined, + triggering_events: undefined, properties: undefined, attributes: undefined, custom_properties: undefined, diff --git a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx @@ -203,6 +203,7 @@ export function CaseDetails({ onExpire_measures: undefined, custom_events: undefined, attributes: undefined, + triggering_events: undefined, justification: undefined, keep_investigating: false, new_measures: undefined, diff --git a/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx b/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx @@ -71,7 +71,7 @@ export function UnlockAccount(): VNode { ); const forgetHandler = - status.status === "fail" || officer.state !== "locked" + officer.state === "not-found" ? undefined : withErrorHandler( async () => officer.forget(), diff --git a/packages/aml-backoffice-ui/src/pages/decision/AmlDecisionRequestWizard.tsx b/packages/aml-backoffice-ui/src/pages/decision/AmlDecisionRequestWizard.tsx @@ -44,9 +44,9 @@ export type WizardSteps = const STEPS_ORDER: WizardSteps[] = [ "attributes", + "rules", "properties", "events", - "rules", "measures", "justification", "summary", diff --git a/packages/aml-backoffice-ui/src/pages/decision/Events.tsx b/packages/aml-backoffice-ui/src/pages/decision/Events.tsx @@ -1,9 +1,12 @@ import { + AbsoluteTime, AML_EVENTS_INFO, AmlDecision, AmlSpaDialect, assertUnreachable, GLS_AmlEventsName, + HttpStatusCode, + LegitimizationRuleSet, MeasureInformation, TalerError, TOPS_AmlEventsName, @@ -12,7 +15,9 @@ import { FormDesign, FormUI, InternationalizationAPI, + Loading, onComponentUnload, + SelectUiChoice, UIFormElementConfig, UIHandlerId, useExchangeApiContext, @@ -26,9 +31,11 @@ import { } from "../../hooks/decision-request.js"; import { usePreferences } from "../../hooks/preferences.js"; import { useAccountActiveDecision } from "../../hooks/decisions.js"; +import { ErrorLoadingWithDebug } from "../../components/ErrorLoadingWithDebug.js"; /** * Trigger additional events + * * @param param0 * @returns */ @@ -43,91 +50,129 @@ export function Events({ account }: { account: string }): VNode { (pref.testingDialect ? undefined : config.config.aml_spa_dialect) ?? AmlSpaDialect.TESTING; - const lastDecision = - !activeDecision || - activeDecision instanceof TalerError || - activeDecision.type === "fail" - ? undefined - : activeDecision.body; - - const calculatedEvents = calculateEventsBasedOnState( - lastDecision, - request, - i18n, - dialect, - ); + 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); + } + } - const design = formDesign(i18n, calculatedEvents.on); + function ShowEventForm({ events: calculatedEvents }: { events: Events }) { + const design = formDesign(i18n, calculatedEvents); - const form = useForm<FormType>(design, { - inhibit: calculatedEvents.on.reduce( - (prev, cur) => { - if (cur.type !== "toggle") return prev; - // const isInhibit = - // request.inhibit_events !== undefined && - // request.inhibit_events.indexOf(cur.id) !== -1; - prev[cur.id] = !false; // FIXME - return prev; - }, - {} as FormType["inhibit"], - ), - trigger: !request.custom_events - ? [] - : request.custom_events.map((name) => ({ name })), - }); + const calculated = + request.triggering_events === undefined + ? calculatedEvents.triggered + : request.triggering_events.filter((name) => + calculatedEvents.triggered.includes(name), + ); - onComponentUnload(() => { - updateRequest({ - ...request, - custom_events: !form.status.result.trigger + const rest = + request.triggering_events === undefined ? [] - : form.status.result.trigger.map((t) => t?.name!), - // inhibit_events: Object.entries(form.status.result.inhibit ?? {}) - // .filter(([key, inhibit]) => !inhibit) - // .map(([key]) => key), + : request.triggering_events.filter((name) => + calculatedEvents.rest.includes(name), + ); + + const form = useForm<FormType>(design, { + calculated, + rest, + }); + + onComponentUnload(() => { + updateRequest({ + ...request, + custom_events: !form.status.result.custom + ? [] + : form.status.result.custom, + triggering_events: [ + ...(form.status.result.calculated ?? []), + ...(form.status.result.rest ?? []), + ], + }); }); - }); - return ( - <div> - <FormUI design={design} model={form.model} /> - </div> + return ( + <div> + <FormUI design={design} handler={form.handler} /> + </div> + ); + } + + const events = calculateEventsBasedOnState( + activeDecision.body, + request, + i18n, + dialect, ); + return <ShowEventForm events={events} />; } +type Events = { + triggered: string[]; + rest: string[]; +}; + type FormType = { - trigger: { name: string }[]; - inhibit: { [name: string]: boolean }; + calculated: string[]; + rest: string[]; + custom: string[]; }; const formDesign = ( i18n: InternationalizationAPI, - inhibitEvents: UIFormElementConfig[], + events: { + triggered: string[]; + rest: string[]; + }, ): FormDesign<MeasureInformation> => ({ type: "double-column", sections: [ { - title: i18n.str`Inhibit default events`, - description: i18n.str`Here you can prevent events to be triggered by the current status.`, - fields: !inhibitEvents.length - ? [ - { - type: "caption", - label: i18n.str`No default events calculated.`, - }, - ] - : inhibitEvents.map((f) => - "id" in f ? { ...f, id: ("inhibit." + f.id) } : f, - ), - }, - { - title: i18n.str`Custom event`, - description: i18n.str`Add more events to be triggered by this request.`, + title: i18n.str`Events`, + description: i18n.str`This events will count when this decision is confirmed.`, fields: [ { - id: "trigger", + type: "selectMultiple", + id: "calculated", + label: i18n.str`Calculated`, + help: i18n.str`Events based on properties and new attributes that should be triggered`, + choices: events.triggered.map( + (name) => + ({ + label: name, + value: name, + }) as SelectUiChoice, + ), + }, + { + type: "selectMultiple", + id: "rest", + label: i18n.str`Others`, + help: i18n.str`Events that can be triggered manually`, + choices: events.rest.map( + (name) => + ({ + label: name, + value: name, + }) as SelectUiChoice, + ), + }, + { + id: "custom", type: "array", - label: i18n.str`Event list`, + label: i18n.str`Custom list`, + help: i18n.str`Events that is not yet supported`, labelFieldId: "name", fields: [ { @@ -233,31 +278,21 @@ function calculateEventsBasedOnState( request: DecisionRequest, i18n: InternationalizationAPI, dialect: AmlSpaDialect, -) { - const init: { - on: UIFormElementConfig[]; - off: UIFormElementConfig[]; - } = { on: [], off: [] }; +): Events { + const init: Events = { triggered: [], rest: [] }; return Object.entries(AML_EVENTS_INFO).reduce((prev, [name, info]) => { - const field = { - id: name, - type: "toggle", - required: true, - label: labelForEvent(name, i18n, dialect), - } satisfies UIFormElementConfig; - if ( - currentState && info.shouldBeTriggered( - currentState.limits, - currentState.properties ?? {}, - request.properties ?? {}, + currentState?.limits?.rules, + request.rules, + currentState?.properties, + request.properties, (request.attributes ?? {}) as Record<string, unknown>, ) ) { - prev.on.push(field); + prev.triggered.push(name); } else { - prev.off.push(field); + prev.rest.push(name); } return prev; }, init); diff --git a/packages/aml-backoffice-ui/src/pages/decision/Properties.tsx b/packages/aml-backoffice-ui/src/pages/decision/Properties.tsx @@ -167,31 +167,26 @@ function propertiesByDialect( id: TalerFormAttributes.AML_ACCOUNT_OPEN, label: i18n.str`Is account active for deposit and payments?`, type: "toggle", - threeState: true, }, { id: TalerFormAttributes.AML_DOMESTIC_PEP, label: i18n.str`Does account belong to a domestic PEP?`, type: "toggle", - threeState: true, }, { id: TalerFormAttributes.AML_FOREIGN_PEP, label: i18n.str`Does account belong to a foreign PEP?`, type: "toggle", - threeState: true, }, { id: TalerFormAttributes.AML_HIGH_RISK_BUSINESS, label: i18n.str`Does account belong to a high risk business?`, type: "toggle", - threeState: true, }, { id: TalerFormAttributes.AML_HIGH_RISK_COUNTRY, label: i18n.str`Does account belong to a person from a high risk country?`, type: "toggle", - threeState: true, }, // { // id: TalerFormAttributes.AML_INVESTIGATION_ART6_COMPLETED, diff --git a/packages/aml-backoffice-ui/src/pages/decision/Summary.tsx b/packages/aml-backoffice-ui/src/pages/decision/Summary.tsx @@ -2,6 +2,7 @@ import { AbsoluteTime, AmlDecisionRequest, assertUnreachable, + Duration, HttpStatusCode, TalerError, } from "@gnu-taler/taler-util"; @@ -67,7 +68,8 @@ export function Summary({ decision.justification === undefined || !decision.justification; const INVALID_ACCOUNT = !account; const INVALID_ATTRIBUTES = - decision.attributes !== undefined && decision.attributes.errors !== undefined; + decision.attributes !== undefined && + decision.attributes.errors !== undefined; const CANT_SUBMIT = INVALID_ACCOUNT || @@ -83,7 +85,7 @@ export function Summary({ custom_events: undefined, custom_properties: undefined, deadline: undefined, - // inhibit_events: undefined, + triggering_events: undefined, attributes: undefined, justification: undefined, keep_investigating: false, @@ -120,6 +122,7 @@ export function Summary({ decision.attributes.expiration, ) : undefined, + events: decision.triggering_events, attributes: decision.attributes?.data, properties: decision.properties!, // TODO: compute properites new_measures: decision.new_measures!.join(" "), @@ -258,7 +261,8 @@ export function Summary({ onClose={() => onMove("attributes")} > <i18n.Translate> - You should check form errors or submit a decision without attributes. + You should check form errors or submit a decision without + attributes. </i18n.Translate> </Attention> ) : (