commit 5f0106be6bbb4fe9d3730646933eed36c26ca94a
parent d89f0bf6322a8b4540a633c0760cecbfa24e6355
Author: Sebastian <sebasjm@gmail.com>
Date: Tue, 6 May 2025 01:24:35 -0300
tops events and some fixes in VQF forms
Diffstat:
20 files changed, 1190 insertions(+), 639 deletions(-)
diff --git a/packages/aml-backoffice-ui/src/hooks/server-info.ts b/packages/aml-backoffice-ui/src/hooks/server-info.ts
@@ -14,22 +14,28 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import {
- AmlSpaDialect,
- EventCounter,
+ AbsoluteTime,
+ CounterResultByEventName,
+ EventReporting_TOPS_calculation,
+ EventReporting_TOPS_queries,
+ EventReporting_VQF_calculation,
+ EventReporting_VQF_queries,
+ fetchTopsInfoFromServer,
+ fetchVqfInfoFromServer,
GLS_AmlEventsName,
OfficerAccount,
OperationOk,
opFixedSuccess,
- TalerExchangeErrorsByMethod,
+ TalerExchangeHttpClient,
TalerExchangeResultByMethod,
TalerHttpError,
TOPS_AmlEventsName,
} from "@gnu-taler/taler-util";
// FIX default import https://github.com/microsoft/TypeScript/issues/49189
-import { UIHandlerId, useExchangeApiContext } from "@gnu-taler/web-util/browser";
+import { useExchangeApiContext } from "@gnu-taler/web-util/browser";
import _useSWR, { SWRHook } from "swr";
-import { Timeframe } from "../pages/Dashboard.js";
import { useOfficer } from "./officer.js";
+import { endOfYear, setYear, startOfYear } from "date-fns";
const useSWR = _useSWR as unknown as SWRHook;
export function useServerMeasures() {
@@ -64,19 +70,48 @@ export function useServerMeasures() {
return undefined;
}
-export type ServerStats = {
- [name: string]: EventCounter;
-};
+const GLS_EVENTS: GLS_AmlEventsName[] = Object.values(GLS_AmlEventsName).map(
+ (c) => GLS_AmlEventsName[c],
+);
+const TESTING_EVENTS: TOPS_AmlEventsName[] = Object.values(
+ TOPS_AmlEventsName,
+).map((c) => TOPS_AmlEventsName[c]);
+
+export function useTopsServerStatistics() {
+ const officer = useOfficer();
+ const session = officer.state === "ready" ? officer.account : undefined;
+
+ const {
+ unthrottledApi: { exchange: api },
+ } = useExchangeApiContext();
+
+ async function fetcher([officer]: [OfficerAccount]) {
+ const final = await fetchTopsInfoFromServer(api, officer);
+
+ return opFixedSuccess(final);
+ }
+
+ const { data, error } = useSWR<
+ OperationOk<CounterResultByEventName<typeof EventReporting_TOPS_queries>>,
+ TalerHttpError
+ >(!session ? undefined : [session], fetcher, {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ errorRetryCount: 0,
+ errorRetryInterval: 1,
+ shouldRetryOnError: false,
+ keepPreviousData: true,
+ });
-const TOPS_EVENTS: TOPS_AmlEventsName[] = Object.values(TOPS_AmlEventsName).map(c => TOPS_AmlEventsName[c]);
-const GLS_EVENTS: GLS_AmlEventsName[] = Object.values(GLS_AmlEventsName).map(c => GLS_AmlEventsName[c]);
-const TESTING_EVENTS: TOPS_AmlEventsName[] = Object.values(TOPS_AmlEventsName).map(c => TOPS_AmlEventsName[c]);
+ if (data) return data;
+ if (error) return error;
+ return undefined;
+}
-export function useServerStatistics(
- dialect: AmlSpaDialect,
- current: Timeframe,
- previous?: Timeframe,
-) {
+export function useVqfServerStatistics(year: number) {
const officer = useOfficer();
const session = officer.state === "ready" ? officer.account : undefined;
@@ -84,61 +119,22 @@ export function useServerStatistics(
unthrottledApi: { exchange: api },
} = useExchangeApiContext();
- const keys =
- dialect === AmlSpaDialect.GLS
- ? GLS_EVENTS
- : dialect === AmlSpaDialect.TOPS
- ? TOPS_EVENTS
- : TESTING_EVENTS;
-
- type Success = TalerExchangeResultByMethod<"getAmlKycStatistics">;
- type Error = TalerExchangeErrorsByMethod<"getAmlKycStatistics">;
- type Response = {
- key: UIHandlerId;
- current: Success | Error;
- previous: Success | Error | undefined;
- };
-
- async function fetcher([officer, keys, current, previous]: [
- OfficerAccount,
- UIHandlerId[],
- Timeframe,
- Timeframe | undefined,
- ]) {
- const firstQuery = await api.getAmlKycStatistics(officer, "test", {
- since: current.start,
- until: current.end,
- });
- if (firstQuery.type === "fail") {
- return firstQuery;
- }
- const queries = keys.map((key) => {
- return Promise.all([
- api.getAmlKycStatistics(officer, key, {
- since: current.start,
- until: current.end,
- }),
- !previous
- ? undefined
- : api.getAmlKycStatistics(officer, key, {
- since: previous.start,
- until: previous.end,
- }),
- ]).then(([c, p]) => {
- const ret: Response = { key, current: c, previous: p };
- return ret;
- });
- });
-
- const p = await Promise.all(queries);
-
- return opFixedSuccess(p);
+ async function fetcher([officer, year]: [OfficerAccount, number]) {
+ const date = setYear(new Date(), year);
+ const jan_1st = AbsoluteTime.fromMilliseconds(startOfYear(date).getTime());
+ const dec_31st = AbsoluteTime.fromMilliseconds(endOfYear(date).getTime());
+
+ const final = await fetchVqfInfoFromServer(api, officer, jan_1st, dec_31st);
+
+ return opFixedSuccess(final);
}
const { data, error } = useSWR<
- OperationOk<Response[]> | Error,
+ OperationOk<
+ CounterResultByEventName<ReturnType<typeof EventReporting_VQF_queries>>
+ >,
TalerHttpError
- >(!session ? undefined : [session, keys, current, previous], fetcher, {
+ >(!session ? undefined : [session, year], fetcher, {
refreshInterval: 0,
refreshWhenHidden: false,
revalidateOnFocus: false,
diff --git a/packages/aml-backoffice-ui/src/pages/Dashboard.tsx b/packages/aml-backoffice-ui/src/pages/Dashboard.tsx
@@ -17,6 +17,8 @@ import {
AbsoluteTime,
AmlSpaDialect,
assertUnreachable,
+ EventReporting_TOPS_calculation,
+ EventReporting_TOPS_queries,
GLS_AmlEventsName,
HttpStatusCode,
OperationOk,
@@ -42,7 +44,7 @@ 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 { useTopsServerStatistics } from "../hooks/server-info.js";
import { HandleAccountNotReady } from "./HandleAccountNotReady.js";
import { Officer } from "./Officer.js";
@@ -76,14 +78,14 @@ function EventMetrics({
routeToDownloadStats: RouteDefinition;
}): VNode {
const { i18n, dateLocale } = useTranslationContext();
- const [metricType, setMetricType] =
- useState<TalerCorebankApi.MonitorTimeframeParam>(
- TalerCorebankApi.MonitorTimeframeParam.hour,
- );
- const params = useMemo(
- () => getTimeframesForDate(now, metricType),
- [metricType],
- );
+ // const [metricType, setMetricType] =
+ // useState<TalerCorebankApi.MonitorTimeframeParam>(
+ // TalerCorebankApi.MonitorTimeframeParam.hour,
+ // );
+ // const params = useMemo(
+ // () => getTimeframesForDate(now, metricType),
+ // [metricType],
+ // );
const [pref] = usePreferences();
const { config } = useExchangeApiContext();
@@ -92,7 +94,7 @@ function EventMetrics({
(pref.testingDialect ? undefined : config.config.aml_spa_dialect) ??
AmlSpaDialect.TESTING;
- const resp = useServerStatistics(dialect, params.current, params.previous);
+ const resp = useTopsServerStatistics();
if (!resp) {
return <Loading />;
@@ -101,79 +103,6 @@ function EventMetrics({
return <ErrorLoadingWithDebug error={resp} />;
}
- // the request test failed?
- const failReponse = resp.type === "fail" ? resp : undefined;
-
- // request test good, but did one of the event metric request failed?
- const oneOftheRequestFailed =
- resp.type === "ok"
- ? resp.body.find(
- (r) =>
- r.current.type === "fail" ||
- (r.previous && r.previous.type === "fail"),
- )
- : undefined;
-
- // collect error on a single variable
- const error = failReponse
- ? failReponse
- : oneOftheRequestFailed
- ? oneOftheRequestFailed.current.type === "fail"
- ? oneOftheRequestFailed.current
- : (oneOftheRequestFailed.previous as TalerExchangeErrorsByMethod<"getAmlKycStatistics">)
- : undefined;
-
- //check
- if (error) {
- switch (error.case) {
- case HttpStatusCode.Forbidden:
- return (
- <Fragment>
- <Attention type="danger" title={i18n.str`Operation denied`}>
- <i18n.Translate>
- This account signature is invalid, contact administrator or
- create a new one.
- </i18n.Translate>
- </Attention>
- <Officer />
- </Fragment>
- );
- case HttpStatusCode.NotFound:
- return (
- <Fragment>
- <Attention type="danger" title={i18n.str`Operation denied`}>
- <i18n.Translate>
- The designated AML account is not known, contact administrator
- or create a new one.
- </i18n.Translate>
- </Attention>
- <Officer />
- </Fragment>
- );
- case HttpStatusCode.Conflict:
- return (
- <Fragment>
- <Attention type="danger" title={i18n.str`Operation denied`}>
- <i18n.Translate>
- The designated AML account is not enabled, contact administrator
- or create a new one.
- </i18n.Translate>
- </Attention>
- <Officer />
- </Fragment>
- );
- default:
- assertUnreachable(error.case);
- }
- }
- const result = resp as OperationOk<
- {
- key: UIHandlerId;
- current: TalerExchangeResultByMethod<"getAmlKycStatistics">;
- previous: TalerExchangeResultByMethod<"getAmlKycStatistics"> | undefined;
- }[]
- >;
-
return (
<div class="px-4 mt-4">
<div class="sm:flex sm:items-center mb-4">
@@ -184,9 +113,9 @@ function EventMetrics({
</div>
</div>
- <SelectTimeframe timeframe={metricType} setTimeframe={setMetricType} />
+ {/* <SelectTimeframe timeframe={metricType} setTimeframe={setMetricType} /> */}
- <div class="w-full flex justify-between">
+ {/* <div class="w-full flex justify-between">
<h1 class="text-base text-gray-900 mt-5">
{i18n.str`Events from ${getDateStringForTimeframe(
params.current.start,
@@ -198,14 +127,12 @@ function EventMetrics({
dateLocale,
)}`}
</h1>
- </div>
+ </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">
- {result.body.map((ev) => {
- const label = labelForEvent(ev.key, i18n, dialect);
- const desc = descriptionForEvent(ev.key, i18n, dialect);
- if (ev.current.type === "fail") return undefined;
- if (ev.previous?.type === "fail") return undefined;
+ {Object.entries(resp.body).map(([name, number]) => {
+ const label = labelForEvent(name, i18n, dialect);
+ const desc = descriptionForEvent(name, i18n, dialect);
return (
<div class="px-4 py-5 sm:p-6">
<dt class="text-base font-normal text-gray-900">
@@ -214,15 +141,13 @@ function EventMetrics({
<div class="text-xs text-gray-500">{desc}</div>
)}
</dt>
- <MetricValueNumber
- current={ev.current.body.counter}
- previous={ev.previous?.body.counter}
- />
+ <MetricValueNumber current={number} previous={undefined} />
</div>
);
})}
</dl>
- <div class="flex justify-end mt-4">
+
+ {/* <div class="flex justify-end mt-4">
<a
href={routeToDownloadStats.url({})}
name="download stats"
@@ -230,65 +155,57 @@ function EventMetrics({
>
<i18n.Translate>Download stats as CSV</i18n.Translate>
</a>
- </div>
+ </div> */}
</div>
);
}
function labelForEvent(
- name: UIHandlerId,
+ name: string,
i18n: InternationalizationAPI,
dialect: AmlSpaDialect,
) {
switch (dialect) {
case AmlSpaDialect.TOPS:
- return labelForEvent_tops(name as TOPS_AmlEventsName, i18n);
+ return labelForEvent_tops(name as any, i18n);
case AmlSpaDialect.GLS:
return labelForEvent_gls(name as GLS_AmlEventsName, i18n);
case AmlSpaDialect.TESTING:
- return labelForEvent_tops(name as TOPS_AmlEventsName, i18n);
+ return labelForEvent_tops(name as any, i18n);
default: {
assertUnreachable(dialect);
}
}
}
function labelForEvent_tops(
- name: TOPS_AmlEventsName,
+ name: keyof ReturnType<typeof EventReporting_TOPS_calculation>,
i18n: InternationalizationAPI,
) {
switch (name) {
- case TOPS_AmlEventsName.ACCOUNT_OPENED:
- return i18n.str`Account opened`;
- case TOPS_AmlEventsName.ACCOUNT_CLOSED:
- return i18n.str`Account closed`;
- case TOPS_AmlEventsName.ACCOUNT_OPENED_HIGH_RISK:
- return i18n.str`High risk account incorporated`;
- case TOPS_AmlEventsName.ACCOUNT_CLOSED_HIGH_RISK:
- return i18n.str`High risk account removed`;
- case TOPS_AmlEventsName.ACCOUNT_OPENED_DOMESTIC_PEP:
- return i18n.str`Account from domestic PEP incorporated`;
- case TOPS_AmlEventsName.ACCOUNT_CLOSED_DOMESTIC_PEP:
- return i18n.str`Account from domestic PEP removed`;
- case TOPS_AmlEventsName.ACCOUNT_OPENED_FOREIGN_PEP:
- return i18n.str`Account from foreign PEP incorporated`;
- case TOPS_AmlEventsName.ACCOUNT_CLOSED_FOREIGN_PEP:
- return i18n.str`Account from foreign PEP removed`;
- case TOPS_AmlEventsName.ACCOUNT_OPENED_HR_COUNTRY:
- return i18n.str`Account from high-risk country incorporated`;
- case TOPS_AmlEventsName.ACCOUNT_CLOSED_HR_COUNTRY:
- return i18n.str`Account from high-risk country removed`;
- case TOPS_AmlEventsName.ACCOUNT_OPENED_INT_ORG_PEP:
- return i18n.str`Account from domestic PEP incorporated`;
- case TOPS_AmlEventsName.ACCOUNT_CLOSED_INT_ORG_PEP:
- return i18n.str`Account from domestic PEP removed`;
- case TOPS_AmlEventsName.ACCOUNT_MROS_REPORTED_SUSPICION_SIMPLE:
- return i18n.str`MROS reported by obligation`;
- case TOPS_AmlEventsName.ACCOUNT_MROS_REPORTED_SUSPICION_SUBSTANTIATED:
- return i18n.str`MROS reported by right`;
- case TOPS_AmlEventsName.ACCOUNT_INVESTIGATION_COMPLETED:
- return i18n.str`Investigations after Art 6 Gwg completed`;
- case TOPS_AmlEventsName.ACCOUNT_INVESTIGATION_STARTED:
- return i18n.str`Investigations after Art 6 Gwg started`;
+ case "accounts_opened":
+ return i18n.str`Accounts open`;
+
+ case "new_gwg_files_last_year":
+ return i18n.str`New files last year`;
+
+ case "gwg_files_closed_last_year":
+ return i18n.str`Closes files last year`;
+
+ case "mros_reports_art9_last_year":
+ return i18n.str`MROS substantaiated`;
+
+ case "mros_reports_art305_last_year":
+ return i18n.str`MROS simple`;
+
+ case "gwg_files_high_risk":
+ return i18n.str`High risk files`;
+
+ case "gwg_files_pep":
+ return i18n.str`PEP files`;
+
+ case "accounts_involed_in_proceedings_last_year":
+ return i18n.str`Under investiation`;
+
default: {
assertUnreachable(name);
}
@@ -310,61 +227,52 @@ function labelForEvent_gls(
}
function descriptionForEvent(
- name: UIHandlerId,
+ name: string,
i18n: InternationalizationAPI,
- dialect: AmlSpaDialect
+ dialect: AmlSpaDialect,
): TranslatedString | undefined {
switch (dialect) {
case AmlSpaDialect.TOPS:
- return descriptionForEvent_tops(name as TOPS_AmlEventsName, i18n);
+ return descriptionForEvent_tops(name as any, i18n);
case AmlSpaDialect.GLS:
return descriptionForEvent_gls(name as GLS_AmlEventsName, i18n);
case AmlSpaDialect.TESTING:
- return descriptionForEvent_tops(name as TOPS_AmlEventsName, i18n);
+ return descriptionForEvent_tops(name as any, i18n);
default: {
assertUnreachable(dialect);
}
}
-
}
function descriptionForEvent_tops(
- name: TOPS_AmlEventsName,
+ name: keyof ReturnType<typeof EventReporting_TOPS_calculation>,
i18n: InternationalizationAPI,
): TranslatedString | undefined {
switch (name) {
- case TOPS_AmlEventsName.ACCOUNT_OPENED:
- return i18n.str`Account opened`;
- case TOPS_AmlEventsName.ACCOUNT_CLOSED:
- return i18n.str`Account closed`;
- case TOPS_AmlEventsName.ACCOUNT_OPENED_HIGH_RISK:
- return i18n.str`High risk account incorporated`;
- case TOPS_AmlEventsName.ACCOUNT_CLOSED_HIGH_RISK:
- return i18n.str`High risk account removed`;
- case TOPS_AmlEventsName.ACCOUNT_OPENED_DOMESTIC_PEP:
- return i18n.str`Account from domestic PEP incorporated`;
- case TOPS_AmlEventsName.ACCOUNT_CLOSED_DOMESTIC_PEP:
- return i18n.str`Account from domestic PEP removed`;
- case TOPS_AmlEventsName.ACCOUNT_OPENED_FOREIGN_PEP:
- return i18n.str`Account from foreign PEP incorporated`;
- case TOPS_AmlEventsName.ACCOUNT_CLOSED_FOREIGN_PEP:
- return i18n.str`Account from foreign PEP removed`;
- case TOPS_AmlEventsName.ACCOUNT_OPENED_HR_COUNTRY:
- return i18n.str`Account from high-risk country incorporated`;
- case TOPS_AmlEventsName.ACCOUNT_CLOSED_HR_COUNTRY:
- return i18n.str`Account from high-risk country removed`;
- case TOPS_AmlEventsName.ACCOUNT_MROS_REPORTED_SUSPICION_SIMPLE:
- return i18n.str`MROS reported by obligation`;
- case TOPS_AmlEventsName.ACCOUNT_MROS_REPORTED_SUSPICION_SUBSTANTIATED:
- return i18n.str`MROS reported by right`;
- case TOPS_AmlEventsName.ACCOUNT_INVESTIGATION_COMPLETED:
- return i18n.str`Investigations after Art 6 Gwg completed`;
- case TOPS_AmlEventsName.ACCOUNT_INVESTIGATION_STARTED:
- return i18n.str`Investigations after Art 6 Gwg started`;
- case TOPS_AmlEventsName.ACCOUNT_OPENED_INT_ORG_PEP:
- return i18n.str`Investigations after Art 6 Gwg opened`;
- case TOPS_AmlEventsName.ACCOUNT_CLOSED_INT_ORG_PEP:
- return i18n.str`Investigations after Art 6 Gwg closed`;
+ case "accounts_opened":
+ return i18n.str`Number of accounts that are opened`;
+
+ case "new_gwg_files_last_year":
+ return i18n.str`Number of new GwG files in the last year`;
+
+ case "gwg_files_closed_last_year":
+ return i18n.str`Number of GwG files closed in the last year`;
+
+ case "mros_reports_art9_last_year":
+ return i18n.str`Number of MROS reports based on Art 9 Abs. 1 GwG (per year)`;
+
+ case "mros_reports_art305_last_year":
+ return i18n.str`Number of MROS reports based on Art 305ter Abs. 2 StGB (per year)`;
+
+ case "gwg_files_high_risk":
+ return i18n.str`Number of GwG files of high-risk customers`;
+
+ case "gwg_files_pep":
+ return i18n.str`Number of GwG files managed with “increased risk” due to PEP status`;
+
+ case "accounts_involed_in_proceedings_last_year":
+ return i18n.str`Number of customers involved in proceedings for which Art 6 GwG did apply`;
+
default: {
assertUnreachable(name);
}
@@ -386,8 +294,6 @@ function descriptionForEvent_gls(
}
}
-
-
function MetricValueNumber({
current,
previous,
@@ -418,10 +324,11 @@ function MetricValueNumber({
</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 ? 0 : previous}
- </small>
+ {previous === undefined ? undefined : (
+ <small class="ml-2 text-sm font-medium text-gray-500">
+ <i18n.Translate>previous</i18n.Translate> {previous}
+ </small>
+ )}
</div>
{!!rate && (
<span
diff --git a/packages/aml-backoffice-ui/src/pages/decision/Events.tsx b/packages/aml-backoffice-ui/src/pages/decision/Events.tsx
@@ -1,8 +1,8 @@
import {
- AML_EVENTS_INFO,
AmlDecision,
AmlSpaDialect,
assertUnreachable,
+ EventsDerivation_TOPS,
GLS_AmlEventsName,
HttpStatusCode,
MeasureInformation,
@@ -206,38 +206,56 @@ function labelForEvent_tops(
i18n: InternationalizationAPI,
) {
switch (event) {
- case TOPS_AmlEventsName.ACCOUNT_OPENED:
+ case TOPS_AmlEventsName.INCR_ACCOUNT_OPEN:
+ case TOPS_AmlEventsName.DECR_ACCOUNT_OPEN:
+ case TOPS_AmlEventsName.INCR_HIGH_RISK_CUSTOMER:
+ case TOPS_AmlEventsName.DECR_HIGH_RISK_CUSTOMER:
+ case TOPS_AmlEventsName.INCR_HIGH_RISK_COUNTRY:
+ case TOPS_AmlEventsName.DECR_HIGH_RISK_COUNTRY:
+ case TOPS_AmlEventsName.INCR_PEP:
+ case TOPS_AmlEventsName.DECR_PEP:
+ case TOPS_AmlEventsName.INCR_PEP_FOREIGN:
+ case TOPS_AmlEventsName.DECR_PEP_FOREIGN:
+ case TOPS_AmlEventsName.INCR_PEP_DOMESTIC:
+ case TOPS_AmlEventsName.DECR_PEP_DOMESTIC:
+ case TOPS_AmlEventsName.INCR_PEP_INTERNATIONAL_ORGANIZATION:
+ case TOPS_AmlEventsName.DECR_PEP_INTERNATIONAL_ORGANIZATION:
+ case TOPS_AmlEventsName.MROS_REPORTED_SUSPICION_SIMPLE:
+ case TOPS_AmlEventsName.MROS_REPORTED_SUSPICION_SUBSTANTIATED:
+ case TOPS_AmlEventsName.INCR_INVESTIGATION_CONCLUDED:
+ case TOPS_AmlEventsName.DECR_INVESTIGATION_CONCLUDED:
+ // case TOPS_AmlEventsName.ACCOUNT_OPENED:
return i18n.str`Account opened`;
- case TOPS_AmlEventsName.ACCOUNT_CLOSED:
- return i18n.str`Account closed`;
- case TOPS_AmlEventsName.ACCOUNT_OPENED_HIGH_RISK:
- return i18n.str`High risk account incorporated`;
- case TOPS_AmlEventsName.ACCOUNT_CLOSED_HIGH_RISK:
- return i18n.str`High risk account removed`;
- case TOPS_AmlEventsName.ACCOUNT_OPENED_DOMESTIC_PEP:
- return i18n.str`Account from dometic PEP incorporated`;
- case TOPS_AmlEventsName.ACCOUNT_CLOSED_DOMESTIC_PEP:
- return i18n.str`Account from dometic PEP removed`;
- case TOPS_AmlEventsName.ACCOUNT_OPENED_FOREIGN_PEP:
- return i18n.str`Account from foreign PEP incorporated`;
- case TOPS_AmlEventsName.ACCOUNT_CLOSED_FOREIGN_PEP:
- return i18n.str`Account from foreign PEP removed`;
- case TOPS_AmlEventsName.ACCOUNT_OPENED_HR_COUNTRY:
- return i18n.str`Account from high-risk country incorporated`;
- case TOPS_AmlEventsName.ACCOUNT_CLOSED_HR_COUNTRY:
- return i18n.str`Account from high-risk country removed`;
- case TOPS_AmlEventsName.ACCOUNT_MROS_REPORTED_SUSPICION_SIMPLE:
- return i18n.str`MROS reported by obligation`;
- case TOPS_AmlEventsName.ACCOUNT_MROS_REPORTED_SUSPICION_SUBSTANTIATED:
- return i18n.str`MROS reported by right`;
- case TOPS_AmlEventsName.ACCOUNT_INVESTIGATION_COMPLETED:
- return i18n.str`Investigations after Art 6 Gwg completed`;
- case TOPS_AmlEventsName.ACCOUNT_INVESTIGATION_STARTED:
- return i18n.str`Investigations after Art 6 Gwg started`;
- case TOPS_AmlEventsName.ACCOUNT_OPENED_INT_ORG_PEP:
- return i18n.str`Account from international organization PEP incorporated`;
- case TOPS_AmlEventsName.ACCOUNT_CLOSED_INT_ORG_PEP:
- return i18n.str`Account from international organization PEP removed`;
+ // case TOPS_AmlEventsName.ACCOUNT_CLOSED:
+ // return i18n.str`Account closed`;
+ // case TOPS_AmlEventsName.ACCOUNT_OPENED_HIGH_RISK:
+ // return i18n.str`High risk account incorporated`;
+ // case TOPS_AmlEventsName.ACCOUNT_CLOSED_HIGH_RISK:
+ // return i18n.str`High risk account removed`;
+ // case TOPS_AmlEventsName.ACCOUNT_OPENED_DOMESTIC_PEP:
+ // return i18n.str`Account from dometic PEP incorporated`;
+ // case TOPS_AmlEventsName.ACCOUNT_CLOSED_DOMESTIC_PEP:
+ // return i18n.str`Account from dometic PEP removed`;
+ // case TOPS_AmlEventsName.ACCOUNT_OPENED_FOREIGN_PEP:
+ // return i18n.str`Account from foreign PEP incorporated`;
+ // case TOPS_AmlEventsName.ACCOUNT_CLOSED_FOREIGN_PEP:
+ // return i18n.str`Account from foreign PEP removed`;
+ // case TOPS_AmlEventsName.ACCOUNT_OPENED_HR_COUNTRY:
+ // return i18n.str`Account from high-risk country incorporated`;
+ // case TOPS_AmlEventsName.ACCOUNT_CLOSED_HR_COUNTRY:
+ // return i18n.str`Account from high-risk country removed`;
+ // case TOPS_AmlEventsName.ACCOUNT_MROS_REPORTED_SUSPICION_SIMPLE:
+ // return i18n.str`MROS reported by obligation`;
+ // case TOPS_AmlEventsName.ACCOUNT_MROS_REPORTED_SUSPICION_SUBSTANTIATED:
+ // return i18n.str`MROS reported by right`;
+ // case TOPS_AmlEventsName.ACCOUNT_INVESTIGATION_COMPLETED:
+ // return i18n.str`Investigations after Art 6 Gwg completed`;
+ // case TOPS_AmlEventsName.ACCOUNT_INVESTIGATION_STARTED:
+ // return i18n.str`Investigations after Art 6 Gwg started`;
+ // case TOPS_AmlEventsName.ACCOUNT_OPENED_INT_ORG_PEP:
+ // return i18n.str`Account from international organization PEP incorporated`;
+ // case TOPS_AmlEventsName.ACCOUNT_CLOSED_INT_ORG_PEP:
+ // return i18n.str`Account from international organization PEP removed`;
default: {
assertUnreachable(event);
}
@@ -266,7 +284,7 @@ function calculateEventsBasedOnState(
dialect: AmlSpaDialect,
): Events {
const init: Events = { triggered: [], rest: [] };
- return Object.entries(AML_EVENTS_INFO).reduce((prev, [name, info]) => {
+ return Object.entries(EventsDerivation_TOPS).reduce((prev, [name, info]) => {
if (
info.shouldBeTriggered(
currentState?.limits?.rules,
diff --git a/packages/aml-backoffice-ui/src/pages/decision/Properties.tsx b/packages/aml-backoffice-ui/src/pages/decision/Properties.tsx
@@ -6,12 +6,11 @@ import {
GLS_AML_PROPERTIES,
HttpStatusCode,
LegitimizationRuleSet,
- PropertiesMapInfo,
+ PropertiesDerivation_TOPS,
+ PropertiesDerivationFunctionByPropertyName,
TalerAmlProperties,
TalerError,
- TalerFormAttributes,
- TOPS_AccountProperties,
- TOPS_AML_PROPERTIES,
+ TOPS_AccountProperties
} from "@gnu-taler/taler-util";
import {
FormDesign,
@@ -26,13 +25,13 @@ import {
useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { h, VNode } from "preact";
+import { ErrorLoadingWithDebug } from "../../components/ErrorLoadingWithDebug.js";
import {
DecisionRequest,
useCurrentDecisionRequest,
} from "../../hooks/decision-request.js";
import { useAccountActiveDecision } from "../../hooks/decisions.js";
import { usePreferences } from "../../hooks/preferences.js";
-import { ErrorLoadingWithDebug } from "../../components/ErrorLoadingWithDebug.js";
import { DEFAULT_LIMITS_WHEN_NEW_ACCOUNT } from "./Rules.js";
/**
@@ -304,14 +303,14 @@ function calculatePropertiesBasedOnState(
function mergeProperties<T extends string>(
iv: Result,
list: readonly T[],
- info: PropertiesMapInfo<T>,
+ info: PropertiesDerivationFunctionByPropertyName<T>,
): Result {
return list.reduce((result, prop) => {
result[prop] = info[prop].deriveProperty(
FORM_ID,
+ newNewAttributes,
currentLimits,
state,
- newNewAttributes,
);
return result;
}, iv);
@@ -322,7 +321,7 @@ function calculatePropertiesBasedOnState(
return mergeProperties(
initial,
TOPS_AccountProperties,
- TOPS_AML_PROPERTIES,
+ PropertiesDerivation_TOPS,
);
}
case AmlSpaDialect.GLS: {
@@ -336,7 +335,7 @@ function calculatePropertiesBasedOnState(
return mergeProperties(
initial,
TOPS_AccountProperties,
- TOPS_AML_PROPERTIES,
+ PropertiesDerivation_TOPS,
);
}
}
diff --git a/packages/aml-backoffice-ui/src/pages/decision/Rules.tsx b/packages/aml-backoffice-ui/src/pages/decision/Rules.tsx
@@ -450,6 +450,22 @@ function UpdateRulesForm({
>
<i18n.Translate>Premium</i18n.Translate>
</button>
+ <button
+ onClick={() => {
+ updateRequestField("rules", E_COMMERCE(config.currency, isWallet));
+ }}
+ 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 disabled:bg-gray-600"
+ >
+ <i18n.Translate>E-commerce</i18n.Translate>
+ </button>
+ <button
+ onClick={() => {
+ updateRequestField("rules", POINT_OF_SALE(config.currency, isWallet));
+ }}
+ 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 disabled:bg-gray-600"
+ >
+ <i18n.Translate>Point-of-sale</i18n.Translate>
+ </button>
<h2 class="mt-4 mb-2">
<i18n.Translate>On expiration behavior</i18n.Translate>
</h2>
@@ -878,3 +894,123 @@ const FREEZE_PLAN: TemplateRulesFunction = (
threshold: Amounts.stringify(Amounts.zeroOfCurrency(currency)),
timeframe: Duration.toTalerProtocolDuration(Duration.getForever()),
}));
+
+const POINT_OF_SALE: TemplateRulesFunction = (currency, isWallet) =>
+ [
+ {
+ display_priority: 1,
+ measures: ["VERBOTEN"],
+ operation_type: LimitOperationType.withdraw,
+ threshold: Amounts.stringify({
+ currency,
+ fraction: 0,
+ value: 0,
+ }),
+ timeframe: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ months: 1 }),
+ ),
+ },
+ {
+ display_priority: 1,
+ measures: ["VERBOTEN"],
+ operation_type: LimitOperationType.merge,
+ threshold: Amounts.stringify({
+ currency,
+ fraction: 0,
+ value: 25 * 1000,
+ }),
+ timeframe: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ months: 1 }),
+ ),
+ },
+ {
+ display_priority: 1,
+ measures: ["preserve-investigate"],
+ operation_type: LimitOperationType.deposit,
+ threshold: Amounts.stringify({
+ currency,
+ fraction: 0,
+ value: 25 * 1000,
+ }),
+ timeframe: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ months: 1 }),
+ ),
+ },
+ {
+ display_priority: 1,
+ measures: ["preserve-investigate"],
+ operation_type: LimitOperationType.aggregate,
+ threshold: Amounts.stringify({
+ currency,
+ fraction: 0,
+ value: 25 * 1000,
+ }),
+ timeframe: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ months: 1 }),
+ ),
+ },
+ ].filter((r) => {
+ return isWallet
+ ? WALLET_RULES.includes(r.operation_type)
+ : BANK_RULES.includes(r.operation_type);
+ });
+
+ const E_COMMERCE: TemplateRulesFunction = (currency, isWallet) =>
+ [
+ {
+ display_priority: 1,
+ measures: ["VERBOTEN"],
+ operation_type: LimitOperationType.withdraw,
+ threshold: Amounts.stringify({
+ currency,
+ fraction: 0,
+ value: 0,
+ }),
+ timeframe: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ months: 1 }),
+ ),
+ },
+ {
+ display_priority: 1,
+ measures: ["VERBOTEN"],
+ operation_type: LimitOperationType.merge,
+ threshold: Amounts.stringify({
+ currency,
+ fraction: 0,
+ value: 0,
+ }),
+ timeframe: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ months: 1 }),
+ ),
+ },
+ {
+ display_priority: 1,
+ measures: ["preserve-investigate"],
+ operation_type: LimitOperationType.deposit,
+ threshold: Amounts.stringify({
+ currency,
+ fraction: 0,
+ value: 25 * 1000,
+ }),
+ timeframe: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ months: 1 }),
+ ),
+ },
+ {
+ display_priority: 1,
+ measures: ["preserve-investigate"],
+ operation_type: LimitOperationType.aggregate,
+ threshold: Amounts.stringify({
+ currency,
+ fraction: 0,
+ value: 25 * 1000,
+ }),
+ timeframe: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ months: 1 }),
+ ),
+ },
+ ].filter((r) => {
+ return isWallet
+ ? WALLET_RULES.includes(r.operation_type)
+ : BANK_RULES.includes(r.operation_type);
+ });
+\ No newline at end of file
diff --git a/packages/aml-backoffice-ui/src/pages/decision/Summary.tsx b/packages/aml-backoffice-ui/src/pages/decision/Summary.tsx
@@ -1,17 +1,14 @@
import {
AbsoluteTime,
- AML_EVENTS_INFO,
AmlDecisionRequest,
assertUnreachable,
HttpStatusCode,
- opEmptySuccess,
opFixedSuccess,
parsePaytoUri,
PaytoString,
stringifyPaytoUri,
- succeedOrThrow,
TalerError,
- TOPS_AmlEventsName,
+ TOPS_AmlEventsName
} from "@gnu-taler/taler-util";
import {
Attention,
@@ -22,14 +19,14 @@ import {
useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
import {
DECISION_REQUEST_EMPTY,
useCurrentDecisionRequest,
} from "../../hooks/decision-request.js";
import { useOfficer } from "../../hooks/officer.js";
import {
- useServerMeasures,
- useServerStatistics,
+ useServerMeasures
} from "../../hooks/server-info.js";
import {
computeAvailableMesaures,
@@ -46,7 +43,6 @@ import {
isRulesCompleted,
WizardSteps,
} from "./AmlDecisionRequestWizard.js";
-import { useState } from "preact/hooks";
/**
* Mark for further investigation and explain decision
@@ -96,10 +92,10 @@ export function Summary({
const MROS_REPORT_COMPLETED = !decision.triggering_events
? false
: decision.triggering_events.includes(
- TOPS_AmlEventsName.ACCOUNT_MROS_REPORTED_SUSPICION_SIMPLE,
+ TOPS_AmlEventsName.MROS_REPORTED_SUSPICION_SIMPLE,
) ||
decision.triggering_events.includes(
- TOPS_AmlEventsName.ACCOUNT_MROS_REPORTED_SUSPICION_SUBSTANTIATED,
+ TOPS_AmlEventsName.MROS_REPORTED_SUSPICION_SUBSTANTIATED,
);
const CANT_SUBMIT =
diff --git a/packages/taler-util/src/aml/aml-events.ts b/packages/taler-util/src/aml/aml-events.ts
@@ -1,178 +0,0 @@
-import { Amounts } from "../amounts.js";
-import { LimitOperationType } from "../types-taler-exchange.js";
-import {
- AccountProperties,
- KycRule,
- LegitimizationRuleSet,
-} from "../types-taler-kyc-aml.js";
-
-/**
- * List of events triggered by TOPS
- */
-export enum TOPS_AmlEventsName {
- ACCOUNT_OPENED = "ACCOUNT_OPENED",
- ACCOUNT_OPENED_HIGH_RISK = "ACCOUNT_OPENED_HIGH_RISK",
- ACCOUNT_OPENED_DOMESTIC_PEP = "ACCOUNT_OPENED_DOMESTIC_PEP",
- ACCOUNT_OPENED_FOREIGN_PEP = "ACCOUNT_OPENED_FOREIGN_PEP",
- ACCOUNT_OPENED_INT_ORG_PEP = "ACCOUNT_OPENED_INT_ORG_PEP",
- ACCOUNT_OPENED_HR_COUNTRY = "ACCOUNT_OPENED_HR_COUNTRY",
-
- ACCOUNT_CLOSED = "ACCOUNT_CLOSED",
- ACCOUNT_CLOSED_HIGH_RISK = "ACCOUNT_CLOSED_HIGH_RISK",
- ACCOUNT_CLOSED_DOMESTIC_PEP = "ACCOUNT_CLOSED_DOMESTIC_PEP",
- ACCOUNT_CLOSED_FOREIGN_PEP = "ACCOUNT_CLOSED_FOREIGN_PEP",
- ACCOUNT_CLOSED_INT_ORG_PEP = "ACCOUNT_CLOSED_INT_ORG_PEP",
- ACCOUNT_CLOSED_HR_COUNTRY = "ACCOUNT_CLOSED_HR_COUNTRY",
-
- ACCOUNT_MROS_REPORTED_SUSPICION_SIMPLE = "ACCOUNT_MROS_REPORTED_SUSPICION_SIMPLE",
- ACCOUNT_MROS_REPORTED_SUSPICION_SUBSTANTIATED = "ACCOUNT_MROS_REPORTED_SUSPICION_SUBSTANTIATED",
- ACCOUNT_INVESTIGATION_STARTED = "ACCOUNT_INVESTIGATION_STARTED",
- ACCOUNT_INVESTIGATION_COMPLETED = "ACCOUNT_INVESTIGATION_COMPLETED",
-}
-
-/**
- * List of events triggered by GLS
- */
-export enum GLS_AmlEventsName {
- ACCOUNT_OPENED = "ACCOUNT_OPENED",
- ACCOUNT_CLOSED = "ACCOUNT_CLOSED",
-}
-
-export type EventMapInfo<T> = {
- [name in keyof T]: {
- /**
- * Based on the current properties and next properties,
- * the current account limits and new attributes of the account
- * calculate if the event should be triggered.
- *
- * return false if there is no enough information to decide.
- *
- * @param prevLimits current active decision limits, undefined if this account has no active decision yet
- * @param nextLimits limits of the decision to be made, undefined if not yet decided
- * @param prevState current active decision properties, undefined if this account has no active decision yet
- * @param nextState new properties of the account defined by the decision, undefined if not yet decided
- * @param newAttributes new information added by this decision
- * @returns
- */
- shouldBeTriggered: (
- prevLimits: KycRule[] | undefined,
- nextLimits: KycRule[] | undefined,
- prevState: AccountProperties | undefined,
- nextState: AccountProperties | undefined,
- newAttributes: Record<string, unknown>,
- ) => boolean;
- };
-};
-
-function isAllowToMakeDeposits(limits: KycRule[]) {
- const depositLimits = limits.filter(
- (r) => r.operation_type === LimitOperationType.deposit,
- );
- // no deposit limits
- if (!depositLimits.length) return true;
- const zero = depositLimits.find((d) => Amounts.isZero(d.threshold));
- // there is a rule that prohibit deposit
- if (zero) return false;
- // the cusomter can at least make some deposits
- return true;
-}
-
-export const AML_EVENTS_INFO: EventMapInfo<typeof TOPS_AmlEventsName> = {
- ACCOUNT_OPENED: {
- shouldBeTriggered(pL, nL, pS, nS, attr) {
- //FIXME: implement the correct rule, this is for testing
- if (!nL) return false;
- return pL === undefined
- ? !isAllowToMakeDeposits(nL)
- : isAllowToMakeDeposits(pL) && !isAllowToMakeDeposits(nL);
- },
- },
- ACCOUNT_CLOSED: {
- shouldBeTriggered(pL, nL, pS, nS, attr) {
- return false;
- },
- },
- ACCOUNT_CLOSED_INT_ORG_PEP: {
- shouldBeTriggered(pL, nL, pS, nS, attr) {
- return false;
- },
- },
- ACCOUNT_OPENED_HIGH_RISK: {
- shouldBeTriggered(pL, nL, pS, nS, attr) {
- return false;
- },
- },
- ACCOUNT_CLOSED_HIGH_RISK: {
- shouldBeTriggered(pL, nL, pS, nS, attr) {
- return false;
- },
- },
- ACCOUNT_OPENED_DOMESTIC_PEP: {
- shouldBeTriggered(pL, nL, pS, nS, attr) {
- return false;
- },
- },
- ACCOUNT_CLOSED_DOMESTIC_PEP: {
- shouldBeTriggered(pL, nL, pS, nS, attr) {
- return false;
- },
- },
- ACCOUNT_OPENED_FOREIGN_PEP: {
- shouldBeTriggered(pL, nL, pS, nS, attr) {
- return false;
- },
- },
- ACCOUNT_CLOSED_FOREIGN_PEP: {
- shouldBeTriggered(pL, nL, pS, nS, attr) {
- return false;
- },
- },
- ACCOUNT_OPENED_HR_COUNTRY: {
- shouldBeTriggered(pL, nL, pS, nS, attr) {
- return false;
- },
- },
- ACCOUNT_CLOSED_HR_COUNTRY: {
- shouldBeTriggered(pL, nL, pS, nS, attr) {
- return false;
- },
- },
- ACCOUNT_INVESTIGATION_COMPLETED: {
- shouldBeTriggered(pL, nL, pS, nS, attr) {
- return false;
- },
- },
- ACCOUNT_INVESTIGATION_STARTED: {
- shouldBeTriggered(pL, nL, pS, nS, attr) {
- return false;
- },
- },
- ACCOUNT_MROS_REPORTED_SUSPICION_SIMPLE: {
- shouldBeTriggered(pL, nL, pS, nS, attr) {
- return false;
- },
- },
- ACCOUNT_MROS_REPORTED_SUSPICION_SUBSTANTIATED: {
- shouldBeTriggered(pL, nL, pS, nS, attr) {
- return false;
- },
- },
- ACCOUNT_OPENED_INT_ORG_PEP: {
- shouldBeTriggered(pL, nL, pS, nS, attr) {
- return false;
- },
- },
-};
-
-export const GLS_AML_EVENTS: EventMapInfo<typeof GLS_AmlEventsName> = {
- ACCOUNT_OPENED: {
- shouldBeTriggered(pL, nL, pS, nS, attr) {
- return false;
- },
- },
- ACCOUNT_CLOSED: {
- shouldBeTriggered(pL, nL, pS, nS, attr) {
- return false;
- },
- },
-};
diff --git a/packages/taler-util/src/aml/aml-properties.ts b/packages/taler-util/src/aml/aml-properties.ts
@@ -1,161 +0,0 @@
-import { TalerAmlProperties } from "../taler-account-properties.js";
-import { TalerFormAttributes } from "../taler-form-attributes.js";
-import {
- AccountProperties,
- LegitimizationRuleSet,
-} from "../types-taler-kyc-aml.js";
-
-/**
- * List of account properties required by TOPS
- */
-
-export const TOPS_AccountProperties = [
- TalerAmlProperties.FILE_NOTE,
- TalerAmlProperties.CUSTOMER_LABEL,
- TalerAmlProperties.ACCOUNT_OPEN,
- TalerAmlProperties.PEP_DOMESTIC,
- TalerAmlProperties.PEP_FOREIGN,
- TalerAmlProperties.PEP_INTERNATIONAL_ORGANIZATION,
- TalerAmlProperties.HIGH_RISK_CUSTOMER,
- TalerAmlProperties.HIGH_RISK_COUNTRY,
- TalerAmlProperties.ACCOUNT_IDLE,
- TalerAmlProperties.INVESTIGATION_TRIGGER,
- TalerAmlProperties.INVESTIGATION_STATE,
- TalerAmlProperties.SANCTION_LIST_BEST_MATCH,
- TalerAmlProperties.SANCTION_LIST_RATING,
- TalerAmlProperties.SANCTION_LIST_CONFIDENCE,
- TalerAmlProperties.SANCTION_LIST_SUPPRESS,
-] as const;
-
-/**
- * List of account properties required by GLS
- */
-export const GLS_AccountProperties = [TalerAmlProperties.FILE_NOTE] as const;
-
-export type PropertiesMapInfo<T extends string> = {
- [name in T]: {
- /**
- * Based on all the current properties, the current account limits and
- * new attributes of the account calculate if the property should
- * change. The value "undefined" means no change.
- *
- * @param limits
- * @param state
- * @param newAttributes
- * @returns
- */
- deriveProperty: (
- formId: string,
- limits: LegitimizationRuleSet,
- state: AccountProperties,
- newAttributes: Record<keyof typeof TalerFormAttributes, unknown>,
- ) => string | boolean | undefined;
- };
-};
-
-export const TOPS_AML_PROPERTIES: PropertiesMapInfo<
- (typeof TOPS_AccountProperties)[number]
-> = {
- ACCOUNT_OPEN: {
- deriveProperty(formId, limits, state, attributes) {
- return undefined;
- },
- },
- PEP_DOMESTIC: {
- deriveProperty(formId, limits, state, attributes) {
- if (formId === "vqf_902_4") {
- return !!attributes[TalerFormAttributes.PEP_DOMESTIC];
- }
- return undefined;
- },
- },
- PEP_FOREIGN: {
- deriveProperty(formId, limits, state, attributes) {
- if (formId === "vqf_902_4") {
- return !!attributes[TalerFormAttributes.PEP_FOREIGN];
- }
- return undefined;
- },
- },
- PEP_INTERNATIONAL_ORGANIZATION: {
- deriveProperty(formId, limits, state, attributes) {
- if (formId === "vqf_902_4") {
- return !!attributes[TalerFormAttributes.PEP_INTERNATIONAL_ORGANIZATION];
- }
- return undefined;
- },
- },
- HIGH_RISK_CUSTOMER: {
- deriveProperty(formId, limits, state, attributes) {
- return (
- attributes[TalerFormAttributes.RISK_CLASSIFICATION_LEVEL] ===
- "HIGH_RISK"
- );
- },
- },
- HIGH_RISK_COUNTRY: {
- deriveProperty(formId, limits, state, attributes) {
- return (
- attributes[TalerFormAttributes.COUNTRY_RISK_NATIONALITY_LEVEL] ===
- "HIGH"
- );
- },
- },
- ACCOUNT_IDLE: {
- deriveProperty(formId, limits, state, attributes) {
- return undefined;
- },
- },
- CUSTOMER_LABEL: {
- deriveProperty(formId, limits, state, attributes) {
- return undefined;
- },
- },
- FILE_NOTE: {
- deriveProperty(formId, limits, state, attributes) {
- return undefined;
- },
- },
- INVESTIGATION_STATE: {
- deriveProperty(formId, limits, state, attributes) {
- // https://bugs.gnunet.org/view.php?id=9677
-
- return undefined;
- },
- },
- INVESTIGATION_TRIGGER: {
- deriveProperty(formId, limits, state, attributes) {
- return undefined;
- },
- },
- SANCTION_LIST_BEST_MATCH: {
- deriveProperty(formId, limits, state, attributes) {
- return undefined;
- },
- },
- SANCTION_LIST_CONFIDENCE: {
- deriveProperty(formId, limits, state, attributes) {
- return undefined;
- },
- },
- SANCTION_LIST_RATING: {
- deriveProperty(formId, limits, state, attributes) {
- return undefined;
- },
- },
- SANCTION_LIST_SUPPRESS: {
- deriveProperty(formId, limits, state, attributes) {
- return undefined;
- },
- },
-};
-
-export const GLS_AML_PROPERTIES: PropertiesMapInfo<
- (typeof GLS_AccountProperties)[number]
-> = {
- FILE_NOTE: {
- deriveProperty(formId, limits, state, attributes) {
- return undefined;
- },
- },
-};
diff --git a/packages/taler-util/src/aml/events.ts b/packages/taler-util/src/aml/events.ts
@@ -0,0 +1,196 @@
+import { Amounts } from "../amounts.js";
+import { LimitOperationType } from "../types-taler-exchange.js";
+import {
+ AccountProperties,
+ KycRule,
+ LegitimizationRuleSet,
+} from "../types-taler-kyc-aml.js";
+
+/**
+ * List of events triggered by TOPS
+ */
+export enum TOPS_AmlEventsName {
+ INCR_ACCOUNT_OPEN = "INCR_ACCOUNT_OPEN",
+ DECR_ACCOUNT_OPEN = "DECR_ACCOUNT_OPEN",
+ INCR_HIGH_RISK_CUSTOMER = "INCR_HIGH_RISK_CUSTOMER",
+ DECR_HIGH_RISK_CUSTOMER = "DECR_HIGH_RISK_CUSTOMER",
+ INCR_HIGH_RISK_COUNTRY = "INCR_HIGH_RISK_COUNTRY",
+ DECR_HIGH_RISK_COUNTRY = "DECR_HIGH_RISK_COUNTRY",
+ INCR_PEP = "INCR_PEP",
+ DECR_PEP = "DECR_PEP",
+ INCR_PEP_FOREIGN = "INCR_PEP_FOREIGN",
+ DECR_PEP_FOREIGN = "DECR_PEP_FOREIGN",
+ INCR_PEP_DOMESTIC = "INCR_PEP_DOMESTIC",
+ DECR_PEP_DOMESTIC = "DECR_PEP_DOMESTIC",
+ INCR_PEP_INTERNATIONAL_ORGANIZATION = "INCR_PEP_INTERNATIONAL_ORGANIZATION",
+ DECR_PEP_INTERNATIONAL_ORGANIZATION = "DECR_PEP_INTERNATIONAL_ORGANIZATION",
+ MROS_REPORTED_SUSPICION_SIMPLE = "MROS_REPORTED_SUSPICION_SIMPLE",
+ MROS_REPORTED_SUSPICION_SUBSTANTIATED = "MROS_REPORTED_SUSPICION_SUBSTANTIATED",
+ INCR_INVESTIGATION_CONCLUDED = "INCR_INVESTIGATION_CONCLUDED",
+ DECR_INVESTIGATION_CONCLUDED = "DECR_INVESTIGATION_CONCLUDED",
+}
+
+/**
+ * List of events triggered by GLS
+ */
+export enum GLS_AmlEventsName {
+ ACCOUNT_OPENED = "ACCOUNT_OPENED",
+ ACCOUNT_CLOSED = "ACCOUNT_CLOSED",
+}
+
+export type EventMapInfo<T> = {
+ [name in keyof T]: {
+ /**
+ * Based on the current properties and next properties,
+ * the current account limits and new attributes of the account
+ * calculate if the event should be triggered.
+ *
+ * return false if there is no enough information to decide.
+ *
+ * @param prevLimits current active decision limits, undefined if this account has no active decision yet
+ * @param nextLimits limits of the decision to be made, undefined if not yet decided
+ * @param prevState current active decision properties, undefined if this account has no active decision yet
+ * @param nextState new properties of the account defined by the decision, undefined if not yet decided
+ * @param newAttributes new information added by this decision
+ * @returns
+ */
+ shouldBeTriggered: (
+ prevLimits: KycRule[] | undefined,
+ nextLimits: KycRule[] | undefined,
+ prevState: AccountProperties | undefined,
+ nextState: AccountProperties | undefined,
+ newAttributes: Record<string, unknown>,
+ ) => boolean;
+ };
+};
+
+function isAllowToMakeDeposits(limits: KycRule[]) {
+ const depositLimits = limits.filter(
+ (r) => r.operation_type === LimitOperationType.deposit,
+ );
+ // no deposit limits
+ if (!depositLimits.length) return true;
+ const zero = depositLimits.find((d) => Amounts.isZero(d.threshold));
+ // there is a rule that prohibit deposit
+ if (zero) return false;
+ // the cusomter can at least make some deposits
+ return true;
+}
+
+/**
+ * Calculate if an event should be triggered for TOPS decisions
+ */
+export const EventsDerivation_TOPS: EventMapInfo<typeof TOPS_AmlEventsName> = {
+ // ACCOUNT_OPENED: {
+ // shouldBeTriggered(pL, nL, pS, nS, attr) {
+ // //FIXME: implement the correct rule, this is for testing
+ // if (!nL) return false;
+ // return pL === undefined
+ // ? !isAllowToMakeDeposits(nL)
+ // : isAllowToMakeDeposits(pL) && !isAllowToMakeDeposits(nL);
+ // },
+ // },
+ INCR_ACCOUNT_OPEN: {
+ shouldBeTriggered(pL, nL, pS, nS, attr) {
+ return false;
+ },
+ },
+ DECR_ACCOUNT_OPEN: {
+ shouldBeTriggered(pL, nL, pS, nS, attr) {
+ return false;
+ },
+ },
+ INCR_HIGH_RISK_CUSTOMER: {
+ shouldBeTriggered(pL, nL, pS, nS, attr) {
+ return false;
+ },
+ },
+ DECR_HIGH_RISK_CUSTOMER: {
+ shouldBeTriggered(pL, nL, pS, nS, attr) {
+ return false;
+ },
+ },
+ INCR_HIGH_RISK_COUNTRY: {
+ shouldBeTriggered(pL, nL, pS, nS, attr) {
+ return false;
+ },
+ },
+ DECR_HIGH_RISK_COUNTRY: {
+ shouldBeTriggered(pL, nL, pS, nS, attr) {
+ return false;
+ },
+ },
+ INCR_PEP: {
+ shouldBeTriggered(pL, nL, pS, nS, attr) {
+ return false;
+ },
+ },
+ DECR_PEP: {
+ shouldBeTriggered(pL, nL, pS, nS, attr) {
+ return false;
+ },
+ },
+ INCR_PEP_FOREIGN: {
+ shouldBeTriggered(pL, nL, pS, nS, attr) {
+ return false;
+ },
+ },
+ DECR_PEP_FOREIGN: {
+ shouldBeTriggered(pL, nL, pS, nS, attr) {
+ return false;
+ },
+ },
+ INCR_PEP_DOMESTIC: {
+ shouldBeTriggered(pL, nL, pS, nS, attr) {
+ return false;
+ },
+ },
+ DECR_PEP_DOMESTIC: {
+ shouldBeTriggered(pL, nL, pS, nS, attr) {
+ return false;
+ },
+ },
+ INCR_PEP_INTERNATIONAL_ORGANIZATION: {
+ shouldBeTriggered(pL, nL, pS, nS, attr) {
+ return false;
+ },
+ },
+ DECR_PEP_INTERNATIONAL_ORGANIZATION: {
+ shouldBeTriggered(pL, nL, pS, nS, attr) {
+ return false;
+ },
+ },
+ MROS_REPORTED_SUSPICION_SIMPLE: {
+ shouldBeTriggered(pL, nL, pS, nS, attr) {
+ return false;
+ },
+ },
+ MROS_REPORTED_SUSPICION_SUBSTANTIATED: {
+ shouldBeTriggered(pL, nL, pS, nS, attr) {
+ return false;
+ },
+ },
+ INCR_INVESTIGATION_CONCLUDED: {
+ shouldBeTriggered(pL, nL, pS, nS, attr) {
+ return false;
+ },
+ },
+ DECR_INVESTIGATION_CONCLUDED: {
+ shouldBeTriggered(pL, nL, pS, nS, attr) {
+ return false;
+ },
+ },
+};
+
+export const GLS_AML_EVENTS: EventMapInfo<typeof GLS_AmlEventsName> = {
+ ACCOUNT_OPENED: {
+ shouldBeTriggered(pL, nL, pS, nS, attr) {
+ return false;
+ },
+ },
+ ACCOUNT_CLOSED: {
+ shouldBeTriggered(pL, nL, pS, nS, attr) {
+ return false;
+ },
+ },
+};
diff --git a/packages/taler-util/src/aml/properties.ts b/packages/taler-util/src/aml/properties.ts
@@ -0,0 +1,164 @@
+import { TalerAmlProperties } from "../taler-account-properties.js";
+import { TalerFormAttributes } from "../taler-form-attributes.js";
+import {
+ AccountProperties,
+ LegitimizationRuleSet,
+} from "../types-taler-kyc-aml.js";
+
+/**
+ * List of account properties required by TOPS
+ */
+export const TOPS_AccountProperties = [
+ TalerAmlProperties.FILE_NOTE,
+ TalerAmlProperties.CUSTOMER_LABEL,
+ TalerAmlProperties.ACCOUNT_OPEN,
+ TalerAmlProperties.PEP_DOMESTIC,
+ TalerAmlProperties.PEP_FOREIGN,
+ TalerAmlProperties.PEP_INTERNATIONAL_ORGANIZATION,
+ TalerAmlProperties.HIGH_RISK_CUSTOMER,
+ TalerAmlProperties.HIGH_RISK_COUNTRY,
+ TalerAmlProperties.ACCOUNT_IDLE,
+ TalerAmlProperties.INVESTIGATION_TRIGGER,
+ TalerAmlProperties.INVESTIGATION_STATE,
+ TalerAmlProperties.SANCTION_LIST_BEST_MATCH,
+ TalerAmlProperties.SANCTION_LIST_RATING,
+ TalerAmlProperties.SANCTION_LIST_CONFIDENCE,
+ TalerAmlProperties.SANCTION_LIST_SUPPRESS,
+] as const;
+
+/**
+ * List of account properties required by GLS
+ */
+export const GLS_AccountProperties = [TalerAmlProperties.FILE_NOTE] as const;
+
+export type PropertiesDerivationFunctionByPropertyName<T extends string> = {
+ [name in T]: {
+ /**
+ * Based on all the current properties, the current account limits and
+ * new attributes of the account calculate if the property should
+ * change. The value "undefined" means no change.
+ *
+ * @param formId the current form being filled by the officer
+ * @param newAttributes the values of the current form
+ * @param limits
+ * @param state the current state of the account
+ * @returns
+ */
+ deriveProperty: (
+ formId: string,
+ newAttributes: Record<keyof typeof TalerFormAttributes, unknown>,
+ limits: LegitimizationRuleSet,
+ state: AccountProperties,
+ ) => string | boolean | undefined;
+ };
+};
+
+/**
+ * Calculate the value of the propertiy for TOPS account properties
+ */
+export const PropertiesDerivation_TOPS: PropertiesDerivationFunctionByPropertyName<
+ (typeof TOPS_AccountProperties)[number]
+> = {
+ ACCOUNT_OPEN: {
+ deriveProperty(formId, attributes, limits, state) {
+ return undefined;
+ },
+ },
+ PEP_DOMESTIC: {
+ deriveProperty(formId, attributes, limits, state) {
+ if (formId === "vqf_902_4") {
+ return !!attributes[TalerFormAttributes.PEP_DOMESTIC];
+ }
+ return undefined;
+ },
+ },
+ PEP_FOREIGN: {
+ deriveProperty(formId, attributes, limits, state) {
+ if (formId === "vqf_902_4") {
+ return !!attributes[TalerFormAttributes.PEP_FOREIGN];
+ }
+ return undefined;
+ },
+ },
+ PEP_INTERNATIONAL_ORGANIZATION: {
+ deriveProperty(formId, attributes, limits, state) {
+ if (formId === "vqf_902_4") {
+ return !!attributes[TalerFormAttributes.PEP_INTERNATIONAL_ORGANIZATION];
+ }
+ return undefined;
+ },
+ },
+ HIGH_RISK_CUSTOMER: {
+ deriveProperty(formId, attributes, limits, state) {
+ return (
+ attributes[TalerFormAttributes.RISK_CLASSIFICATION_LEVEL] ===
+ "HIGH_RISK"
+ );
+ },
+ },
+ HIGH_RISK_COUNTRY: {
+ deriveProperty(formId, attributes, limits, state) {
+ return (
+ attributes[TalerFormAttributes.COUNTRY_RISK_NATIONALITY_LEVEL] ===
+ "HIGH"
+ );
+ },
+ },
+ ACCOUNT_IDLE: {
+ deriveProperty(formId, attributes, limits, state) {
+ return undefined;
+ },
+ },
+ CUSTOMER_LABEL: {
+ deriveProperty(formId, attributes, limits, state) {
+ return undefined;
+ },
+ },
+ FILE_NOTE: {
+ deriveProperty(formId, attributes, limits, state) {
+ return undefined;
+ },
+ },
+ INVESTIGATION_STATE: {
+ deriveProperty(formId, attributes, limits, state) {
+ // https://bugs.gnunet.org/view.php?id=9677
+
+ return undefined;
+ },
+ },
+ INVESTIGATION_TRIGGER: {
+ deriveProperty(formId, attributes, limits, state) {
+ return undefined;
+ },
+ },
+ SANCTION_LIST_BEST_MATCH: {
+ deriveProperty(formId, attributes, limits, state) {
+ return undefined;
+ },
+ },
+ SANCTION_LIST_CONFIDENCE: {
+ deriveProperty(formId, attributes, limits, state) {
+ return undefined;
+ },
+ },
+ SANCTION_LIST_RATING: {
+ deriveProperty(formId, attributes, limits, state) {
+ return undefined;
+ },
+ },
+ SANCTION_LIST_SUPPRESS: {
+ deriveProperty(formId, attributes, limits, state) {
+ return undefined;
+ },
+ },
+};
+
+export const GLS_AML_PROPERTIES: PropertiesDerivationFunctionByPropertyName<
+ (typeof GLS_AccountProperties)[number]
+> = {
+ FILE_NOTE: {
+ deriveProperty(formId, attributes, limits, state) {
+ return undefined;
+ },
+ },
+};
diff --git a/packages/taler-util/src/aml/reporting.ts b/packages/taler-util/src/aml/reporting.ts
@@ -0,0 +1,387 @@
+import { AbsoluteTime, Duration, OfficerAccount, TalerExchangeHttpClient, TOPS_AmlEventsName } from "../index.js";
+
+/**
+ * Define a set of parameters to make a request to the server
+ */
+export type EventQuery<Ev> = {
+ event: Ev;
+ start: AbsoluteTime | undefined;
+ end: AbsoluteTime | undefined;
+};
+
+/**
+ * Map between a name and a request parameter
+ */
+export type QueryModel<Ev> = {
+ [name: string]: EventQuery<Ev>;
+};
+
+/**
+ * All the request needed to create the Event Reporting (TOPS)
+ * https://docs.taler.net/deployments/tops.html#event-reporting-tops
+ *
+ * Maps a request key to request parameters
+ *
+ */
+export const EventReporting_TOPS_queries = {
+ // Number of accounts that are opened
+ accounts_open_incr: {
+ event: TOPS_AmlEventsName.INCR_ACCOUNT_OPEN,
+ start: undefined,
+ end: undefined,
+ },
+ accounts_open_decr: {
+ event: TOPS_AmlEventsName.DECR_ACCOUNT_OPEN,
+ start: undefined,
+ end: undefined,
+ },
+ // Number of new GwG files in the last year
+ gwg_files_new_last_year: {
+ event: TOPS_AmlEventsName.INCR_ACCOUNT_OPEN,
+ start: AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ Duration.fromSpec({ years: -1 }),
+ ),
+ end: AbsoluteTime.now(),
+ },
+ // Number of GwG files closed in the last year
+ gwg_files_closed_last_year: {
+ event: TOPS_AmlEventsName.DECR_ACCOUNT_OPEN,
+ start: AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ Duration.fromSpec({ years: -1 }),
+ ),
+ end: AbsoluteTime.now(),
+ },
+ // Number of GwG files of high-risk customers
+ gwg_files_high_risk_incr: {
+ event: TOPS_AmlEventsName.INCR_HIGH_RISK_CUSTOMER, //FIXME: spec refers to INCR_HIGH_RISK
+ start: undefined,
+ end: undefined,
+ },
+ gwg_files_high_risk_decr: {
+ event: TOPS_AmlEventsName.DECR_HIGH_RISK_CUSTOMER,
+ start: undefined,
+ end: undefined,
+ },
+ // Number of GwG files managed with “increased risk” due to PEP status
+ gwg_files_pep_incr: {
+ event: TOPS_AmlEventsName.INCR_PEP,
+ start: undefined,
+ end: undefined,
+ },
+ gwg_files_pep_decr: {
+ event: TOPS_AmlEventsName.DECR_PEP,
+ start: undefined,
+ end: undefined,
+ },
+ // Number of MROS reports based on Art 9 Abs. 1 GwG (per year)
+ mros_reports_art9_last_year: {
+ event: TOPS_AmlEventsName.MROS_REPORTED_SUSPICION_SUBSTANTIATED,
+ start: AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ Duration.fromSpec({ years: -1 }),
+ ),
+ end: AbsoluteTime.now(),
+ },
+ // Number of MROS reports based on Art 305ter Abs. 2 StGB (per year)
+ mros_reports_art305_last_year: {
+ event: TOPS_AmlEventsName.MROS_REPORTED_SUSPICION_SIMPLE,
+ start: AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ Duration.fromSpec({ years: -1 }),
+ ),
+ end: AbsoluteTime.now(),
+ },
+ // Number of customers involved in proceedings for which Art 6 GwG did apply
+ accounts_involed_in_proceedings_last_year: {
+ event: TOPS_AmlEventsName.INCR_INVESTIGATION_CONCLUDED, //FIXME: spec refers to INCR_INVESTIGATION
+ start: AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ Duration.fromSpec({ years: -1 }),
+ ),
+ end: AbsoluteTime.now(),
+ },
+} satisfies QueryModel<TOPS_AmlEventsName>;
+
+/**
+ * All the calculation needed to create the Event Reporting (TOPS)
+ * https://docs.taler.net/deployments/tops.html#event-reporting-tops
+ *
+ * Maps a event reporting name with a calculation which uses the
+ * result of a query to the server.
+ *
+ * @param events The result of event reporting query
+ * @returns
+ */
+export const EventReporting_TOPS_calculation = (
+ events: CounterResultByEventName<typeof EventReporting_TOPS_queries>,
+) =>
+ ({
+ // Number of accounts that are opened:
+ accounts_opened: safeSub(
+ events.accounts_open_incr,
+ events.accounts_open_decr,
+ ),
+
+ // Number of new GwG files in the last year.
+ new_gwg_files_last_year: events.gwg_files_new_last_year,
+
+ // Number of GwG files closed in the last year
+ gwg_files_closed_last_year: events.gwg_files_closed_last_year,
+
+ // Number of GwG files of high-risk customers
+ gwg_files_high_risk: safeSub(
+ events.gwg_files_high_risk_incr,
+ events.gwg_files_high_risk_decr,
+ ),
+
+ // Number of GwG files managed with “increased risk” due to PEP status
+ gwg_files_pep: safeSub(
+ events.gwg_files_pep_incr,
+ events.gwg_files_pep_decr,
+ ),
+
+ // Number of MROS reports based on Art 9 Abs. 1 GwG (per year)
+ mros_reports_art9_last_year: events.mros_reports_art9_last_year,
+
+ // Number of MROS reports based on Art 305ter Abs. 2 StGB (per year)
+ mros_reports_art305_last_year: events.mros_reports_art305_last_year,
+
+ // Number of customers involved in proceedings for which Art 6 GwG did apply
+ accounts_involed_in_proceedings_last_year:
+ events.accounts_involed_in_proceedings_last_year,
+ }) as const;
+
+/**
+ * All the request needed to create the Event Reporting (CQF)
+ * https://docs.taler.net/deployments/tops.html#event-reporting-vqf
+ *
+ * Maps a request key to request parameters.
+ * Requires the times for the query range.
+ *
+ */
+export const EventReporting_VQF_queries = (jan_1st: AbsoluteTime, dec_31st: AbsoluteTime) => {
+ const zero = AbsoluteTime.fromMilliseconds(0);
+
+ return {
+ // Number of open accounts on January 1st (self-declaration 3.1.1)
+ accounts_open_first_jan_incr: {
+ event: TOPS_AmlEventsName.INCR_ACCOUNT_OPEN,
+ start: zero,
+ end: jan_1st,
+ },
+ accounts_open_first_jan_decr: {
+ event: TOPS_AmlEventsName.INCR_ACCOUNT_OPEN,
+ start: zero,
+ end: jan_1st,
+ },
+ // Number of newly opened accounts between 01.01.20XX and 31.12.20XX (self-declaration 3.1.2.)
+ accounts_opened_on_year: {
+ event: TOPS_AmlEventsName.INCR_ACCOUNT_OPEN,
+ start: jan_1st,
+ end: dec_31st,
+ },
+ // Number of AML files managed during the year 20XX (self-declaration 3.1.3.)
+ aml_files_managed_on_year_incr: {
+ event: TOPS_AmlEventsName.INCR_ACCOUNT_OPEN,
+ start: zero,
+ end: dec_31st,
+ },
+ aml_files_managed_on_year_decr: {
+ event: TOPS_AmlEventsName.DECR_ACCOUNT_OPEN,
+ start: zero,
+ end: jan_1st,
+ },
+ // Number of AML files closed between 01.01.20XX and 31.12.20XX (self-declaration 3.1.4)
+ aml_files_closed_on_year: {
+ event: TOPS_AmlEventsName.DECR_ACCOUNT_OPEN,
+ start: jan_1st,
+ end: dec_31st,
+ },
+ // Were there business relationships in the year 20XX with high risk? (self-declaration 4.1)
+ accounts_high_risk_incr: {
+ event: TOPS_AmlEventsName.INCR_HIGH_RISK_CUSTOMER,
+ start: zero,
+ end: dec_31st,
+ },
+ accounts_high_risk_decr: {
+ event: TOPS_AmlEventsName.DECR_HIGH_RISK_CUSTOMER,
+ start: zero,
+ end: dec_31st,
+ },
+ // Of those, how many were with PEPs? (self-declaration 4.2.)
+ accounts_pep_incr: {
+ event: TOPS_AmlEventsName.INCR_PEP,
+ start: zero,
+ end: dec_31st,
+ },
+ accounts_pep_decr: {
+ event: TOPS_AmlEventsName.DECR_PEP,
+ start: zero,
+ end: dec_31st,
+ },
+ // Of those PEPs, how many were with foreign PEPs? (self-declaration 4.3.)
+ accounts_pep_foreign_incr: {
+ event: TOPS_AmlEventsName.INCR_PEP_FOREIGN,
+ start: zero,
+ end: dec_31st,
+ },
+ accounts_pep_foreign_decr: {
+ event: TOPS_AmlEventsName.DECR_PEP_FOREIGN,
+ start: zero,
+ end: dec_31st,
+ },
+ // Number of other additional (other than PEPs and foreign PEPs) high-risk business relationships in 20XX
+ // comment: no need to add extra query
+ //
+ // Number of high-risk business relationship n total in 20xx (self-declaration 4.5.)
+ // comment: we have this information already
+ //
+ // Number of reports (substantiated suspicion) to MROS during 20xx (self-declaration 5.1)
+ mros_suspicion_substantiated: {
+ event: TOPS_AmlEventsName.MROS_REPORTED_SUSPICION_SUBSTANTIATED,
+ start: jan_1st,
+ end: dec_31st,
+ },
+ // Number of reports (simple suspicion) to MROS during 20xx (self-declaration 5.2)
+ mros_suspicion_simple: {
+ event: TOPS_AmlEventsName.MROS_REPORTED_SUSPICION_SIMPLE,
+ start: jan_1st,
+ end: dec_31st,
+ },
+ // Total number of reports to MROS during 20xx (self-declaration 5.3)
+ // comment: no need to add extra query
+ } satisfies QueryModel<TOPS_AmlEventsName>;
+};
+
+export const EventReporting_VQF_calculation = (
+ events: CounterResultByEventName<ReturnType<typeof EventReporting_VQF_queries>>,
+) => {
+ return {
+ // Number of open accounts on January 1st (self-declaration 3.1.1)
+ accounts_open_first_jan: safeSub(
+ events.accounts_open_first_jan_incr,
+ events.accounts_open_first_jan_decr,
+ ),
+
+ // Number of newly opened accounts between 01.01.20XX and 31.12.20XX (self-declaration 3.1.2.)
+ accounts_opened_on_year: events.accounts_opened_on_year,
+
+ // Number of AML files managed during the year 20XX (self-declaration 3.1.3.)
+ aml_files_managed_on_year: safeSub(
+ events.aml_files_managed_on_year_incr,
+ events.aml_files_managed_on_year_decr,
+ ),
+
+ // Number of AML files closed between 01.01.20XX and 31.12.20XX (self-declaration 3.1.4)
+ aml_files_closed_on_year: events.aml_files_closed_on_year,
+
+ // Were there business relationships in the year 20XX with high risk? (self-declaration 4.1)
+ accounts_high_risk: safeSub(
+ events.accounts_high_risk_incr,
+ events.accounts_high_risk_decr,
+ ),
+
+ // Of those, how many were with PEPs? (self-declaration 4.2.)
+ accounts_pep: safeSub(events.accounts_pep_incr, events.accounts_pep_decr),
+
+ // Of those PEPs, how many were with foreign PEPs? (self-declaration 4.3.)
+ accounts_pep_foreign: safeSub(
+ events.accounts_pep_foreign_incr,
+ events.accounts_pep_foreign_decr,
+ ),
+
+ // Number of other additional (other than PEPs and foreign PEPs) high-risk business relationships in 20XX (self-declaration 4.4.)
+ accounts_high_risk_other: safeSub(
+ safeSub(events.accounts_high_risk_incr, events.accounts_high_risk_decr), // 4.5
+ safeSub(events.accounts_pep_incr, events.accounts_pep_decr), // 4.2
+ ),
+
+ // Number of high-risk business relationship n total in 20xx (self-declaration 4.5.)
+ // comment: already implemented on 4.1
+
+ // Number of reports (substantiated suspicion) to MROS during 20xx (self-declaration 5.1)
+ mros_suspicion_substantiated: events.mros_suspicion_substantiated,
+
+ // Number of reports (simple suspicion) to MROS during 20xx (self-declaration 5.2)
+ mros_suspicion_simple: events.mros_suspicion_simple,
+
+ // Total number of reports to MROS during 20xx (self-declaration 5.3)
+ mros_total: safeAdd(
+ events.mros_suspicion_substantiated,
+ events.mros_suspicion_simple,
+ ),
+ };
+};
+
+export type CounterResultByEventName<T> = {
+ [name in keyof T]?: number;
+};
+
+export type EventQueryByEventName<T> = {
+ [name in keyof T]: EventQuery<string>;
+};
+
+function safeSub(
+ a: number | undefined,
+ b: number | undefined,
+): number | undefined {
+ return a === undefined || b == undefined ? undefined : a - b;
+}
+function safeAdd(
+ a: number | undefined,
+ b: number | undefined,
+): number | undefined {
+ return a === undefined || b == undefined ? undefined : a + b;
+}
+
+
+export async function fetchTopsInfoFromServer(api: TalerExchangeHttpClient, officer: OfficerAccount) {
+ type EventType = typeof EventReporting_TOPS_queries;
+ const eventList = Object.entries(EventReporting_TOPS_queries);
+
+ const allQueries = eventList.map(async ([_key, value]) => {
+ const key = _key as keyof EventType;
+ const response = await api.getAmlKycStatistics(officer, value.event, {
+ since: value.start,
+ until: value.end,
+ });
+ return { key, response };
+ });
+
+ const allResponses = await Promise.all(allQueries);
+
+ const resultMap = allResponses.reduce((prev, event) => {
+ prev[event.key] =
+ event.response.type === "ok" ? event.response.body.counter : undefined;
+ return prev;
+ }, {} as CounterResultByEventName<EventType>);
+
+ return EventReporting_TOPS_calculation(resultMap);
+}
+
+export async function fetchVqfInfoFromServer(api: TalerExchangeHttpClient, officer: OfficerAccount, jan_1st: AbsoluteTime, dec_31st: AbsoluteTime) {
+ const VQF_EVENTS_THIS_YEAR = EventReporting_VQF_queries(jan_1st, dec_31st);
+ type EventType = typeof VQF_EVENTS_THIS_YEAR;
+ const eventList = Object.entries(VQF_EVENTS_THIS_YEAR);
+
+ const allQueries = eventList.map(async ([_key, value]) => {
+ const key = _key as keyof EventType;
+ const response = await api.getAmlKycStatistics(officer, value.event, {
+ since: value.start,
+ until: value.end,
+ });
+ return { key, response };
+ });
+
+ const allResponses = await Promise.all(allQueries);
+
+ const resultMap = allResponses.reduce((prev, event) => {
+ prev[event.key] =
+ event.response.type === "ok" ? event.response.body.counter : undefined;
+ return prev;
+ }, {} as CounterResultByEventName<EventType>);
+
+ return EventReporting_VQF_calculation(resultMap);
+}
diff --git a/packages/taler-util/src/index.ts b/packages/taler-util/src/index.ts
@@ -86,8 +86,9 @@ export * from "./taler-signatures.js";
export * from "./account-restrictions.js";
-export * from "./aml/aml-events.js";
-export * from "./aml/aml-properties.js";
+export * from "./aml/events.js";
+export * from "./aml/properties.js";
+export * from "./aml/reporting.js";
export * from "./taler-account-properties.js";
export * from "./taler-form-attributes.js";
diff --git a/packages/web-util/src/forms/fields/InputIsoDate.tsx b/packages/web-util/src/forms/fields/InputIsoDate.tsx
@@ -132,6 +132,13 @@ export function InputIsoDate(
value={AbsoluteTime.fromMilliseconds(calendarOpenTime)}
onChange={(v) => {
// The date is always *stored* as an ISO date.
+ // !!!!! why?
+ /**
+ * form and fields should only care about how the information is asked
+ * to the user and should not care about how is store or sent to the server
+ *
+ * this format here should always be the 'pattern' of the field
+ */
onChange(
v.t_ms === "never" ? undefined : format(v.t_ms, "yyyy-MM-dd"),
);
diff --git a/packages/web-util/src/forms/gana/GLS_Onboarding.ts b/packages/web-util/src/forms/gana/GLS_Onboarding.ts
@@ -1,3 +1,4 @@
+import { isFuture, parse } from "date-fns";
import {
DoubleColumnFormDesign,
InternationalizationAPI,
@@ -39,6 +40,14 @@ export function GLS_Onboarding(
placeholder: "dd/MM/yyyy",
pattern: "dd/MM/yyyy",
required: true,
+ validator(text, form) {
+ //FIXME: why returning in this format even if pattern is in another?
+ const time = parse(text, "yyyy-MM-dd", new Date());
+ if (isFuture(time)) {
+ return i18n.str`it can't be in the future`;
+ }
+ return undefined;
+ },
},
{
id: TalerFormAttributes.PERSON_NATIONALITY,
@@ -103,6 +112,14 @@ export function GLS_Onboarding(
placeholder: "dd/MM/yyyy",
pattern: "dd/MM/yyyy",
required: true,
+ validator(text, form) {
+ //FIXME: why returning in this format even if pattern is in another?
+ const time = parse(text, "yyyy-MM-dd", new Date());
+ if (isFuture(time)) {
+ return i18n.str`it can't be in the future`;
+ }
+ return undefined;
+ },
},
{
id: TalerFormAttributes.BUSINESS_IS_NON_PROFIT,
diff --git a/packages/web-util/src/forms/gana/VQF_902_11_officer.ts b/packages/web-util/src/forms/gana/VQF_902_11_officer.ts
@@ -4,6 +4,7 @@ import {
FormMetadata,
InternationalizationAPI,
} from "../../index.browser.js";
+import { format } from "date-fns";
export const form_vqf_902_11_officer = (
i18n: InternationalizationAPI,
@@ -18,6 +19,8 @@ export const form_vqf_902_11_officer = (
export function VQF_902_11_officer(
i18n: InternationalizationAPI,
): DoubleColumnFormDesign {
+ const today = format(new Date(), "yyyy-MM-dd");
+
return {
type: "double-column",
title: i18n.str`Establishment of the controlling person (submitted by AML officer)`,
diff --git a/packages/web-util/src/forms/gana/VQF_902_1_customer.ts b/packages/web-util/src/forms/gana/VQF_902_1_customer.ts
@@ -14,6 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
+import { isFuture, parse } from "date-fns";
import {
DoubleColumnFormDesign,
InternationalizationAPI,
@@ -146,6 +147,14 @@ export function design_VQF_902_1_customer(
pattern: "dd/MM/yyyy",
defaultCalendarValue: "1980-01-01",
required: true,
+ validator(text, form) {
+ //FIXME: why returning in this format even if pattern is in another?
+ const time = parse(text, "yyyy-MM-dd", new Date());
+ if (isFuture(time)) {
+ return i18n.str`it can't be in the future`;
+ }
+ return undefined;
+ },
},
{
id: TalerFormAttributes.NATIONALITY,
@@ -276,12 +285,21 @@ export function design_VQF_902_1_customer(
pattern: "dd/MM/yyyy",
defaultCalendarValue: "1980-01-01",
required: true,
+ validator(text, form) {
+ //FIXME: why returning in this format even if pattern is in another?
+ const time = parse(text, "yyyy-MM-dd", new Date());
+ if (isFuture(time)) {
+ return i18n.str`it can't be in the future`;
+ }
+ return undefined;
+ },
},
{
id: TalerFormAttributes.NATIONALITY,
label: i18n.str`Nationality`,
type: "selectOne",
choices: countryNationalityList(i18n),
+ preferredChoiceVals: ["CH"],
required: true,
},
fieldPersonalId(i18n),
diff --git a/packages/web-util/src/forms/gana/VQF_902_1_officer.ts b/packages/web-util/src/forms/gana/VQF_902_1_officer.ts
@@ -15,7 +15,7 @@
*/
import { TalerFormAttributes } from "@gnu-taler/taler-util";
-import { format } from "date-fns";
+import { format, isFuture, isToday, parse } from "date-fns";
import {
DoubleColumnFormDesign,
FormMetadata,
@@ -53,6 +53,7 @@ export function VQF_902_1_officer(
placeholder: "dd/MM/yyyy",
pattern: "dd/MM/yyyy",
defaultValue: today,
+ disabled: true,
required: true,
},
{
@@ -123,6 +124,14 @@ export function VQF_902_1_officer(
defaultValue: today,
placeholder: "dd/MM/yyyy",
pattern: "dd/MM/yyyy",
+ validator(text, form) {
+ //FIXME: why returning in this format even if pattern is in another?
+ const time = parse(text, "yyyy-MM-dd", new Date());
+ if (isFuture(time)) {
+ return i18n.str`it can't be in the future`;
+ }
+ return undefined;
+ },
},
],
},
diff --git a/packages/web-util/src/forms/gana/VQF_902_9.ts b/packages/web-util/src/forms/gana/VQF_902_9.ts
@@ -5,6 +5,7 @@ import {
InternationalizationAPI,
} from "../../index.browser.js";
import { countryNationalityList } from "../../utils/select-ui-lists.js";
+import { format, isFuture, parse } from "date-fns";
export const form_vqf_902_9 = (
i18n: InternationalizationAPI,
@@ -18,6 +19,7 @@ export const form_vqf_902_9 = (
export function VQF_902_9(
i18n: InternationalizationAPI,
): DoubleColumnFormDesign {
+ const today = format(new Date(), "yyyy-MM-dd");
return {
type: "double-column",
title: i18n.str`Declaration of identity of the beneficial owner`,
@@ -95,12 +97,21 @@ export function VQF_902_9(
placeholder: "dd/MM/yyyy",
pattern: "dd/MM/yyyy",
required: true,
+ validator(text, form) {
+ //FIXME: why returning in this format even if pattern is in another?
+ const time = parse(text, "yyyy-MM-dd", new Date());
+ if (isFuture(time)) {
+ return i18n.str`it can't be in the future`;
+ }
+ return undefined;
+ },
},
{
id: TalerFormAttributes.NATIONALITY,
label: i18n.str`Nationality`,
type: "selectOne",
choices: countryNationalityList(i18n),
+ preferredChoiceVals: ["CH"],
required: true,
},
],
@@ -128,11 +139,12 @@ export function VQF_902_9(
{
id: TalerFormAttributes.SIGN_DATE,
label: i18n.str`Date`,
- type: "absoluteTimeText",
+ type: "isoDateText",
placeholder: "dd/MM/yyyy",
pattern: "dd/MM/yyyy",
required: true,
disabled: true,
+ defaultValue: today,
},
],
},
diff --git a/packages/web-util/src/forms/gana/VQF_902_9_customer.ts b/packages/web-util/src/forms/gana/VQF_902_9_customer.ts
@@ -5,6 +5,7 @@ import {
InternationalizationAPI,
} from "../../index.browser.js";
import { countryNationalityList } from "../../utils/select-ui-lists.js";
+import { format, isFuture, parse } from "date-fns";
export const form_vqf_902_9_customer = (
i18n: InternationalizationAPI,
@@ -18,6 +19,8 @@ export const form_vqf_902_9_customer = (
export function VQF_902_9_customer(
i18n: InternationalizationAPI,
): DoubleColumnFormDesign {
+ const today = format(new Date(), "yyyy-MM-dd");
+
return {
type: "double-column",
title: i18n.str`Declaration of identity of the beneficial owner`,
@@ -68,12 +71,21 @@ export function VQF_902_9_customer(
placeholder: "dd/MM/yyyy",
pattern: "dd/MM/yyyy",
required: true,
+ validator(text, form) {
+ //FIXME: why returning in this format even if pattern is in another?
+ const time = parse(text, "yyyy-MM-dd", new Date());
+ if (isFuture(time)) {
+ return i18n.str`it can't be in the future`;
+ }
+ return undefined;
+ },
},
{
id: TalerFormAttributes.NATIONALITY,
label: i18n.str`Nationality`,
type: "selectOne",
choices: countryNationalityList(i18n),
+ preferredChoiceVals: ["CH"],
required: true,
},
],
@@ -98,9 +110,10 @@ export function VQF_902_9_customer(
{
id: TalerFormAttributes.SIGN_DATE,
label: i18n.str`Date`,
- type: "absoluteTimeText",
+ type: "isoDateText",
placeholder: "dd/MM/yyyy",
pattern: "dd/MM/yyyy",
+ defaultValue: today,
required: true,
disabled: true,
},
diff --git a/packages/web-util/src/forms/gana/VQF_902_9_officer.ts b/packages/web-util/src/forms/gana/VQF_902_9_officer.ts
@@ -5,6 +5,7 @@ import {
InternationalizationAPI,
} from "../../index.browser.js";
import { countryNationalityList } from "../../utils/select-ui-lists.js";
+import { isFuture, parse } from "date-fns";
export const form_vqf_902_9_officer = (
i18n: InternationalizationAPI,
@@ -68,12 +69,21 @@ export function VQF_902_9_officer(
placeholder: "dd/MM/yyyy",
pattern: "dd/MM/yyyy",
required: true,
+ validator(text, form) {
+ //FIXME: why returning in this format even if pattern is in another?
+ const time = parse(text, "yyyy-MM-dd", new Date());
+ if (isFuture(time)) {
+ return i18n.str`it can't be in the future`;
+ }
+ return undefined;
+ },
},
{
id: TalerFormAttributes.NATIONALITY,
label: i18n.str`Nationality`,
type: "selectOne",
choices: countryNationalityList(i18n),
+ preferredChoiceVals: ["CH"],
required: true,
},
],