gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-typescript-core] branch master updated (64e63d508 -> 8d834d43c)


From: gnunet
Subject: [taler-typescript-core] branch master updated (64e63d508 -> 8d834d43c)
Date: Tue, 21 Jan 2025 22:17:20 +0100

This is an automated email from the git hooks/post-receive script.

sebasjm pushed a change to branch master
in repository taler-typescript-core.

    from 64e63d508 harness: test for new_measures
     new e9a3fce8d add gls onboarding placeholder
     new 8d834d43c dashboard

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../aml-backoffice-ui/src/ExchangeAmlFrame.tsx     |   9 +-
 packages/aml-backoffice-ui/src/Routing.tsx         |  11 +-
 packages/aml-backoffice-ui/src/hooks/account.ts    |  32 --
 .../src/hooks/decision-request.ts                  |   9 +-
 .../src/hooks/{account.ts => server-info.ts}       |  90 +++-
 .../aml-backoffice-ui/src/pages/CaseDetails.tsx    | 104 ++--
 packages/aml-backoffice-ui/src/pages/Dashboard.tsx | 570 +++++++++++++++++++++
 packages/aml-backoffice-ui/src/pages/Measures.tsx  |   5 +-
 .../pages/decision/AmlDecisionRequestWizard.tsx    |  12 +-
 .../src/pages/decision/Events.tsx                  | 188 +++----
 .../src/pages/decision/Justification.tsx           | 119 ++++-
 .../src/pages/decision/Measures.tsx                |  16 +-
 .../src/pages/decision/Properties.tsx              |  21 +-
 .../aml-backoffice-ui/src/pages/decision/Rules.tsx |  16 +-
 .../decision/{Justification.tsx => Summary.tsx}    |  15 +-
 .../src/pages/decision/aml-events.ts               | 133 +++++
 packages/bank-ui/src/hooks/regional.ts             |  25 +-
 packages/bank-ui/src/pages/admin/AdminHome.tsx     | 138 ++++-
 packages/kyc-ui/src/forms/index.ts                 |   7 +
 packages/taler-util/src/http-client/exchange.ts    |  13 +-
 packages/taler-util/src/types-taler-exchange.ts    |  26 +-
 packages/web-util/src/forms/gana/GLS_Onboarding.ts |  41 ++
 .../src/forms/gana/taler_form_attributes.ts        |   5 +
 packages/web-util/src/forms/index.ts               |   1 +
 24 files changed, 1320 insertions(+), 286 deletions(-)
 copy packages/aml-backoffice-ui/src/hooks/{account.ts => server-info.ts} (58%)
 create mode 100644 packages/aml-backoffice-ui/src/pages/Dashboard.tsx
 copy packages/aml-backoffice-ui/src/pages/decision/{Justification.tsx => 
Summary.tsx} (55%)
 create mode 100644 packages/aml-backoffice-ui/src/pages/decision/aml-events.ts
 create mode 100644 packages/web-util/src/forms/gana/GLS_Onboarding.ts

diff --git a/packages/aml-backoffice-ui/src/ExchangeAmlFrame.tsx 
b/packages/aml-backoffice-ui/src/ExchangeAmlFrame.tsx
index 8b5911641..2cd82505f 100644
--- a/packages/aml-backoffice-ui/src/ExchangeAmlFrame.tsx
+++ b/packages/aml-backoffice-ui/src/ExchangeAmlFrame.tsx
@@ -218,7 +218,7 @@ export function ExchangeAmlFrame({
 
       <div class="-mt-32 flex grow ">
         {officer?.state !== "ready" ? undefined : <Navigation />}
-        <div class="flex mx-auto my-4">
+        <div class="flex mx-auto my-4 min-w-80">
           <main
             class="block rounded-lg bg-white px-5 py-6 shadow "
             style={{ minWidth: 600 }}
@@ -241,7 +241,11 @@ function Navigation(): VNode {
   const { i18n } = useTranslationContext();
   const [{ showDebugInfo }] = usePreferences();
   const pageList = [
-    { route: privatePages.profile, Icon: PeopleIcon, label: i18n.str`Profile` 
},
+    {
+      route: privatePages.dashboard,
+      Icon: PeopleIcon,
+      label: i18n.str`Dashboard`,
+    },
     {
       route: privatePages.investigation,
       Icon: ToInvestigateIcon,
@@ -253,6 +257,7 @@ function Navigation(): VNode {
       Icon: SearchIcon,
       label: i18n.str`Search`,
     },
+    { route: privatePages.profile, Icon: PeopleIcon, label: i18n.str`Profile` 
},
     showDebugInfo
       ? { route: privatePages.measures, Icon: FormIcon, label: i18n.str`Forms` 
}
       : undefined,
diff --git a/packages/aml-backoffice-ui/src/Routing.tsx 
b/packages/aml-backoffice-ui/src/Routing.tsx
index 1358eda50..58a0730f1 100644
--- a/packages/aml-backoffice-ui/src/Routing.tsx
+++ b/packages/aml-backoffice-ui/src/Routing.tsx
@@ -43,6 +43,7 @@ import {
   WizardSteps,
 } from "./pages/decision/AmlDecisionRequestWizard.js";
 import { useCurrentDecisionRequest } from "./hooks/decision-request.js";
+import { Dashboard } from "./pages/Dashboard.js";
 
 export function Routing(): VNode {
   const session = useOfficer();
@@ -107,6 +108,8 @@ function PublicRounting(): VNode {
 
 export const privatePages = {
   profile: urlPattern(/\/profile/, () => "#/profile"),
+  dashboard: urlPattern(/\/dashboard/, () => "#/dashboard"),
+  statsDownload: urlPattern(/\/download-stats/, () => "#/download-stats"),
   decideWithStep: urlPattern<{ cid: string; step: string }>(
     /\/decide\/(?<cid>[a-zA-Z0-9]+)\/(?<step>[a-z]+)/,
     ({ cid, step }) => `#/decide/${cid}/${step}`,
@@ -147,7 +150,7 @@ function PrivateRouting(): VNode {
   const [request, _, updateRequest] = useCurrentDecisionRequest();
   useEffect(() => {
     if (location.name === undefined) {
-      navigateTo(privatePages.profile.url({}));
+      navigateTo(privatePages.dashboard.url({}));
     }
   }, [location]);
 
@@ -261,6 +264,12 @@ function PrivateRouting(): VNode {
     case "search": {
       return <Search />;
     }
+    case "statsDownload": {
+      return <div>not yet implemented</div>;
+    }
+    case "dashboard": {
+      return <Dashboard routeDownloadStats={privatePages.statsDownload} />;
+    }
     default:
       assertUnreachable(location);
   }
diff --git a/packages/aml-backoffice-ui/src/hooks/account.ts 
b/packages/aml-backoffice-ui/src/hooks/account.ts
index a4fbcb803..f34da0825 100644
--- a/packages/aml-backoffice-ui/src/hooks/account.ts
+++ b/packages/aml-backoffice-ui/src/hooks/account.ts
@@ -68,35 +68,3 @@ export function useAccountInformation(paytoHash?: string) {
   if (error) return error;
   return undefined;
 }
-
-export function useServerMeasures() {
-  const officer = useOfficer();
-  const session = officer.state === "ready" ? officer.account : undefined;
-
-  const {
-    lib: { exchange: api },
-  } = useExchangeApiContext();
-
-  async function fetcher([officer]: [OfficerAccount]) {
-    return await api.getAmlMesasures(officer);
-  }
-
-  const { data, error } = useSWR<
-    TalerExchangeResultByMethod<"getAmlMesasures">,
-    TalerHttpError
-  >(!session ? undefined : [session], fetcher, {
-    refreshInterval: 0,
-    refreshWhenHidden: false,
-    revalidateOnFocus: false,
-    revalidateOnReconnect: false,
-    refreshWhenOffline: false,
-    errorRetryCount: 0,
-    errorRetryInterval: 1,
-    shouldRetryOnError: false,
-    keepPreviousData: true,
-  });
-
-  if (data) return data;
-  if (error) return error;
-  return undefined;
-}
diff --git a/packages/aml-backoffice-ui/src/hooks/decision-request.ts 
b/packages/aml-backoffice-ui/src/hooks/decision-request.ts
index 9b67b5654..34c2aeda3 100644
--- a/packages/aml-backoffice-ui/src/hooks/decision-request.ts
+++ b/packages/aml-backoffice-ui/src/hooks/decision-request.ts
@@ -35,14 +35,15 @@ import { buildStorageKey, useLocalStorage } from 
"@gnu-taler/web-util/browser";
 
 export interface DecisionRequest {
   rules: KycRule[] | undefined;
+  new_measures: string | undefined;
   deadline: AbsoluteTime | undefined;
-  properties: object | undefined;
-  custom_properties: object | undefined;
+  onExpire_measures: string | undefined;
+  properties: Record<string, any> | undefined;
+  custom_properties: Record<string, any> | undefined;
   custom_events: string[] | undefined;
   inhibit_events: string[] | undefined;
   keep_investigating: boolean;
   justification: string | undefined;
-  new_measures: string | undefined;
 }
 
 export const codecForDecisionRequest = (): Codec<DecisionRequest> =>
@@ -59,10 +60,12 @@ export const codecForDecisionRequest = (): 
Codec<DecisionRequest> =>
       codecOptionalDefault(codecForBoolean(), false),
     )
     .property("new_measures", codecOptional(codecForString()))
+    .property("onExpire_measures", codecOptional(codecForString()))
     .build("DecisionRequest");
 
 const defaultDecisionRequest: DecisionRequest = {
   deadline: undefined,
+  onExpire_measures: undefined,
   custom_events: undefined,
   inhibit_events: undefined,
   justification: undefined,
diff --git a/packages/aml-backoffice-ui/src/hooks/account.ts 
b/packages/aml-backoffice-ui/src/hooks/server-info.ts
similarity index 58%
copy from packages/aml-backoffice-ui/src/hooks/account.ts
copy to packages/aml-backoffice-ui/src/hooks/server-info.ts
index a4fbcb803..47743440a 100644
--- a/packages/aml-backoffice-ui/src/hooks/account.ts
+++ b/packages/aml-backoffice-ui/src/hooks/server-info.ts
@@ -14,8 +14,15 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 import {
+  AbsoluteTime,
+  AmlSpaDialect,
+  assertUnreachable,
+  EventCounter,
   OfficerAccount,
+  OperationOk,
+  opFixedSuccess,
   PaytoString,
+  TalerCorebankApi,
   TalerExchangeResultByMethod,
   TalerHttpError,
 } from "@gnu-taler/taler-util";
@@ -23,36 +30,31 @@ import {
 import { useExchangeApiContext } from "@gnu-taler/web-util/browser";
 import _useSWR, { mutate, SWRHook } from "swr";
 import { useOfficer } from "./officer.js";
+import { AmlProgram } from "../../../taler-util/lib/types-taler-kyc-aml.js";
+import {
+  AML_EVENTS_INFO,
+  AmlEventsName,
+} from "../pages/decision/aml-events.js";
+import { useMemo } from "preact/hooks";
+import { Timeframe } from "../pages/Dashboard.js";
 const useSWR = _useSWR as unknown as SWRHook;
 
-export function revalidateAccountInformation() {
-  return mutate(
-    (key) =>
-      Array.isArray(key) &&
-      key[key.length - 1] === "getAmlAttributesForAccount",
-    undefined,
-    { revalidate: true },
-  );
-}
-export function useAccountInformation(paytoHash?: string) {
+export function useServerMeasures() {
   const officer = useOfficer();
-  const session =
-    officer.state === "ready" && paytoHash !== undefined
-      ? officer.account
-      : undefined;
+  const session = officer.state === "ready" ? officer.account : undefined;
 
   const {
     lib: { exchange: api },
   } = useExchangeApiContext();
 
-  async function fetcher([officer, account]: [OfficerAccount, PaytoString]) {
-    return await api.getAmlAttributesForAccount(officer, account);
+  async function fetcher([officer]: [OfficerAccount]) {
+    return await api.getAmlMesasures(officer);
   }
 
   const { data, error } = useSWR<
-    TalerExchangeResultByMethod<"getAmlAttributesForAccount">,
+    TalerExchangeResultByMethod<"getAmlMesasures">,
     TalerHttpError
-  >(!session ? undefined : [session, paytoHash], fetcher, {
+  >(!session ? undefined : [session], fetcher, {
     refreshInterval: 0,
     refreshWhenHidden: false,
     revalidateOnFocus: false,
@@ -69,7 +71,15 @@ export function useAccountInformation(paytoHash?: string) {
   return undefined;
 }
 
-export function useServerMeasures() {
+export type ServerStats = {
+  [name: string]: EventCounter;
+};
+
+export function useServerStatistics(
+  dialect: AmlSpaDialect,
+  current: Timeframe,
+  previous?: Timeframe,
+) {
   const officer = useOfficer();
   const session = officer.state === "ready" ? officer.account : undefined;
 
@@ -77,14 +87,48 @@ export function useServerMeasures() {
     lib: { exchange: api },
   } = useExchangeApiContext();
 
-  async function fetcher([officer]: [OfficerAccount]) {
-    return await api.getAmlMesasures(officer);
+  const keys = useMemo(() => {
+    return Object.entries(AML_EVENTS_INFO)
+      .filter(([name, info]) => {
+        return info.dialect.includes(dialect);
+      })
+      .map(([name]) => name as AmlEventsName);
+  }, [dialect]);
+
+  async function fetcher([officer, keys, current, previous]: [
+    OfficerAccount,
+    AmlEventsName[],
+    Timeframe,
+    Timeframe | undefined,
+  ]) {
+    const queries = keys.map((key) => {
+      return Promise.all([
+        api.getAmlKycStatistics(officer, key, {
+          since: current.start,
+          until: current.end,
+        }),
+        !previous
+          ? undefined
+          : api.getAmlKycStatistics(officer, key, {
+              since: previous.start,
+              until: previous.end,
+            }),
+      ]).then(([c, p]) => {
+        return { key, current: c.body, previous: p?.body };
+      });
+    });
+
+    const p = await Promise.all(queries);
+
+    return opFixedSuccess(p);
   }
 
   const { data, error } = useSWR<
-    TalerExchangeResultByMethod<"getAmlMesasures">,
+    OperationOk<
+      { key: AmlEventsName; current: EventCounter; previous?: EventCounter }[]
+    >,
     TalerHttpError
-  >(!session ? undefined : [session], fetcher, {
+  >(!session ? undefined : [session, keys, current, previous], fetcher, {
     refreshInterval: 0,
     refreshWhenHidden: false,
     revalidateOnFocus: false,
diff --git a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx 
b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
index dd5aea232..68c037cc7 100644
--- a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
+++ b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
@@ -58,13 +58,14 @@ import { useState } from "preact/hooks";
 import { ErrorLoadingWithDebug } from "../components/ErrorLoadingWithDebug.js";
 import { useUiFormsContext } from "../context/ui-forms.js";
 import { preloadedForms } from "../forms/index.js";
-import { useAccountInformation, useServerMeasures } from "../hooks/account.js";
+import { useAccountInformation } from "../hooks/account.js";
 import { DecisionRequest } from "../hooks/decision-request.js";
 import { useAccountDecisions } from "../hooks/decisions.js";
 import { useOfficer } from "../hooks/officer.js";
 import { CurrentMeasureTable, MeasureInfo } from "./MeasuresTable.js";
 import { Officer } from "./Officer.js";
 import { ShowConsolidated } from "./ShowConsolidated.js";
+import { useServerMeasures } from "../hooks/server-info.js";
 
 export type AmlEvent =
   | AmlFormEvent
@@ -142,13 +143,13 @@ export function CaseDetails({
   // paytoString?: PaytoString;
 }) {
   const [selected, setSelected] = useState<AbsoluteTime>(AbsoluteTime.now());
-  const [request, setDesicionRequest] = useState<NewDecision | undefined>(
-    undefined,
-  );
+  // const [request, setDesicionRequest] = useState<NewDecision | undefined>(
+  //   undefined,
+  // );
   // const [decisionWizardStep, setDecisionWizardStep] =
   //   useState<WizardSteps>("events");
-  const [selectMeasure, setSelectMeasure] = useState<boolean>();
-  const { config } = useExchangeApiContext();
+  // const [selectMeasure, setSelectMeasure] = useState<boolean>();
+  // const { config } = useExchangeApiContext();
 
   const { i18n } = useTranslationContext();
   const details = useAccountInformation(account);
@@ -196,47 +197,47 @@ export function CaseDetails({
 
   const events = getEventsFromAmlHistory(accountDetails, i18n, allForms);
 
-  if (selectMeasure) {
-    return (
-      <ShowMeasuresToSelect
-        onSelect={(d) => {
-          setSelectMeasure(false);
-          setDesicionRequest({
-            request: {
-              // payto_uri: paytoString,
-              decision_time: AbsoluteTime.toProtocolTimestamp(
-                AbsoluteTime.now(),
-              ),
-              h_payto: account,
-              keep_investigating: false,
-              properties: {},
-              // the custom measure with context
-              new_measures: d.name,
-              new_rules: {
-                // this value is going to be overridden
-                custom_measures: {},
-                expiration_time: AbsoluteTime.toProtocolTimestamp(
-                  AbsoluteTime.never(),
-                ),
-                rules: FREEZE_RULES(config.config.currency),
-              },
-            },
-            askInformation: false,
-          });
-        }}
-      />
-    );
-  }
-  if (request) {
-    return (
-      <SubmitNewDecision
-        decision={request}
-        onComplete={() => {
-          setDesicionRequest(undefined);
-        }}
-      />
-    );
-  }
+  // if (selectMeasure) {
+  //   return (
+  //     <ShowMeasuresToSelect
+  //       onSelect={(d) => {
+  //         setSelectMeasure(false);
+  //         setDesicionRequest({
+  //           request: {
+  //             // payto_uri: paytoString,
+  //             decision_time: AbsoluteTime.toProtocolTimestamp(
+  //               AbsoluteTime.now(),
+  //             ),
+  //             h_payto: account,
+  //             keep_investigating: false,
+  //             properties: {},
+  //             // the custom measure with context
+  //             new_measures: d.name,
+  //             new_rules: {
+  //               // this value is going to be overridden
+  //               custom_measures: {},
+  //               expiration_time: AbsoluteTime.toProtocolTimestamp(
+  //                 AbsoluteTime.never(),
+  //               ),
+  //               rules: FREEZE_RULES(config.config.currency),
+  //             },
+  //           },
+  //           askInformation: false,
+  //         });
+  //       }}
+  //     />
+  //   );
+  // }
+  // if (request) {
+  //   return (
+  //     <SubmitNewDecision
+  //       decision={request}
+  //       onComplete={() => {
+  //         setDesicionRequest(undefined);
+  //       }}
+  //     />
+  //   );
+  // }
   // if (decisionWizardStep) {
   //   return (
   // <AmlDecisionRequestWizard
@@ -254,6 +255,7 @@ export function CaseDetails({
             onNewDecision({
               deadline: undefined,
               custom_properties: undefined,
+              onExpire_measures: undefined,
               custom_events: undefined,
               inhibit_events: undefined,
               justification: undefined,
@@ -267,7 +269,7 @@ export function CaseDetails({
         >
           <i18n.Translate>New decision</i18n.Translate>
         </button>
-
+        {/* 
         <button
           onClick={async () => {
             setDesicionRequest({
@@ -377,16 +379,16 @@ export function CaseDetails({
           class="m-4  rounded-md w-fit border-0 px-3 py-2 text-center text-sm 
bg-indigo-700 text-white shadow-sm hover:bg-indigo-700"
         >
           <i18n.Translate>Ask for more information</i18n.Translate>
-        </button>
+        </button> */}
 
-        <button
+        {/* <button
           onClick={async () => {
             setSelectMeasure(true);
           }}
           class="m-4  rounded-md w-fit border-0 px-3 py-2 text-center text-sm 
bg-indigo-700 text-white shadow-sm hover:bg-indigo-700"
         >
           <i18n.Translate>Set predefined measure</i18n.Translate>
-        </button>
+        </button> */}
       </div>
     );
   }
diff --git a/packages/aml-backoffice-ui/src/pages/Dashboard.tsx 
b/packages/aml-backoffice-ui/src/pages/Dashboard.tsx
new file mode 100644
index 000000000..e87236cfc
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/pages/Dashboard.tsx
@@ -0,0 +1,570 @@
+/*
+ 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 {
+  AbsoluteTime,
+  AmlSpaDialect,
+  assertUnreachable,
+  TalerCorebankApi,
+  TalerError,
+  TranslatedString,
+} from "@gnu-taler/taler-util";
+import {
+  InternationalizationAPI,
+  RouteDefinition,
+  useExchangeApiContext,
+  useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { format, sub } from "date-fns";
+import { Fragment, h, VNode } from "preact";
+import { useMemo, useState } from "preact/hooks";
+import { ErrorLoadingWithDebug } from "../components/ErrorLoadingWithDebug.js";
+import { useOfficer } from "../hooks/officer.js";
+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";
+
+export function Dashboard({
+  routeDownloadStats,
+}: {
+  routeDownloadStats: RouteDefinition;
+}) {
+  const officer = useOfficer();
+  const { i18n } = useTranslationContext();
+
+  if (officer.state !== "ready") {
+    return <HandleAccountNotReady officer={officer} />;
+  }
+
+  return (
+    <div>
+      <h1 class="my-2 text-3xl font-bold tracking-tight text-gray-900 ">
+        <i18n.Translate>Dashboard</i18n.Translate>
+      </h1>
+      <EventMetrics routeDownloadStats={routeDownloadStats} />
+    </div>
+  );
+}
+
+const now = new Date();
+
+function EventMetrics({
+  routeDownloadStats,
+}: {
+  routeDownloadStats: RouteDefinition;
+}): VNode {
+  const { i18n, dateLocale } = useTranslationContext();
+  const [metricType, setMetricType] =
+    useState<TalerCorebankApi.MonitorTimeframeParam>(
+      TalerCorebankApi.MonitorTimeframeParam.hour,
+    );
+  const params = useMemo(
+    () => getTimeframesForDate(now, metricType),
+    [metricType],
+  );
+
+  const [pref] = usePreferences();
+  const { config } = useExchangeApiContext();
+
+  const dialect =
+    (pref.testingDialect ? undefined : config.config.aml_spa_dialect) ??
+    AmlSpaDialect.TESTING;
+
+  const resp = useServerStatistics(dialect, params.current, params.previous);
+
+  if (!resp) return <Fragment />;
+  if (resp instanceof TalerError) {
+    return <ErrorLoadingWithDebug error={resp} />;
+  }
+
+  return (
+    <div class="px-4 mt-4">
+      <div class="sm:flex sm:items-center mb-4">
+        <div class="sm:flex-auto">
+          <h1 class="text-base font-semibold leading-6 text-gray-900">
+            <i18n.Translate>Events</i18n.Translate>
+          </h1>
+        </div>
+      </div>
+
+      <SelectTimeframe timeframe={metricType} setTimeframe={setMetricType} />
+
+      <div class="w-full flex justify-between">
+        <h1 class="text-base text-gray-900 mt-5">
+          {i18n.str`Trading volume from ${getDateStringForTimeframe(
+            params.current.start,
+            metricType,
+            dateLocale,
+          )} to ${getDateStringForTimeframe(
+            params.current.end,
+            metricType,
+            dateLocale,
+          )}`}
+        </h1>
+      </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) => {
+          const label = labelForEvent(ev.key, i18n);
+          const desc = descriptionForEvent(ev.key, i18n);
+          return (
+            <div class="px-4 py-5 sm:p-6">
+              <dt class="text-base font-normal text-gray-900">
+                {label}
+                {!desc ? undefined : (
+                  <div class="text-xs text-gray-500">{desc}</div>
+                )}
+              </dt>
+              <MetricValueNumber
+                current={ev.current.counter}
+                previous={ev.previous?.counter}
+              />
+            </div>
+          );
+        })}
+      </dl>
+      <div class="flex justify-end mt-4">
+        <a
+          href={routeDownloadStats.url({})}
+          name="download stats"
+          class="disabled:opacity-50 disabled:cursor-default cursor-pointer 
rounded-md bg-indigo-600 px-3 py-2 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>Download stats as CSV</i18n.Translate>
+        </a>
+      </div>
+    </div>
+  );
+}
+
+function labelForEvent(name: AmlEventsName, i18n: InternationalizationAPI) {
+  switch (name) {
+    case AmlEventsName.ACCOUNT_FROZEN:
+      return i18n.str`Frozen accounts`;
+    case AmlEventsName.ACCOUNT_PEP:
+      return i18n.str`PEP persons`;
+    case AmlEventsName.TEST_EVENT_KEY_1:
+      return i18n.str`TEST_EVENT_KEY_1`;
+    case AmlEventsName.TEST_EVENT_KEY_2:
+      return i18n.str`TEST_EVENT_KEY_2`;
+    case AmlEventsName.TEST_EVENT_KEY_3:
+      return i18n.str`TEST_EVENT_KEY_3`;
+    case AmlEventsName.TEST_EVENT_KEY_4:
+      return i18n.str`TEST_EVENT_KEY_4`;
+    case AmlEventsName.TEST_EVENT_KEY_5:
+      return i18n.str`TEST_EVENT_KEY_5`;
+    case AmlEventsName.TEST_EVENT_KEY_6:
+      return i18n.str`TEST_EVENT_KEY_6`;
+    case AmlEventsName.TEST_EVENT_KEY_7:
+      return i18n.str`TEST_EVENT_KEY_7`;
+    case AmlEventsName.TEST_EVENT_KEY_8:
+      return i18n.str`TEST_EVENT_KEY_8`;
+    case AmlEventsName.TEST_EVENT_KEY_9:
+      return i18n.str`TEST_EVENT_KEY_9`;
+    case AmlEventsName.TEST_EVENT_KEY_10:
+      return i18n.str`TEST_EVENT_KEY_10`;
+    case AmlEventsName.TEST_EVENT_KEY_11:
+      return i18n.str`TEST_EVENT_KEY_11`;
+    case AmlEventsName.TEST_EVENT_KEY_12:
+      return i18n.str`TEST_EVENT_KEY_12`;
+    case AmlEventsName.TEST_EVENT_KEY_13:
+      return i18n.str`TEST_EVENT_KEY_13`;
+    case AmlEventsName.TEST_EVENT_KEY_14:
+      return i18n.str`TEST_EVENT_KEY_14`;
+    default: {
+      assertUnreachable(name);
+    }
+  }
+}
+
+function descriptionForEvent(
+  name: AmlEventsName,
+  i18n: InternationalizationAPI,
+): TranslatedString | undefined {
+  switch (name) {
+    case AmlEventsName.ACCOUNT_FROZEN:
+      return i18n.str`Accounts that can move funds`;
+    case AmlEventsName.ACCOUNT_PEP:
+      return i18n.str`Public exposed persons`;
+    case AmlEventsName.TEST_EVENT_KEY_1:
+      return i18n.str`TEST_EVENT_KEY_1`;
+    case AmlEventsName.TEST_EVENT_KEY_2:
+      return i18n.str`TEST_EVENT_KEY_2`;
+    case AmlEventsName.TEST_EVENT_KEY_3:
+      return i18n.str`TEST_EVENT_KEY_3`;
+    case AmlEventsName.TEST_EVENT_KEY_4:
+      return i18n.str`TEST_EVENT_KEY_4`;
+    case AmlEventsName.TEST_EVENT_KEY_5:
+      return i18n.str`TEST_EVENT_KEY_5`;
+    case AmlEventsName.TEST_EVENT_KEY_6:
+      return i18n.str`TEST_EVENT_KEY_6`;
+    case AmlEventsName.TEST_EVENT_KEY_7:
+      return i18n.str`TEST_EVENT_KEY_7`;
+    case AmlEventsName.TEST_EVENT_KEY_8:
+      return i18n.str`TEST_EVENT_KEY_8`;
+    case AmlEventsName.TEST_EVENT_KEY_9:
+      return i18n.str`TEST_EVENT_KEY_9`;
+    case AmlEventsName.TEST_EVENT_KEY_10:
+      return i18n.str`TEST_EVENT_KEY_10`;
+    case AmlEventsName.TEST_EVENT_KEY_11:
+      return i18n.str`TEST_EVENT_KEY_11`;
+    case AmlEventsName.TEST_EVENT_KEY_12:
+      return i18n.str`TEST_EVENT_KEY_12`;
+    case AmlEventsName.TEST_EVENT_KEY_13:
+      return i18n.str`TEST_EVENT_KEY_13`;
+    case AmlEventsName.TEST_EVENT_KEY_14:
+      return i18n.str`TEST_EVENT_KEY_14`;
+    default: {
+      assertUnreachable(name);
+    }
+  }
+}
+
+function MetricValueNumber({
+  current,
+  previous,
+}: {
+  current: number | undefined;
+  previous: number | undefined;
+}): VNode {
+  const { i18n } = useTranslationContext();
+
+  const cmp = current && previous ? (current < previous ? -1 : 1) : 0;
+
+  const rate =
+    !current || Number.isNaN(current) || !previous || Number.isNaN(previous)
+      ? 0
+      : cmp === -1
+        ? 1 - Math.round(current) / Math.round(previous)
+        : cmp === 1
+          ? Math.round(current) / Math.round(previous) - 1
+          : 0;
+
+  const negative = cmp === 0 ? undefined : cmp === -1;
+  const rateStr = `${(Math.abs(rate) * 100).toFixed(2)}%`;
+  return (
+    <Fragment>
+      <dd class="mt-1 block ">
+        <div class="flex justify-start text-2xl items-baseline font-semibold 
text-indigo-600">
+          {!current ? "-" : current}
+        </div>
+        <div class="flex flex-col">
+          <div class="flex justify-end items-baseline text-2xl font-semibold 
text-indigo-600">
+            <small class="ml-2 text-sm font-medium text-gray-500">
+              <i18n.Translate>previous</i18n.Translate>{" "}
+              {!previous ? "-" : previous}
+            </small>
+          </div>
+          {!!rate && (
+            <span
+              data-negative={negative}
+              class="flex items-center gap-x-1.5 w-fit rounded-md bg-green-100 
text-green-800 data-[negative=true]:bg-red-100 px-2 py-1 text-xs font-medium 
data-[negative=true]:text-red-700 whitespace-pre"
+            >
+              {negative ? (
+                <svg
+                  xmlns="http://www.w3.org/2000/svg";
+                  fill="none"
+                  viewBox="0 0 24 24"
+                  stroke-width="1.5"
+                  stroke="currentColor"
+                  class="w-6 h-6"
+                >
+                  <path
+                    stroke-linecap="round"
+                    stroke-linejoin="round"
+                    d="M12 4.5v15m0 0l6.75-6.75M12 19.5l-6.75-6.75"
+                  />
+                </svg>
+              ) : (
+                <svg
+                  xmlns="http://www.w3.org/2000/svg";
+                  fill="none"
+                  viewBox="0 0 24 24"
+                  stroke-width="1.5"
+                  stroke="currentColor"
+                  class="w-6 h-6"
+                >
+                  <path
+                    stroke-linecap="round"
+                    stroke-linejoin="round"
+                    d="M12 19.5v-15m0 0l-6.75 6.75M12 4.5l6.75 6.75"
+                  />
+                </svg>
+              )}
+
+              {negative ? (
+                <span class="sr-only">
+                  <i18n.Translate>Decreased by</i18n.Translate>
+                </span>
+              ) : (
+                <span class="sr-only">
+                  <i18n.Translate>Increased by</i18n.Translate>
+                </span>
+              )}
+              {rateStr}
+            </span>
+          )}
+        </div>
+      </dd>
+    </Fragment>
+  );
+}
+
+export type Timeframe = { start: AbsoluteTime; end: AbsoluteTime };
+
+export function getTimeframesForDate(
+  time: Date,
+  timeframe: TalerCorebankApi.MonitorTimeframeParam,
+): {
+  current: Timeframe;
+  previous: Timeframe;
+} {
+  switch (timeframe) {
+    case TalerCorebankApi.MonitorTimeframeParam.hour: {
+      const [high, middle, low] = [0, 1, 2].map((timeIndex) =>
+        AbsoluteTime.fromMilliseconds(
+          sub(time, { hours: timeIndex }).getTime(),
+        ),
+      );
+      return {
+        current: { start: middle, end: high },
+        previous: { start: low, end: middle },
+      };
+    }
+    case TalerCorebankApi.MonitorTimeframeParam.day: {
+      const [high, middle, low] = [0, 1, 2].map((timeIndex) =>
+        AbsoluteTime.fromMilliseconds(sub(time, { days: timeIndex 
}).getTime()),
+      );
+      return {
+        current: { start: middle, end: high },
+        previous: { start: low, end: middle },
+      };
+    }
+    case TalerCorebankApi.MonitorTimeframeParam.month: {
+      const [high, middle, low] = [0, 1, 2].map((timeIndex) =>
+        AbsoluteTime.fromMilliseconds(
+          sub(time, { months: timeIndex }).getTime(),
+        ),
+      );
+      return {
+        current: { start: middle, end: high },
+        previous: { start: low, end: middle },
+      };
+    }
+
+    case TalerCorebankApi.MonitorTimeframeParam.year: {
+      const [high, middle, low] = [0, 1, 2].map((timeIndex) =>
+        AbsoluteTime.fromMilliseconds(
+          sub(time, { years: timeIndex }).getTime(),
+        ),
+      );
+      return {
+        current: { start: middle, end: high },
+        previous: { start: low, end: middle },
+      };
+    }
+    case TalerCorebankApi.MonitorTimeframeParam.decade: {
+      const [high, middle, low] = [0, 1, 2].map((timeIndex) =>
+        AbsoluteTime.fromMilliseconds(
+          sub(time, { years: timeIndex * 10 }).getTime(),
+        ),
+      );
+      return {
+        current: { start: middle, end: high },
+        previous: { start: low, end: middle },
+      };
+    }
+    default:
+      assertUnreachable(timeframe);
+  }
+}
+
+function getDateStringForTimeframe(
+  date: AbsoluteTime,
+  timeframe: TalerCorebankApi.MonitorTimeframeParam,
+  locale: Locale,
+): string {
+  if (date.t_ms === "never") return "--";
+  switch (timeframe) {
+    case TalerCorebankApi.MonitorTimeframeParam.hour:
+      return `${format(date.t_ms, "HH:00", { locale })}hs`;
+    case TalerCorebankApi.MonitorTimeframeParam.day:
+      return format(date.t_ms, "EEEE", { locale });
+    case TalerCorebankApi.MonitorTimeframeParam.month:
+      return format(date.t_ms, "MMMM", { locale });
+    case TalerCorebankApi.MonitorTimeframeParam.year:
+      return format(date.t_ms, "yyyy", { locale });
+    case TalerCorebankApi.MonitorTimeframeParam.decade:
+      return format(date.t_ms, "yyyy", { locale });
+  }
+  assertUnreachable(timeframe);
+}
+
+function SelectTimeframe({
+  timeframe,
+  setTimeframe,
+}: {
+  timeframe: TalerCorebankApi.MonitorTimeframeParam;
+  setTimeframe: (t: TalerCorebankApi.MonitorTimeframeParam) => void;
+}): VNode {
+  const { i18n } = useTranslationContext();
+  return (
+    <Fragment>
+      <div class="sm:hidden">
+        <label for="tabs" class="sr-only">
+          <i18n.Translate>Select a section</i18n.Translate>
+        </label>
+        <select
+          id="tabs"
+          name="tabs"
+          class="block w-full rounded-md border-gray-300 
focus:border-indigo-500 focus:ring-indigo-500"
+          onChange={(e) => {
+            setTimeframe(
+              parseInt(
+                e.currentTarget.value,
+                10,
+              ) as TalerCorebankApi.MonitorTimeframeParam,
+            );
+          }}
+        >
+          <option
+            value={TalerCorebankApi.MonitorTimeframeParam.hour}
+            selected={timeframe == TalerCorebankApi.MonitorTimeframeParam.hour}
+          >
+            <i18n.Translate>Last hour</i18n.Translate>
+          </option>
+          <option
+            value={TalerCorebankApi.MonitorTimeframeParam.day}
+            selected={timeframe == TalerCorebankApi.MonitorTimeframeParam.day}
+          >
+            <i18n.Translate>Previous day</i18n.Translate>
+          </option>
+          <option
+            value={TalerCorebankApi.MonitorTimeframeParam.month}
+            selected={timeframe == 
TalerCorebankApi.MonitorTimeframeParam.month}
+          >
+            <i18n.Translate>Last month</i18n.Translate>
+          </option>
+          <option
+            value={TalerCorebankApi.MonitorTimeframeParam.year}
+            selected={timeframe == TalerCorebankApi.MonitorTimeframeParam.year}
+          >
+            <i18n.Translate>Last year</i18n.Translate>
+          </option>
+        </select>
+      </div>
+      <div class="hidden sm:block">
+        {/* FIXME: This should be LINKS */}
+        <nav
+          class="isolate flex divide-x divide-gray-200 rounded-lg shadow"
+          aria-label="Tabs"
+        >
+          <button
+            type="button"
+            name="set last hour"
+            onClick={(e) => {
+              e.preventDefault();
+              setTimeframe(TalerCorebankApi.MonitorTimeframeParam.hour);
+            }}
+            data-selected={
+              timeframe == TalerCorebankApi.MonitorTimeframeParam.hour
+            }
+            class="rounded-l-lg text-gray-500 hover:text-gray-700 
data-[selected=true]:text-gray-900 group relative min-w-0 flex-1 
overflow-hidden bg-white py-4 px-4 text-center text-sm font-medium 
hover:bg-gray-50 focus:z-10"
+          >
+            <span>
+              <i18n.Translate>Last hour</i18n.Translate>
+            </span>
+            <span
+              aria-hidden="true"
+              data-selected={
+                timeframe == TalerCorebankApi.MonitorTimeframeParam.hour
+              }
+              class="bg-transparent data-[selected=true]:bg-indigo-500 
absolute inset-x-0 bottom-0 h-0.5"
+            ></span>
+          </button>
+          <button
+            type="button"
+            name="set previous day"
+            onClick={(e) => {
+              e.preventDefault();
+              setTimeframe(TalerCorebankApi.MonitorTimeframeParam.day);
+            }}
+            data-selected={
+              timeframe == TalerCorebankApi.MonitorTimeframeParam.day
+            }
+            class="             text-gray-500 hover:text-gray-700 
data-[selected=true]:text-gray-900 group relative min-w-0 flex-1 
overflow-hidden bg-white py-4 px-4 text-center text-sm font-medium 
hover:bg-gray-50 focus:z-10"
+          >
+            <span>
+              <i18n.Translate>Previous day</i18n.Translate>
+            </span>
+            <span
+              aria-hidden="true"
+              data-selected={
+                timeframe == TalerCorebankApi.MonitorTimeframeParam.day
+              }
+              class="bg-transparent data-[selected=true]:bg-indigo-500 
absolute inset-x-0 bottom-0 h-0.5"
+            ></span>
+          </button>
+          <button
+            type="button"
+            name="set last month"
+            onClick={(e) => {
+              e.preventDefault();
+              setTimeframe(TalerCorebankApi.MonitorTimeframeParam.month);
+            }}
+            data-selected={
+              timeframe == TalerCorebankApi.MonitorTimeframeParam.month
+            }
+            class="rounded-r-lg text-gray-500 hover:text-gray-700 
data-[selected=true]:text-gray-900 group relative min-w-0 flex-1 
overflow-hidden bg-white py-4 px-4 text-center text-sm font-medium 
hover:bg-gray-50 focus:z-10"
+          >
+            <span>
+              <i18n.Translate>Last month</i18n.Translate>
+            </span>
+            <span
+              aria-hidden="true"
+              data-selected={
+                timeframe == TalerCorebankApi.MonitorTimeframeParam.month
+              }
+              class="bg-transparent data-[selected=true]:bg-indigo-500 
absolute inset-x-0 bottom-0 h-0.5"
+            ></span>
+          </button>
+          <button
+            type="button"
+            name="set last year"
+            onClick={(e) => {
+              e.preventDefault();
+              setTimeframe(TalerCorebankApi.MonitorTimeframeParam.year);
+            }}
+            data-selected={
+              timeframe == TalerCorebankApi.MonitorTimeframeParam.year
+            }
+            class="rounded-r-lg text-gray-500 hover:text-gray-700 
data-[selected=true]:text-gray-900 group relative min-w-0 flex-1 
overflow-hidden bg-white py-4 px-4 text-center text-sm font-medium 
hover:bg-gray-50 focus:z-10"
+          >
+            <span>
+              <i18n.Translate>Last Year</i18n.Translate>
+            </span>
+            <span
+              aria-hidden="true"
+              data-selected={
+                timeframe == TalerCorebankApi.MonitorTimeframeParam.year
+              }
+              class="bg-transparent data-[selected=true]:bg-indigo-500 
absolute inset-x-0 bottom-0 h-0.5"
+            ></span>
+          </button>
+        </nav>
+      </div>
+    </Fragment>
+  );
+}
diff --git a/packages/aml-backoffice-ui/src/pages/Measures.tsx 
b/packages/aml-backoffice-ui/src/pages/Measures.tsx
index 28236fe0c..571b77c8f 100644
--- a/packages/aml-backoffice-ui/src/pages/Measures.tsx
+++ b/packages/aml-backoffice-ui/src/pages/Measures.tsx
@@ -28,16 +28,13 @@ import {
 } from "@gnu-taler/web-util/browser";
 import { Fragment, h, VNode } from "preact";
 import { ErrorLoadingWithDebug } from "../components/ErrorLoadingWithDebug.js";
-import { useServerMeasures } from "../hooks/account.js";
 import { CurrentMeasureTable, MeasureInfo } from "./MeasuresTable.js";
 import { Officer } from "./Officer.js";
+import { useServerMeasures } from "../hooks/server-info.js";
 
 export function Measures({}: {}) {
   const { i18n } = useTranslationContext();
 
-  // const { forms } = useUiFormsContext();
-
-  // const allForms = [...forms, ...preloadedForms(i18n)];
   const measures = useServerMeasures();
 
   if (!measures) {
diff --git 
a/packages/aml-backoffice-ui/src/pages/decision/AmlDecisionRequestWizard.tsx 
b/packages/aml-backoffice-ui/src/pages/decision/AmlDecisionRequestWizard.tsx
index 3d6f8a1b4..b677e2a0d 100644
--- a/packages/aml-backoffice-ui/src/pages/decision/AmlDecisionRequestWizard.tsx
+++ b/packages/aml-backoffice-ui/src/pages/decision/AmlDecisionRequestWizard.tsx
@@ -34,13 +34,15 @@ import { Properties } from "./Properties.js";
 import { Rules } from "./Rules.js";
 import { Measures } from "./Measures.js";
 import { Justification } from "./Justification.js";
+import { Summary } from "./Summary.js";
 
 export type WizardSteps =
   | "rules" // define the limits
   | "measures" // define a new form/challenge
   | "properties" // define account information
   | "events" // define events to trigger
-  | "justification"; // finalize, investigate?;
+  | "justification" // finalize, investigate?;
+  | "summary";
 
 const STEPS_ORDER: WizardSteps[] = [
   "rules",
@@ -48,6 +50,7 @@ const STEPS_ORDER: WizardSteps[] = [
   "properties",
   "events",
   "justification",
+  "summary",
 ];
 
 const STEPS_ORDER_MAP = STEPS_ORDER.reduce(
@@ -108,6 +111,8 @@ export function AmlDecisionRequestWizard({
         return <Measures />;
       case "justification":
         return <Justification />;
+      case "summary":
+        return <Summary />;
     }
     assertUnreachable(stepOrDefault);
   })();
@@ -178,6 +183,11 @@ function WizardSteps({
       description: i18n.str`Add information about the account.`,
       isCompleted: isPropertiesCompleted,
     },
+    summary: {
+      label: i18n.str`Summary`,
+      description: i18n.str`Review and send.`,
+      isCompleted: () => false,
+    },
   };
   return (
     <div class="lg:border-b lg:border-t lg:border-gray-200">
diff --git a/packages/aml-backoffice-ui/src/pages/decision/Events.tsx 
b/packages/aml-backoffice-ui/src/pages/decision/Events.tsx
index e65586a77..a984b430a 100644
--- a/packages/aml-backoffice-ui/src/pages/decision/Events.tsx
+++ b/packages/aml-backoffice-ui/src/pages/decision/Events.tsx
@@ -1,19 +1,23 @@
 import {
-  useTranslationContext,
-  useExchangeApiContext,
-  useForm,
+  AmlSpaDialect,
+  assertUnreachable,
+  MeasureInformation,
+} from "@gnu-taler/taler-util";
+import {
+  FormDesign,
   FormUI,
-  TalerFormAttributes,
-  UIHandlerId,
   InternationalizationAPI,
-  UIFormElementConfig,
-  FormDesign,
   onComponentUnload,
+  UIFormElementConfig,
+  UIHandlerId,
+  useExchangeApiContext,
+  useForm,
+  useTranslationContext,
 } from "@gnu-taler/web-util/browser";
 import { h, VNode } from "preact";
 import { useCurrentDecisionRequest } from "../../hooks/decision-request.js";
 import { usePreferences } from "../../hooks/preferences.js";
-import { MeasureInformation } from "@gnu-taler/taler-util";
+import { AML_EVENTS_INFO, AmlEventsName } from "./aml-events.js";
 
 /**
  * Trigger additional events
@@ -26,21 +30,36 @@ export function Events({}: {}): VNode {
   const [pref] = usePreferences();
   const { config } = useExchangeApiContext();
 
-  const calculatedProps = {
-    ...(request.properties ?? {}),
-    ...(request.custom_properties ?? {}),
-  };
+  const dialect =
+    (pref.testingDialect ? undefined : config.config.aml_spa_dialect) ??
+    AmlSpaDialect.TESTING;
 
-  const calculatedEvents = eventsByDialect(
-    i18n,
-    pref.testingDialect ? "testing" : config.config.aml_spa_dialect,
-    calculatedProps,
+  const calculatedEvents = Object.entries(AML_EVENTS_INFO).reduce(
+    (prev, [name, info]) => {
+      const field = {
+        id: name as UIHandlerId,
+        type: "toggle",
+        required: true,
+        label: labelForEvent(name as AmlEventsName, i18n),
+      } satisfies UIFormElementConfig;
+
+      if (info.shouldBeTriggered(request, dialect)) {
+        prev.on.push(field);
+      } else {
+        prev.off.push(field);
+      }
+      return prev;
+    },
+    { on: [], off: [] } as {
+      on: UIFormElementConfig[];
+      off: UIFormElementConfig[];
+    },
   );
 
-  const design = eventsForm(i18n, calculatedEvents);
+  const design = formDesign(i18n, calculatedEvents.on);
 
-  const form = useForm<EventsForm>(design, {
-    inhibit: calculatedEvents.reduce(
+  const form = useForm<FormType>(design, {
+    inhibit: calculatedEvents.on.reduce(
       (prev, cur) => {
         if (cur.type !== "toggle") return prev;
         const isInhibit =
@@ -49,7 +68,7 @@ export function Events({}: {}): VNode {
         prev[cur.id] = isInhibit;
         return prev;
       },
-      {} as EventsForm["inhibit"],
+      {} as FormType["inhibit"],
     ),
     trigger: !request.custom_events
       ? []
@@ -75,28 +94,28 @@ export function Events({}: {}): VNode {
   );
 }
 
-export type EventsForm = {
+type FormType = {
   trigger: { name: string }[];
   inhibit: { [name: string]: boolean };
 };
 
-export const eventsForm = (
+const formDesign = (
   i18n: InternationalizationAPI,
-  defaultEvents: UIFormElementConfig[],
+  inhibitEvents: UIFormElementConfig[],
 ): FormDesign<MeasureInformation> => ({
   type: "double-column",
   sections: [
     {
       title: i18n.str`Inhibit default events`,
-      description: i18n.str`Use this form to prevent events to be triggered by 
the current status.`,
-      fields: !defaultEvents.length
+      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.`,
             },
           ]
-        : defaultEvents.map((f) =>
+        : inhibitEvents.map((f) =>
             "id" in f ? { ...f, id: ("inhibit." + f.id) as UIHandlerId } : f,
           ),
     },
@@ -120,89 +139,44 @@ export const eventsForm = (
       ],
     },
   ],
-  // fields: [
-  //   {
-  //     id: "trigger" as UIHandlerId,
-  //     type: "array",
-  //     labelFieldId: "name" as UIHandlerId,
-  //     label: i18n.str`Trigger`,
-  //     fields: [],
-  //   },
-  //   {
-  //     id: "inhibit" as UIHandlerId,
-  //     type: "array",
-  //     labelFieldId: "name" as UIHandlerId,
-  //     label: i18n.str`Inhibit`,
-  //     fields: [],
-  //   },
-  // ],
 });
 
-export function eventsByDialect(
-  i18n: InternationalizationAPI,
-  dialect: string | undefined,
-  properties: object,
-): UIFormElementConfig[] {
-  if (!dialect) return [];
-  const result: UIFormElementConfig[] = [];
-  switch (dialect) {
-    case "testing": {
-      const props = properties as TalerFormAttributes.AccountProperties_TOPS;
-      if (props.ACCOUNT_FROZEN) {
-        result.push({
-          id: "ACCOUNT_FROZEN" satisfies keyof 
TalerFormAttributes.AccountProperties_TOPS as UIHandlerId,
-          label: i18n.str`Is froozen?`,
-          // gana_type: "Boolean",
-          type: "toggle",
-          required: true,
-        });
-      }
-      if (props.ACCOUNT_SANCTIONED) {
-        result.push({
-          id: "ACCOUNT_SANCTIONED" satisfies keyof 
TalerFormAttributes.AccountProperties_TOPS as UIHandlerId,
-          label: i18n.str`Is sacntioned?`,
-          // gana_type: "Boolean",
-          type: "toggle",
-          required: true,
-        });
-      }
-      if (props.ACCOUNT_HIGH_RISK) {
-        result.push({
-          id: "ACCOUNT_HIGH_RISK" satisfies keyof 
TalerFormAttributes.AccountProperties_TOPS as UIHandlerId,
-          label: i18n.str`Is high risk?`,
-          // gana_type: "Boolean",
-          type: "toggle",
-          required: true,
-        });
-      }
-      break;
-    }
-    case "gls": {
-      const props = properties as TalerFormAttributes.AccountProperties_TOPS;
-      if (props.ACCOUNT_FROZEN) {
-        result.push({
-          id: "ACCOUNT_FROZEN" satisfies keyof 
TalerFormAttributes.AccountProperties_TOPS as UIHandlerId,
-          label: i18n.str`Is frozen?`,
-          // gana_type: "Boolean",
-          type: "toggle",
-          required: true,
-        });
-      }
-      break;
-    }
-    case "tops": {
-      const props = properties as TalerFormAttributes.AccountProperties_TOPS;
-      if (props.ACCOUNT_HIGH_RISK) {
-        result.push({
-          id: "ACCOUNT_HIGH_RISK" satisfies keyof 
TalerFormAttributes.AccountProperties_TOPS as UIHandlerId,
-          label: i18n.str`Is high risk?`,
-          // gana_type: "Boolean",
-          type: "toggle",
-          required: true,
-        });
-      }
-      break;
+function labelForEvent(event: AmlEventsName, i18n: InternationalizationAPI) {
+  switch (event) {
+    case AmlEventsName.ACCOUNT_FROZEN:
+      return i18n.str`new account frozen`;
+    case AmlEventsName.ACCOUNT_PEP:
+      return i18n.str`new exposed person`;
+    case AmlEventsName.TEST_EVENT_KEY_1:
+      return i18n.str`TEST_EVENT_KEY_1`;
+    case AmlEventsName.TEST_EVENT_KEY_2:
+      return i18n.str`TEST_EVENT_KEY_2`;
+    case AmlEventsName.TEST_EVENT_KEY_3:
+      return i18n.str`TEST_EVENT_KEY_3`;
+    case AmlEventsName.TEST_EVENT_KEY_4:
+      return i18n.str`TEST_EVENT_KEY_4`;
+    case AmlEventsName.TEST_EVENT_KEY_5:
+      return i18n.str`TEST_EVENT_KEY_5`;
+    case AmlEventsName.TEST_EVENT_KEY_6:
+      return i18n.str`TEST_EVENT_KEY_6`;
+    case AmlEventsName.TEST_EVENT_KEY_7:
+      return i18n.str`TEST_EVENT_KEY_7`;
+    case AmlEventsName.TEST_EVENT_KEY_8:
+      return i18n.str`TEST_EVENT_KEY_8`;
+    case AmlEventsName.TEST_EVENT_KEY_9:
+      return i18n.str`TEST_EVENT_KEY_9`;
+    case AmlEventsName.TEST_EVENT_KEY_10:
+      return i18n.str`TEST_EVENT_KEY_10`;
+    case AmlEventsName.TEST_EVENT_KEY_11:
+      return i18n.str`TEST_EVENT_KEY_11`;
+    case AmlEventsName.TEST_EVENT_KEY_12:
+      return i18n.str`TEST_EVENT_KEY_12`;
+    case AmlEventsName.TEST_EVENT_KEY_13:
+      return i18n.str`TEST_EVENT_KEY_13`;
+    case AmlEventsName.TEST_EVENT_KEY_14:
+      return i18n.str`TEST_EVENT_KEY_14`;
+    default: {
+      assertUnreachable(event);
     }
   }
-  return result;
 }
diff --git a/packages/aml-backoffice-ui/src/pages/decision/Justification.tsx 
b/packages/aml-backoffice-ui/src/pages/decision/Justification.tsx
index 32c7110b2..a059d755d 100644
--- a/packages/aml-backoffice-ui/src/pages/decision/Justification.tsx
+++ b/packages/aml-backoffice-ui/src/pages/decision/Justification.tsx
@@ -1,6 +1,27 @@
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import {
+  FormDesign,
+  FormUI,
+  InternationalizationAPI,
+  onComponentUnload,
+  UIHandlerId,
+  useForm,
+  useTranslationContext,
+} from "@gnu-taler/web-util/browser";
 import { h, VNode } from "preact";
 import { useCurrentDecisionRequest } from "../../hooks/decision-request.js";
+import {
+  AbsoluteTime,
+  Duration,
+  MeasureInformation,
+  TalerError,
+} from "@gnu-taler/taler-util";
+import {
+  deserializeMeasures,
+  measureArrayField,
+  MeasurePath,
+  serializeMeasures,
+} from "./Measures.js";
+import { useServerMeasures } from "../../hooks/server-info.js";
 
 /**
  * Mark for further investigation and explain decision
@@ -9,6 +30,98 @@ import { useCurrentDecisionRequest } from 
"../../hooks/decision-request.js";
  */
 export function Justification({}: {}): VNode {
   const { i18n } = useTranslationContext();
-  const [request] = useCurrentDecisionRequest();
-  return <div> not yet impltemented: justification and investigation</div>;
+  const [request, _, updateRequest] = useCurrentDecisionRequest();
+  const measures = useServerMeasures();
+  const measureList =
+    !measures || measures instanceof TalerError || measures.type === "fail"
+      ? []
+      : Object.entries(measures.body.roots).map(([id, mi]) => ({ id, ...mi }));
+  const design = formDesign(i18n, measureList);
+
+  const expMeasres: MeasurePath[] = !request.onExpire_measures
+    ? []
+    : deserializeMeasures(request.onExpire_measures);
+
+  const form = useForm<FormType>(design, {
+    investigate: request.keep_investigating,
+    justification: request.justification,
+    expiration: request.deadline,
+    measures: expMeasres,
+  });
+
+  onComponentUnload(() => {
+    updateRequest({
+      ...request,
+      keep_investigating: !!form.status.result.investigate,
+      justification: form.status.result.justification ?? "",
+      onExpire_measures: serializeMeasures(
+        (form.status.result.measures ?? []) as MeasurePath[],
+      ),
+
+      deadline:
+        (form.status.result.expiration as AbsoluteTime) ?? 
AbsoluteTime.never(),
+      // onExpire_measures,
+    });
+  });
+
+  return (
+    <div>
+      <FormUI design={design} handler={form.handler} />
+    </div>
+  );
 }
+
+type FormType = {
+  justification: string;
+  investigate: boolean;
+  expiration: AbsoluteTime;
+  measures: MeasurePath[];
+};
+
+const formDesign = (
+  i18n: InternationalizationAPI,
+  mi: (MeasureInformation & { id: string })[],
+): FormDesign<FormType> => ({
+  type: "single-column",
+  fields: [
+    {
+      id: "justification" as UIHandlerId,
+      type: "textArea",
+      label: i18n.str`Justification`,
+    },
+    {
+      id: "investigate" as UIHandlerId,
+      type: "toggle",
+      label: i18n.str`Keep investigation?`,
+    },
+    {
+      type: "choiceHorizontal",
+      label: i18n.str`Expiration`,
+      id: "expiration" as UIHandlerId,
+      choices: [
+        {
+          label: i18n.str`In a week`,
+          value: AbsoluteTime.addDuration(
+            AbsoluteTime.now(),
+            Duration.fromSpec({ days: 7 }),
+          ) as any,
+        },
+        {
+          label: i18n.str`In a month`,
+          value: AbsoluteTime.addDuration(
+            AbsoluteTime.now(),
+            Duration.fromSpec({ months: 1 }),
+          ) as any,
+        },
+      ],
+    },
+    {
+      id: "expiration" as UIHandlerId,
+      type: "absoluteTimeText",
+      placeholder: "dd/MM/yyyy",
+      pattern: "dd/MM/yyyy",
+      label: i18n.str`Expiration`,
+    },
+    measureArrayField(i18n, mi),
+  ],
+});
diff --git a/packages/aml-backoffice-ui/src/pages/decision/Measures.tsx 
b/packages/aml-backoffice-ui/src/pages/decision/Measures.tsx
index 1ef4d8859..0969e733b 100644
--- a/packages/aml-backoffice-ui/src/pages/decision/Measures.tsx
+++ b/packages/aml-backoffice-ui/src/pages/decision/Measures.tsx
@@ -11,9 +11,9 @@ import {
   RecursivePartial,
 } from "@gnu-taler/web-util/browser";
 import { h, VNode } from "preact";
-import { useServerMeasures } from "../../hooks/account.js";
 import { useCurrentDecisionRequest } from "../../hooks/decision-request.js";
 import { ShowMeasuresToSelect } from "../CaseDetails.js";
+import { useServerMeasures } from "../../hooks/server-info.js";
 
 export function serializeMeasures(
   paths?: RecursivePartial<MeasurePath[]>,
@@ -47,12 +47,12 @@ export function Measures({}: {}): VNode {
       ? []
       : Object.entries(measures.body.roots).map(([id, mi]) => ({ id, ...mi }));
 
-  const initValue: MeasureForm = !request.new_measures
+  const initValue: FormType = !request.new_measures
     ? { paths: [] }
     : { paths: deserializeMeasures(request.new_measures) };
 
-  const design = measureForm(i18n, measureList);
-  const form = useForm<MeasureForm>(design, initValue);
+  const design = formDesign(i18n, measureList);
+  const form = useForm<FormType>(design, initValue);
 
   onComponentUnload(() => {
     const r = !form.status.result.paths
@@ -77,9 +77,9 @@ export function Measures({}: {}): VNode {
   );
 }
 
-type MeasurePath = { steps: string[] };
+export type MeasurePath = { steps: string[] };
 
-type MeasureForm = {
+type FormType = {
   paths: MeasurePath[];
 };
 
@@ -110,10 +110,10 @@ export function measureArrayField(
   };
 }
 
-function measureForm(
+function formDesign(
   i18n: InternationalizationAPI,
   mi: (MeasureInformation & { id: string })[],
-): FormDesign<MeasureForm> {
+): FormDesign<FormType> {
   return {
     type: "single-column",
     fields: [measureArrayField(i18n, mi)],
diff --git a/packages/aml-backoffice-ui/src/pages/decision/Properties.tsx 
b/packages/aml-backoffice-ui/src/pages/decision/Properties.tsx
index ef685e857..d74b93cc8 100644
--- a/packages/aml-backoffice-ui/src/pages/decision/Properties.tsx
+++ b/packages/aml-backoffice-ui/src/pages/decision/Properties.tsx
@@ -13,6 +13,7 @@ import {
 import { h, VNode } from "preact";
 import { useCurrentDecisionRequest } from "../../hooks/decision-request.js";
 import { usePreferences } from "../../hooks/preferences.js";
+import { AmlSpaDialect } from "@gnu-taler/taler-util";
 
 /**
  * Update account properites
@@ -115,7 +116,7 @@ export function propertiesByDialect(
 ): UIFormElementConfig[] {
   if (!dialect) return [];
   switch (dialect) {
-    case "testing": {
+    case AmlSpaDialect.TESTING: {
       return [
         {
           id: "ACCOUNT_PEP" satisfies keyof 
TalerFormAttributes.AccountProperties_Testing as UIHandlerId,
@@ -124,9 +125,23 @@ export function propertiesByDialect(
           type: "toggle",
           required: true,
         },
+        {
+          id: "ACCOUNT_BUSINESS_DOMAIN" satisfies keyof 
TalerFormAttributes.AccountProperties_Testing as UIHandlerId,
+          label: i18n.str`Business domain`,
+          // gana_type: "Text",
+          type: "text",
+          required: true,
+        },
+        {
+          id: "ACCOUNT_FROZEN" satisfies keyof 
TalerFormAttributes.AccountProperties_Testing as UIHandlerId,
+          label: i18n.str`Is frozen?`,
+          // gana_type: "Boolean",
+          type: "toggle",
+          required: true,
+        },
       ];
     }
-    case "gls": {
+    case AmlSpaDialect.GLS: {
       return [
         {
           id: "ACCOUNT_REPORTED" satisfies keyof 
TalerFormAttributes.AccountProperties_GLS as UIHandlerId,
@@ -137,7 +152,7 @@ export function propertiesByDialect(
         },
       ];
     }
-    case "tops": {
+    case AmlSpaDialect.TOPS: {
       return [
         {
           id: "ACCOUNT_FROZEN" satisfies keyof 
TalerFormAttributes.AccountProperties_TOPS as UIHandlerId,
diff --git a/packages/aml-backoffice-ui/src/pages/decision/Rules.tsx 
b/packages/aml-backoffice-ui/src/pages/decision/Rules.tsx
index db1653e02..9bf5657d7 100644
--- a/packages/aml-backoffice-ui/src/pages/decision/Rules.tsx
+++ b/packages/aml-backoffice-ui/src/pages/decision/Rules.tsx
@@ -20,12 +20,11 @@ import {
   useTranslationContext,
 } from "@gnu-taler/web-util/browser";
 import { h, VNode } from "preact";
-import { useState } from "preact/hooks";
-import { useServerMeasures } from "../../hooks/account.js";
 import { useCurrentDecisionRequest } from "../../hooks/decision-request.js";
 import { useAccountActiveDecision } from "../../hooks/decisions.js";
 import { RulesInfo, ShowDecisionLimitInfo } from "../CaseDetails.js";
 import { measureArrayField, serializeMeasures } from "./Measures.js";
+import { useServerMeasures } from "../../hooks/server-info.js";
 
 /**
  * Defined new limits for the account
@@ -55,6 +54,12 @@ export function Rules({ account }: { account?: string }): 
VNode {
       ? undefined
       : activeDecision.body;
 
+  onComponentUnload(() => {
+    if (!request.rules) {
+      updateRequest("rules", []);
+    }
+  });
+
   function addNewRule(nr: FormType) {
     const result = !request.rules ? [] : [...request.rules];
     result.push({
@@ -132,13 +137,6 @@ type FormType = {
   paths: { steps: Array<string> }[];
 };
 
-// operation_type: LimitOperationType;
-// threshold: AmountString;
-// timeframe: RelativeTime;
-// measures: string[];
-// display_priority: Integer;
-// exposed?: boolean;
-// is_and_combinator?: boolean;
 function labelForOperationType(
   op: LimitOperationType,
   i18n: InternationalizationAPI,
diff --git a/packages/aml-backoffice-ui/src/pages/decision/Justification.tsx 
b/packages/aml-backoffice-ui/src/pages/decision/Summary.tsx
similarity index 55%
copy from packages/aml-backoffice-ui/src/pages/decision/Justification.tsx
copy to packages/aml-backoffice-ui/src/pages/decision/Summary.tsx
index 32c7110b2..37fa9b493 100644
--- a/packages/aml-backoffice-ui/src/pages/decision/Justification.tsx
+++ b/packages/aml-backoffice-ui/src/pages/decision/Summary.tsx
@@ -1,4 +1,12 @@
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import {
+  FormDesign,
+  FormUI,
+  InternationalizationAPI,
+  onComponentUnload,
+  UIHandlerId,
+  useForm,
+  useTranslationContext,
+} from "@gnu-taler/web-util/browser";
 import { h, VNode } from "preact";
 import { useCurrentDecisionRequest } from "../../hooks/decision-request.js";
 
@@ -7,8 +15,9 @@ import { useCurrentDecisionRequest } from 
"../../hooks/decision-request.js";
  * @param param0
  * @returns
  */
-export function Justification({}: {}): VNode {
+export function Summary({}: {}): VNode {
   const { i18n } = useTranslationContext();
   const [request] = useCurrentDecisionRequest();
-  return <div> not yet impltemented: justification and investigation</div>;
+
+  return <div>summary</div>;
 }
diff --git a/packages/aml-backoffice-ui/src/pages/decision/aml-events.ts 
b/packages/aml-backoffice-ui/src/pages/decision/aml-events.ts
new file mode 100644
index 000000000..535e68a82
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/pages/decision/aml-events.ts
@@ -0,0 +1,133 @@
+import { AmlSpaDialect } from "@gnu-taler/taler-util";
+import { TalerFormAttributes } from "@gnu-taler/web-util/browser";
+import { DecisionRequest } from "../../hooks/decision-request.js";
+
+export enum AmlEventsName {
+  ACCOUNT_FROZEN = "ACCOUNT_FROZEN",
+  ACCOUNT_PEP = "ACCOUNT_PEP",
+  TEST_EVENT_KEY_1 = "TEST_EVENT_KEY_1",
+  TEST_EVENT_KEY_2 = "TEST_EVENT_KEY_2",
+  TEST_EVENT_KEY_3 = "TEST_EVENT_KEY_3",
+  TEST_EVENT_KEY_4 = "TEST_EVENT_KEY_4",
+  TEST_EVENT_KEY_5 = "TEST_EVENT_KEY_5",
+  TEST_EVENT_KEY_6 = "TEST_EVENT_KEY_6",
+  TEST_EVENT_KEY_7 = "TEST_EVENT_KEY_7",
+  TEST_EVENT_KEY_8 = "TEST_EVENT_KEY_8",
+  TEST_EVENT_KEY_9 = "TEST_EVENT_KEY_9",
+  TEST_EVENT_KEY_10 = "TEST_EVENT_KEY_10",
+  TEST_EVENT_KEY_11 = "TEST_EVENT_KEY_11",
+  TEST_EVENT_KEY_12 = "TEST_EVENT_KEY_12",
+  TEST_EVENT_KEY_13 = "TEST_EVENT_KEY_13",
+  TEST_EVENT_KEY_14 = "TEST_EVENT_KEY_14",
+}
+
+export type EventMapInfo = {
+  [name in AmlEventsName]: {
+    // fieldLabel: TranslatedString;
+    dialect: AmlSpaDialect[];
+    shouldBeTriggered: (req: DecisionRequest, d: AmlSpaDialect) => boolean;
+  };
+};
+
+export const AML_EVENTS_INFO: EventMapInfo = {
+  ACCOUNT_FROZEN: {
+    dialect: [AmlSpaDialect.TESTING],
+    shouldBeTriggered(req, dialect) {
+      if (!req.properties) return false;
+      return !!(req.properties as TalerFormAttributes.AccountProperties_TOPS)
+        .ACCOUNT_FROZEN;
+    },
+  },
+  ACCOUNT_PEP: {
+    dialect: [AmlSpaDialect.GLS, AmlSpaDialect.TOPS, AmlSpaDialect.TESTING],
+    shouldBeTriggered(req, dialect) {
+      if (!req.properties) return false;
+      return !!(req.properties as TalerFormAttributes.AccountProperties_TOPS)
+        .ACCOUNT_PEP;
+    },
+  },
+  TEST_EVENT_KEY_1: {
+    dialect: [AmlSpaDialect.TESTING],
+    shouldBeTriggered(req, dialect) {
+      return true;
+    },
+  },
+  TEST_EVENT_KEY_2: {
+    dialect: [AmlSpaDialect.TESTING],
+    shouldBeTriggered(req, dialect) {
+      return true;
+    },
+  },
+  TEST_EVENT_KEY_3: {
+    dialect: [AmlSpaDialect.TESTING],
+    shouldBeTriggered(req, dialect) {
+      return true;
+    },
+  },
+  TEST_EVENT_KEY_4: {
+    dialect: [AmlSpaDialect.TESTING],
+    shouldBeTriggered(req, dialect) {
+      return true;
+    },
+  },
+  TEST_EVENT_KEY_5: {
+    dialect: [AmlSpaDialect.TESTING],
+    shouldBeTriggered(req, dialect) {
+      return true;
+    },
+  },
+  TEST_EVENT_KEY_6: {
+    dialect: [AmlSpaDialect.TESTING],
+    shouldBeTriggered(req, dialect) {
+      return true;
+    },
+  },
+  TEST_EVENT_KEY_7: {
+    dialect: [AmlSpaDialect.TESTING],
+    shouldBeTriggered(req, dialect) {
+      return true;
+    },
+  },
+  TEST_EVENT_KEY_8: {
+    dialect: [AmlSpaDialect.TESTING],
+    shouldBeTriggered(req, dialect) {
+      return true;
+    },
+  },
+  TEST_EVENT_KEY_9: {
+    dialect: [AmlSpaDialect.TESTING],
+    shouldBeTriggered(req, dialect) {
+      return true;
+    },
+  },
+  TEST_EVENT_KEY_10: {
+    dialect: [AmlSpaDialect.TESTING],
+    shouldBeTriggered(req, dialect) {
+      return true;
+    },
+  },
+  TEST_EVENT_KEY_11: {
+    dialect: [AmlSpaDialect.TESTING],
+    shouldBeTriggered(req, dialect) {
+      return true;
+    },
+  },
+  TEST_EVENT_KEY_12: {
+    dialect: [AmlSpaDialect.TESTING],
+    shouldBeTriggered(req, dialect) {
+      return true;
+    },
+  },
+  TEST_EVENT_KEY_13: {
+    dialect: [AmlSpaDialect.TESTING],
+    shouldBeTriggered(req, dialect) {
+      return true;
+    },
+  },
+  TEST_EVENT_KEY_14: {
+    dialect: [AmlSpaDialect.TESTING],
+    shouldBeTriggered(req, dialect) {
+      return true;
+    },
+  },
+};
diff --git a/packages/bank-ui/src/hooks/regional.ts 
b/packages/bank-ui/src/hooks/regional.ts
index 417874cf8..d960a0f1f 100644
--- a/packages/bank-ui/src/hooks/regional.ts
+++ b/packages/bank-ui/src/hooks/regional.ts
@@ -42,10 +42,10 @@ const useSWR = _useSWR as unknown as SWRHook;
 
 export type TransferCalculation =
   | {
-    debit: AmountJson;
-    credit: AmountJson;
-    beforeFee: AmountJson;
-  }
+      debit: AmountJson;
+      credit: AmountJson;
+      beforeFee: AmountJson;
+    }
   | "amount-is-too-small";
 type EstimatorFunction = (
   amount: AmountJson,
@@ -112,7 +112,9 @@ export function useCashinEstimator(): ConversionEstimators {
             if (resp.detail) {
               throw TalerError.fromUncheckedDetail(resp.detail);
             } else {
-              throw TalerError.fromException(new Error("failed to get 
conversion cashin rate"))
+              throw TalerError.fromException(
+                new Error("failed to get conversion cashin rate"),
+              );
             }
         }
       }
@@ -141,7 +143,9 @@ export function useCashinEstimator(): ConversionEstimators {
             if (resp.detail) {
               throw TalerError.fromUncheckedDetail(resp.detail);
             } else {
-              throw TalerError.fromException(new Error("failed to get 
conversion cashin rate"))
+              throw TalerError.fromException(
+                new Error("failed to get conversion cashin rate"),
+              );
             }
         }
       }
@@ -178,7 +182,9 @@ export function useCashoutEstimator(): ConversionEstimators 
{
             if (resp.detail) {
               throw TalerError.fromUncheckedDetail(resp.detail);
             } else {
-              throw TalerError.fromException(new Error("failed to get 
conversion cashout rate"))
+              throw TalerError.fromException(
+                new Error("failed to get conversion cashout rate"),
+              );
             }
         }
       }
@@ -207,7 +213,9 @@ export function useCashoutEstimator(): ConversionEstimators 
{
             if (resp.detail) {
               throw TalerError.fromUncheckedDetail(resp.detail);
             } else {
-              throw TalerError.fromException(new Error("failed to get 
conversion cashout rate"))
+              throw TalerError.fromException(
+                new Error("failed to get conversion cashout rate"),
+              );
             }
         }
       }
@@ -484,6 +492,7 @@ export function useLastMonitorInfo(
     lib: { bank: api },
   } = useBankCoreApiContext();
   const { state: credentials } = useSessionState();
+
   const token =
     credentials.status !== "loggedIn" ? undefined : credentials.token;
 
diff --git a/packages/bank-ui/src/pages/admin/AdminHome.tsx 
b/packages/bank-ui/src/pages/admin/AdminHome.tsx
index 863886605..1f8b1ad18 100644
--- a/packages/bank-ui/src/pages/admin/AdminHome.tsx
+++ b/packages/bank-ui/src/pages/admin/AdminHome.tsx
@@ -187,7 +187,7 @@ export function getTimeframesForDate(
           sub(time, { days: 1 }).getTime(),
         ),
         previous: AbsoluteTime.fromMilliseconds(
-          sub(time, { days: 4 }).getTime(),
+          sub(time, { days: 2 }).getTime(),
         ),
       };
     case TalerCorebankApi.MonitorTimeframeParam.month:
@@ -241,11 +241,10 @@ function Metrics({
   if (resp instanceof TalerError) {
     return <ErrorLoadingWithDebug error={resp} />;
   }
-  if (!respInfo) return <Fragment />;
-  if (respInfo instanceof TalerError) {
+  if (respInfo && respInfo instanceof TalerError) {
     return <ErrorLoadingWithDebug error={respInfo} />;
   }
-  if (respInfo.type === "fail") {
+  if (respInfo && respInfo.type === "fail") {
     switch (respInfo.case) {
       case HttpStatusCode.NotImplemented: {
         return (
@@ -483,7 +482,8 @@ function Metrics({
         </h1>
       </div>
       <dl class="mt-5 grid grid-cols-1 md:grid-cols-2  divide-y 
divide-gray-200 overflow-hidden rounded-lg bg-white shadow-lg md:divide-x 
md:divide-y-0">
-        {resp.current.body.type !== "with-conversions" ||
+        {!respInfo ||
+        resp.current.body.type !== "with-conversions" ||
         resp.previous.body.type !== "with-conversions" ? undefined : (
           <Fragment>
             <div class="px-4 py-5 sm:p-6">
@@ -496,7 +496,7 @@ function Metrics({
                   </i18n.Translate>
                 </div>
               </dt>
-              <MetricValue
+              <MetricValueAmount
                 current={resp.current.body.cashinFiatVolume}
                 previous={resp.previous.body.cashinFiatVolume}
                 spec={respInfo.body.fiat_currency_specification}
@@ -512,7 +512,7 @@ function Metrics({
                   account.
                 </i18n.Translate>
               </div>
-              <MetricValue
+              <MetricValueAmount
                 current={resp.current.body.cashoutFiatVolume}
                 previous={resp.previous.body.cashoutFiatVolume}
                 spec={respInfo.body.fiat_currency_specification}
@@ -529,7 +529,7 @@ function Metrics({
               </i18n.Translate>
             </div>
           </dt>
-          <MetricValue
+          <MetricValueAmount
             current={resp.current.body.talerInVolume}
             previous={resp.previous.body.talerInVolume}
             spec={config.currency_specification}
@@ -544,12 +544,40 @@ function Metrics({
               </i18n.Translate>
             </div>
           </dt>
-          <MetricValue
+          <MetricValueAmount
             current={resp.current.body.talerOutVolume}
             previous={resp.previous.body.talerOutVolume}
             spec={config.currency_specification}
           />
         </div>
+        <div class="px-4 py-5 sm:p-6">
+          <dt class="text-base font-normal text-gray-900">
+            <i18n.Translate>Payin</i18n.Translate>
+            <div class="text-xs text-gray-500">
+              <i18n.Translate>
+                Transferred from an account to a Taler exchange.
+              </i18n.Translate>
+            </div>
+          </dt>
+          <MetricValueNumber
+            current={resp.current.body.talerInCount}
+            previous={resp.previous.body.talerInCount}
+          />
+        </div>
+        <div class="px-4 py-5 sm:p-6">
+          <dt class="text-base font-normal text-gray-900">
+            <i18n.Translate>Payout</i18n.Translate>
+            <div class="text-xs text-gray-500">
+              <i18n.Translate>
+                Transferred from a Taler exchange to another account.
+              </i18n.Translate>
+            </div>
+          </dt>
+          <MetricValueNumber
+            current={resp.current.body.talerOutCount}
+            previous={resp.previous.body.talerOutCount}
+          />
+        </div>
       </dl>
       <div class="flex justify-end mt-4">
         <a
@@ -564,7 +592,7 @@ function Metrics({
   );
 }
 
-function MetricValue({
+function MetricValueAmount({
   current,
   previous,
   spec,
@@ -678,3 +706,93 @@ function MetricValue({
     </Fragment>
   );
 }
+
+function MetricValueNumber({
+  current,
+  previous,
+}: {
+  current: number | undefined;
+  previous: number | undefined;
+}): VNode {
+  const { i18n } = useTranslationContext();
+
+  const cmp = current && previous ? (current < previous ? -1 : 1) : 0;
+
+  const rate =
+    !current || Number.isNaN(current) || !previous || Number.isNaN(previous)
+      ? 0
+      : cmp === -1
+        ? 1 - Math.round(current) / Math.round(previous)
+        : cmp === 1
+          ? Math.round(current) / Math.round(previous) - 1
+          : 0;
+
+  const negative = cmp === 0 ? undefined : cmp === -1;
+  const rateStr = `${(Math.abs(rate) * 100).toFixed(2)}%`;
+  return (
+    <Fragment>
+      <dd class="mt-1 block ">
+        <div class="flex justify-start text-2xl items-baseline font-semibold 
text-indigo-600">
+          {!current ? "-" : current}
+        </div>
+        <div class="flex flex-col">
+          <div class="flex justify-end items-baseline text-2xl font-semibold 
text-indigo-600">
+            <small class="ml-2 text-sm font-medium text-gray-500">
+              <i18n.Translate>previous</i18n.Translate>{" "}
+              {!previous ? "-" : previous}
+            </small>
+          </div>
+          {!!rate && (
+            <span
+              data-negative={negative}
+              class="flex items-center gap-x-1.5 w-fit rounded-md bg-green-100 
text-green-800 data-[negative=true]:bg-red-100 px-2 py-1 text-xs font-medium 
data-[negative=true]:text-red-700 whitespace-pre"
+            >
+              {negative ? (
+                <svg
+                  xmlns="http://www.w3.org/2000/svg";
+                  fill="none"
+                  viewBox="0 0 24 24"
+                  stroke-width="1.5"
+                  stroke="currentColor"
+                  class="w-6 h-6"
+                >
+                  <path
+                    stroke-linecap="round"
+                    stroke-linejoin="round"
+                    d="M12 4.5v15m0 0l6.75-6.75M12 19.5l-6.75-6.75"
+                  />
+                </svg>
+              ) : (
+                <svg
+                  xmlns="http://www.w3.org/2000/svg";
+                  fill="none"
+                  viewBox="0 0 24 24"
+                  stroke-width="1.5"
+                  stroke="currentColor"
+                  class="w-6 h-6"
+                >
+                  <path
+                    stroke-linecap="round"
+                    stroke-linejoin="round"
+                    d="M12 19.5v-15m0 0l-6.75 6.75M12 4.5l6.75 6.75"
+                  />
+                </svg>
+              )}
+
+              {negative ? (
+                <span class="sr-only">
+                  <i18n.Translate>Decreased by</i18n.Translate>
+                </span>
+              ) : (
+                <span class="sr-only">
+                  <i18n.Translate>Increased by</i18n.Translate>
+                </span>
+              )}
+              {rateStr}
+            </span>
+          )}
+        </div>
+      </dd>
+    </Fragment>
+  );
+}
diff --git a/packages/kyc-ui/src/forms/index.ts 
b/packages/kyc-ui/src/forms/index.ts
index 29de3b13a..2f626c610 100644
--- a/packages/kyc-ui/src/forms/index.ts
+++ b/packages/kyc-ui/src/forms/index.ts
@@ -15,6 +15,7 @@
  */
 import {
   FormMetadata,
+  GLS_Onboarding,
   InternationalizationAPI,
   VQF_902_1,
   VQF_902_11,
@@ -53,6 +54,12 @@ export const preloadedForms: (
     version: 1,
     config: acceptTos(i18n, context),
   },
+  {
+    label: i18n.str`GLS onboarding`,
+    id: "gls-onboarding",
+    version: 1,
+    config: GLS_Onboarding(i18n),
+  },
   {
     label: i18n.str`Identification Form`,
     description: i18n.str`The customer has to be identified on entering into a 
permanent business relationship or on concluding a cash transaction, which 
meets the according threshold.`,
diff --git a/packages/taler-util/src/http-client/exchange.ts 
b/packages/taler-util/src/http-client/exchange.ts
index 1075921dc..4e30882c7 100644
--- a/packages/taler-util/src/http-client/exchange.ts
+++ b/packages/taler-util/src/http-client/exchange.ts
@@ -81,6 +81,7 @@ import { TalerError } from "../errors.js";
 import { TalerErrorCode } from "../taler-error-codes.js";
 import { codecForEmptyObject } from "../types-taler-wallet.js";
 import { canonicalJson } from "../helpers.js";
+import { AbsoluteTime } from "../time.js";
 
 export type TalerExchangeResultByMethod<
   prop extends keyof TalerExchangeHttpClient,
@@ -861,17 +862,17 @@ export class TalerExchangeHttpClient {
     auth: OfficerAccount,
     name: string,
     filter: {
-      since?: Date;
-      until?: Date;
+      since?: AbsoluteTime;
+      until?: AbsoluteTime;
     } = {},
   ) {
     const url = new URL(`aml/${auth.id}/kyc-statistics/${name}`, this.baseUrl);
 
-    if (filter.since !== undefined) {
-      url.searchParams.set("start_date", String(filter.since.getTime()));
+    if (filter.since !== undefined && filter.since.t_ms !== "never") {
+      url.searchParams.set("start_date", String(filter.since.t_ms));
     }
-    if (filter.until !== undefined) {
-      url.searchParams.set("end_date", String(filter.until.getTime()));
+    if (filter.until !== undefined && filter.until.t_ms !== "never") {
+      url.searchParams.set("end_date", String(filter.until.t_ms));
     }
 
     const resp = await this.httpLib.fetch(url.href, {
diff --git a/packages/taler-util/src/types-taler-exchange.ts 
b/packages/taler-util/src/types-taler-exchange.ts
index c8d0c925a..f78c71e04 100644
--- a/packages/taler-util/src/types-taler-exchange.ts
+++ b/packages/taler-util/src/types-taler-exchange.ts
@@ -1610,6 +1610,12 @@ export type AmlDecisionRequestWithoutSignature = Omit<
   "officer_sig"
 >;
 
+export enum AmlSpaDialect {
+  TOPS = "tops",
+  GLS = "gls",
+  TESTING = "testing",
+}
+
 export interface ExchangeVersionResponse {
   // libtool-style representation of the Exchange protocol version, see
   // 
https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning
@@ -1638,7 +1644,7 @@ export interface ExchangeVersionResponse {
   // to show and sets of properties/events to trigger in
   // AML decisions.
   // @since protocol **v24**.
-  aml_spa_dialect?: string;
+  aml_spa_dialect?: AmlSpaDialect;
 }
 
 export interface WireAccount {
@@ -1780,16 +1786,6 @@ export interface AccountKycStatus {
   limits?: AccountLimit[];
 }
 
-export type LimitOperationType2 =
-  | "WITHDRAW"
-  | "DEPOSIT"
-  | "MERGE"
-  | "AGGREGATE"
-  | "BALANCE"
-  | "REFUND"
-  | "CLOSE"
-  | "TRANSACTION";
-
 export enum LimitOperationType {
   withdraw = "WITHDRAW",
   deposit = "DEPOSIT",
@@ -2446,6 +2442,12 @@ interface CSDenominationKey {
   cs_public_key: Cs25519Point;
 }
 
+export const codecForAmlSpaDialect = codecForEither(
+  codecForConstString(AmlSpaDialect.GLS),
+  codecForConstString(AmlSpaDialect.TOPS),
+  codecForConstString(AmlSpaDialect.TESTING),
+);
+
 export const codecForExchangeConfig = (): Codec<ExchangeVersionResponse> =>
   buildCodecForObject<ExchangeVersionResponse>()
     .property("version", codecForString())
@@ -2454,7 +2456,7 @@ export const codecForExchangeConfig = (): 
Codec<ExchangeVersionResponse> =>
     .property("currency", codecForString())
     .property("currency_specification", codecForCurrencySpecificiation())
     .property("supported_kyc_requirements", codecForList(codecForString()))
-    .property("aml_spa_dialect", codecOptional(codecForString()))
+    .property("aml_spa_dialect", codecOptional(codecForAmlSpaDialect))
     .deprecatedProperty("shopping_url")
     .deprecatedProperty("wallet_balance_limit_without_kyc")
     .build("TalerExchangeApi.ExchangeVersionResponse");
diff --git a/packages/web-util/src/forms/gana/GLS_Onboarding.ts 
b/packages/web-util/src/forms/gana/GLS_Onboarding.ts
new file mode 100644
index 000000000..53ba8673b
--- /dev/null
+++ b/packages/web-util/src/forms/gana/GLS_Onboarding.ts
@@ -0,0 +1,41 @@
+import {
+  InternationalizationAPI,
+  UIHandlerId,
+  SelectUiChoice,
+  DoubleColumnFormDesign,
+} from "@gnu-taler/web-util/browser";
+import { TalerFormAttributes } from "./taler_form_attributes.js";
+
+export function GLS_Onboarding(
+  i18n: InternationalizationAPI,
+): DoubleColumnFormDesign {
+  return {
+    type: "double-column",
+    sections: [
+      {
+        title: i18n.str`This form was completed by`,
+        fields: [
+          {
+            id: "TAX_IS_ACTIVE" satisfies keyof 
TalerFormAttributes.GLS_Onboarding as UIHandlerId,
+            label: i18n.str`Is tax active?`,
+            // gana_type: "AbsoluteDateTime",
+            type: "toggle",
+          },
+        ],
+      },
+      {
+        title: i18n.str`Information on customer`,
+        description: i18n.str`The customer is the person with whom the member 
concludes the contract with regard to the financial service provided (civil 
law). Does the member act as director of a domiciliary company, this 
domiciliary company is the customer.`,
+        fields: [
+          {
+            id: "PERSON_FULL_NAME" satisfies keyof 
TalerFormAttributes.GLS_Onboarding as UIHandlerId,
+            label: i18n.str`Full name`,
+            // gana_type: "String",
+            type: "text",
+            required: true,
+          },
+        ],
+      },
+    ],
+  };
+}
diff --git a/packages/web-util/src/forms/gana/taler_form_attributes.ts 
b/packages/web-util/src/forms/gana/taler_form_attributes.ts
index 7fcd63975..238d58728 100644
--- a/packages/web-util/src/forms/gana/taler_form_attributes.ts
+++ b/packages/web-util/src/forms/gana/taler_form_attributes.ts
@@ -1330,6 +1330,11 @@ export namespace TalerFormAttributes {
      * Required: false
      */
     ACCOUNT_PEP?: Boolean;
+    /**
+     * Is the client's account currently frozen?
+     * Required: false
+     */
+    ACCOUNT_FROZEN?: Boolean;
   }
   export interface AccountProperties_TOPS {
     /**
diff --git a/packages/web-util/src/forms/index.ts 
b/packages/web-util/src/forms/index.ts
index 8908166a9..2d81e5557 100644
--- a/packages/web-util/src/forms/index.ts
+++ b/packages/web-util/src/forms/index.ts
@@ -13,6 +13,7 @@ export * from "./gana/VQF_902_12.js";
 export * from "./gana/VQF_902_13.js";
 export * from "./gana/VQF_902_14.js";
 export * from "./gana/VQF_902_15.js";
+export * from "./gana/GLS_Onboarding.js";
 export * from "./gana/taler_form_attributes.js";
 export * from "./fields/InputAbsoluteTime.js";
 export * from "./fields/InputAmount.js";

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]