commit a011a780d8002ce54e5e94afccde5cf8194a3f9b
parent 39fd544b71f538613a96c299076e0864d46abfd5
Author: Florian Dold <florian@dold.me>
Date: Wed, 28 Jan 2026 00:02:25 +0100
aml: also test events, fix import cycle
Diffstat:
7 files changed, 219 insertions(+), 60 deletions(-)
diff --git a/packages/taler-util/src/aml/events.test.ts b/packages/taler-util/src/aml/events.test.ts
@@ -0,0 +1,110 @@
+/*
+ This file is part of GNU Taler
+ (C) 2026 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 test from "ava";
+import { deriveTopsAmlEvents } from "./events.js";
+
+test("AML event derivation for TOPS, unknown form", (t) => {
+ const events = deriveTopsAmlEvents("some_unknown_form", {}, {}, {});
+ t.deepEqual([...events], []);
+});
+
+test("AML event derivation for TOPS, vqf_902_1_officer", (t) => {
+ {
+ const events = deriveTopsAmlEvents(
+ "vqf_902_1_officer",
+ {},
+ {},
+ {
+ ACCOUNT_OPEN: true,
+ },
+ );
+ t.deepEqual([...events], ["INCR_ACCOUNT_OPEN"]);
+ }
+
+ {
+ // Already open, no event
+ const events = deriveTopsAmlEvents(
+ "vqf_902_1_officer",
+ {},
+ {
+ ACCOUNT_OPEN: true,
+ },
+ {
+ ACCOUNT_OPEN: true,
+ },
+ );
+ t.deepEqual([...events], []);
+ }
+
+ {
+ const events = deriveTopsAmlEvents(
+ "vqf_902_1_officer",
+ {},
+ {},
+ {
+ ACCOUNT_OPEN: true,
+ PEP_FOREIGN: true,
+ },
+ );
+ t.deepEqual(
+ [...events],
+ ["INCR_ACCOUNT_OPEN", "INCR_PEP", "INCR_PEP_FOREIGN"],
+ );
+ }
+});
+
+test("AML event derivation for TOPS, vqf_902_4", (t) => {
+ {
+ // Account not open => no event
+ const events = deriveTopsAmlEvents(
+ "vqf_902_4",
+ {},
+ {},
+ {
+ PEP_FOREIGN: true,
+ },
+ );
+ t.deepEqual([...events], []);
+ }
+ {
+ const events = deriveTopsAmlEvents(
+ "vqf_902_4",
+ {},
+ {
+ ACCOUNT_OPEN: true,
+ },
+ {
+ PEP_FOREIGN: true,
+ },
+ );
+ t.deepEqual([...events], ["INCR_PEP", "INCR_PEP_FOREIGN"]);
+ }
+ {
+ const events = deriveTopsAmlEvents(
+ "vqf_902_4",
+ {},
+ {
+ ACCOUNT_OPEN: true,
+ PEP_FOREIGN: true,
+ },
+ {
+ PEP_FOREIGN: false,
+ },
+ );
+ t.deepEqual([...events], ["DECR_PEP", "DECR_PEP_FOREIGN"]);
+ }
+});
diff --git a/packages/taler-util/src/aml/events.ts b/packages/taler-util/src/aml/events.ts
@@ -1,7 +1,17 @@
import { Amounts } from "../amounts.js";
-import { AccountProperties, KycRule, LimitOperationType } from "../types-taler-exchange.js";
import { TalerAmlProperties } from "../taler-account-properties.js";
-import { isOneOf } from "./properties.js";
+import {
+ AccountProperties,
+ KycRule,
+ LimitOperationType,
+} from "../types-taler-exchange.js";
+import {
+ AmlFormAttributesMap,
+ isOneOf,
+ TopsAccountPropertiesMap,
+} from "./properties.js";
+
+// FIXME: We really ought to have a GANA registry for AML event names
/**
* List of events triggered by TOPS
@@ -68,8 +78,6 @@ export type EventMapInfo<T> = {
*/
shouldBeTriggered: (
formId: string,
- prevLimits: KycRule[] | undefined,
- nextLimits: KycRule[] | undefined,
prevState: AccountProperties | undefined,
nextState: AccountProperties | undefined,
newAttributes: Record<string, unknown>,
@@ -123,7 +131,7 @@ function isAnyKindOfPep(state: AccountProperties): boolean {
*/
export const EventsDerivation_TOPS: EventMapInfo<typeof TOPS_AmlEventsName> = {
INCR_ACCOUNT_OPEN: {
- shouldBeTriggered(formId, pL, nL, prevState, nextState, attr) {
+ shouldBeTriggered(formId, prevState, nextState, attr) {
if (nextState === undefined) {
return false;
}
@@ -142,12 +150,12 @@ export const EventsDerivation_TOPS: EventMapInfo<typeof TOPS_AmlEventsName> = {
},
},
DECR_ACCOUNT_OPEN: {
- shouldBeTriggered(formId, pL, nL, prevState, nextState, attr) {
+ shouldBeTriggered(formId, prevState, nextState, attr) {
return false;
},
},
INCR_HIGH_RISK_CUSTOMER: {
- shouldBeTriggered(formId, pL, nL, prevState, nextState, attr) {
+ shouldBeTriggered(formId, prevState, nextState, attr) {
if (nextState === undefined) {
return false;
}
@@ -181,7 +189,7 @@ export const EventsDerivation_TOPS: EventMapInfo<typeof TOPS_AmlEventsName> = {
},
},
DECR_HIGH_RISK_CUSTOMER: {
- shouldBeTriggered(formId, pL, nL, prevState, nextState, attr) {
+ shouldBeTriggered(formId, prevState, nextState, attr) {
if (nextState === undefined) {
return false;
}
@@ -204,7 +212,7 @@ export const EventsDerivation_TOPS: EventMapInfo<typeof TOPS_AmlEventsName> = {
},
},
INCR_HIGH_RISK_COUNTRY: {
- shouldBeTriggered(formId, pL, nL, prevState, nextState, attr) {
+ shouldBeTriggered(formId, prevState, nextState, attr) {
if (nextState === undefined) {
return false;
}
@@ -238,7 +246,7 @@ export const EventsDerivation_TOPS: EventMapInfo<typeof TOPS_AmlEventsName> = {
},
},
DECR_HIGH_RISK_COUNTRY: {
- shouldBeTriggered(formId, pL, nL, prevState, nextState, attr) {
+ shouldBeTriggered(formId, prevState, nextState, attr) {
if (nextState === undefined) {
return false;
}
@@ -261,7 +269,7 @@ export const EventsDerivation_TOPS: EventMapInfo<typeof TOPS_AmlEventsName> = {
},
},
INCR_PEP: {
- shouldBeTriggered(formId, pL, nL, prevState, nextState, attr) {
+ shouldBeTriggered(formId, prevState, nextState, attr) {
if (nextState === undefined) {
return false;
}
@@ -294,7 +302,7 @@ export const EventsDerivation_TOPS: EventMapInfo<typeof TOPS_AmlEventsName> = {
},
},
DECR_PEP: {
- shouldBeTriggered(formId, pL, nL, prevState, nextState, attr) {
+ shouldBeTriggered(formId, prevState, nextState, attr) {
if (nextState === undefined) {
return false;
}
@@ -316,7 +324,7 @@ export const EventsDerivation_TOPS: EventMapInfo<typeof TOPS_AmlEventsName> = {
},
},
INCR_PEP_FOREIGN: {
- shouldBeTriggered(formId, pL, nL, prevState, nextState, attr) {
+ shouldBeTriggered(formId, prevState, nextState, attr) {
if (nextState === undefined) {
return false;
}
@@ -346,7 +354,7 @@ export const EventsDerivation_TOPS: EventMapInfo<typeof TOPS_AmlEventsName> = {
},
},
DECR_PEP_FOREIGN: {
- shouldBeTriggered(formId, pL, nL, prevState, nextState, attr) {
+ shouldBeTriggered(formId, prevState, nextState, attr) {
if (nextState === undefined) {
return false;
}
@@ -365,7 +373,7 @@ export const EventsDerivation_TOPS: EventMapInfo<typeof TOPS_AmlEventsName> = {
},
},
INCR_PEP_DOMESTIC: {
- shouldBeTriggered(formId, pL, nL, prevState, nextState, attr) {
+ shouldBeTriggered(formId, prevState, nextState, attr) {
if (nextState === undefined) {
return false;
}
@@ -395,7 +403,7 @@ export const EventsDerivation_TOPS: EventMapInfo<typeof TOPS_AmlEventsName> = {
},
},
DECR_PEP_DOMESTIC: {
- shouldBeTriggered(formId, pL, nL, prevState, nextState, attr) {
+ shouldBeTriggered(formId, prevState, nextState, attr) {
if (nextState === undefined) {
return false;
}
@@ -414,7 +422,7 @@ export const EventsDerivation_TOPS: EventMapInfo<typeof TOPS_AmlEventsName> = {
},
},
INCR_PEP_INTERNATIONAL_ORGANIZATION: {
- shouldBeTriggered(formId, pL, nL, prevState, nextState, attr) {
+ shouldBeTriggered(formId, prevState, nextState, attr) {
if (nextState === undefined) {
return false;
}
@@ -448,7 +456,7 @@ export const EventsDerivation_TOPS: EventMapInfo<typeof TOPS_AmlEventsName> = {
},
},
DECR_PEP_INTERNATIONAL_ORGANIZATION: {
- shouldBeTriggered(formId, pL, nL, prevState, nextState, attr) {
+ shouldBeTriggered(formId, prevState, nextState, attr) {
if (nextState === undefined) {
return false;
}
@@ -471,7 +479,7 @@ export const EventsDerivation_TOPS: EventMapInfo<typeof TOPS_AmlEventsName> = {
},
},
MROS_REPORTED_SUSPICION_SIMPLE: {
- shouldBeTriggered(formId, pL, nL, prevState, nextState, attr) {
+ shouldBeTriggered(formId, prevState, nextState, attr) {
if (prevState === undefined || nextState === undefined) {
return false;
}
@@ -492,7 +500,7 @@ export const EventsDerivation_TOPS: EventMapInfo<typeof TOPS_AmlEventsName> = {
},
},
MROS_REPORTED_SUSPICION_SUBSTANTIATED: {
- shouldBeTriggered(formId, pL, nL, prevState, nextState, attr) {
+ shouldBeTriggered(formId, prevState, nextState, attr) {
if (prevState === undefined || nextState === undefined) {
return false;
}
@@ -513,7 +521,7 @@ export const EventsDerivation_TOPS: EventMapInfo<typeof TOPS_AmlEventsName> = {
},
},
INCR_INVESTIGATION_CONCLUDED: {
- shouldBeTriggered(formId, pL, nL, prevState, nextState, attr) {
+ shouldBeTriggered(formId, prevState, nextState, attr) {
if (prevState === undefined || nextState === undefined) {
return false;
}
@@ -538,7 +546,7 @@ export const EventsDerivation_TOPS: EventMapInfo<typeof TOPS_AmlEventsName> = {
},
},
DECR_INVESTIGATION_CONCLUDED: {
- shouldBeTriggered(formId, pL, nL, prevState, nextState, attr) {
+ shouldBeTriggered(formId, prevState, nextState, attr) {
return false;
},
},
@@ -546,13 +554,33 @@ export const EventsDerivation_TOPS: EventMapInfo<typeof TOPS_AmlEventsName> = {
export const GLS_AML_EVENTS: EventMapInfo<typeof GLS_AmlEventsName> = {
ACCOUNT_OPENED: {
- shouldBeTriggered(formId, pL, nL, prevState, nextState, attr) {
+ shouldBeTriggered(formId, prevState, nextState, attr) {
return false;
},
},
ACCOUNT_CLOSED: {
- shouldBeTriggered(formId, pL, nL, prevState, nextState, attr) {
+ shouldBeTriggered(formId, prevState, nextState, attr) {
return false;
},
},
};
+
+const topsEventNames = Object.values(TOPS_AmlEventsName);
+
+export function deriveTopsAmlEvents(
+ formId: string,
+ newAttributes: AmlFormAttributesMap,
+ oldProperties: TopsAccountPropertiesMap,
+ newProperties: TopsAccountPropertiesMap,
+): Set<TOPS_AmlEventsName> {
+ const events: Set<TOPS_AmlEventsName> = new Set();
+ for (const evt of topsEventNames) {
+ const h = EventsDerivation_TOPS[evt];
+ if (
+ h.shouldBeTriggered(formId, oldProperties, newProperties, newAttributes)
+ ) {
+ events.add(evt);
+ }
+ }
+ return events;
+}
diff --git a/packages/taler-util/src/aml/properties.test.ts b/packages/taler-util/src/aml/properties.test.ts
@@ -19,27 +19,48 @@ import { TalerAmlProperties } from "../taler-account-properties.js";
import { TalerFormAttributes } from "../taler-form-attributes.js";
import { deriveTopsAmlProperties } from "./properties.js";
-test("prop derivation for TOPS", (t) => {
+test("AML prop derivation for TOPS, unknown form", (t) => {
{
const props = deriveTopsAmlProperties("custom_does_not_exist132", {}, {});
t.deepEqual(props, {});
}
{
- const props = deriveTopsAmlProperties("vqf_902_1_officer", {}, {});
+ const props = deriveTopsAmlProperties(
+ "vqf_902_4",
+ {
+ [TalerFormAttributes.RISK_CLASSIFICATION_LEVEL]: "HIGH_RISK",
+ },
+ {},
+ );
t.deepEqual(props, {
- [TalerAmlProperties.ACCOUNT_OPEN]: true,
+ [TalerAmlProperties.HIGH_RISK_CUSTOMER]: true,
});
}
+});
+
+test("AML prop derivation for TOPS, vqf_902_4", (t) => {
{
const props = deriveTopsAmlProperties(
"vqf_902_4",
{
[TalerFormAttributes.RISK_CLASSIFICATION_LEVEL]: "HIGH_RISK",
},
- {},
+ {
+ PEP_FOREIGN: true,
+ },
);
t.deepEqual(props, {
[TalerAmlProperties.HIGH_RISK_CUSTOMER]: true,
+ [TalerAmlProperties.PEP_FOREIGN]: true,
+ });
+ }
+});
+
+test("AML prop derivation for TOPS, vqf_902_1_officer", (t) => {
+ {
+ const props = deriveTopsAmlProperties("vqf_902_1_officer", {}, {});
+ t.deepEqual(props, {
+ [TalerAmlProperties.ACCOUNT_OPEN]: true,
});
}
});
diff --git a/packages/taler-util/src/aml/reporting.ts b/packages/taler-util/src/aml/reporting.ts
@@ -364,7 +364,9 @@ export async function fetchTopsInfoFromServer(
const resultMap = allResponses.reduce((prev, event) => {
prev[event.key] =
- event.response.type === "ok" ? event.response.body.statistics[0].counter : undefined;
+ event.response.type === "ok"
+ ? event.response.body.statistics[0].counter
+ : undefined;
return prev;
}, {} as CounterResultByEventName<EventType>);
@@ -435,7 +437,9 @@ export async function fetchVqfInfoFromServer(
const resultMap = allResponses.reduce((prev, event) => {
prev[event.key] =
- event.response.type === "ok" ? event.response.body.statistics[0].counter : undefined;
+ event.response.type === "ok"
+ ? event.response.body.statistics[0].counter
+ : undefined;
return prev;
}, {} as CounterResultByEventName<EventType>);
diff --git a/packages/taler-util/src/errors.ts b/packages/taler-util/src/errors.ts
@@ -14,25 +14,24 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
+import { CancellationToken } from "./CancellationToken.js";
+import { TalerErrorCode } from "./taler-error-codes.js";
+import { AbsoluteTime } from "./time.js";
+import {
+ TransactionState,
+ TransactionType,
+} from "./types-taler-wallet-transactions.js";
+import {
+ PaymentInsufficientBalanceDetails,
+ TalerErrorDetail,
+} from "./types-taler-wallet.js";
+
/**
* Classes and helpers for error handling specific to wallet operations.
*
* @author Florian Dold <dold@taler.net>
*/
-/**
- * Imports.
- */
-import {
- AbsoluteTime,
- CancellationToken,
- PaymentInsufficientBalanceDetails,
- TalerErrorCode,
- TalerErrorDetail,
- TransactionState,
- TransactionType,
-} from "@gnu-taler/taler-util";
-
type empty = Record<string, never>;
export interface DetailsMap {
@@ -366,8 +365,8 @@ export function getErrorDetailFromException(e: any): TalerErrorDetail {
/**
* This function should not be called at runtime.
* Useful on switch/case to detect that all path has been contemplated.
- *
- * @param x
+ *
+ * @param x
*/
export function assertUnreachable(x: never): never {
throw new Error("Didn't expect to get here");
diff --git a/packages/taler-util/src/http-common.ts b/packages/taler-util/src/http-common.ts
@@ -16,16 +16,13 @@
SPDX-License-Identifier: AGPL3.0-or-later
*/
-import { CancellationToken } from "@gnu-taler/taler-util";
+import { base64FromArrayBuffer } from "./base64.js";
+import { CancellationToken } from "./CancellationToken.js";
import { Codec } from "./codec.js";
+import { makeErrorDetail, TalerError } from "./errors.js";
import { j2s } from "./helpers.js";
-import {
- TalerError,
- base64FromArrayBuffer,
- makeErrorDetail,
- stringToBytes,
-} from "./index.js";
import { Logger } from "./logging.js";
+import { stringToBytes } from "./taler-crypto.js";
import { TalerErrorCode } from "./taler-error-codes.js";
import { AbsoluteTime, Duration } from "./time.js";
import { TalerErrorDetail } from "./types-taler-wallet.js";
diff --git a/packages/taler-util/src/index.ts b/packages/taler-util/src/index.ts
@@ -1,8 +1,8 @@
export * from "./amounts.js";
export * from "./bank-api-client.js";
export * from "./base64.js";
-export * from "./bitcoin.js";
export * from "./bech32.js";
+export * from "./bitcoin.js";
export * from "./CancellationToken.js";
export * from "./codec.js";
export * from "./contract-terms.js";
@@ -19,8 +19,8 @@ export * from "./http-client/bank-wire.js";
export * from "./http-client/challenger.js";
export * from "./http-client/donau-client.js";
export * from "./http-client/exchange-client.js";
-export * from "./http-client/merchant.js";
export * from "./http-client/mailbox.js";
+export * from "./http-client/merchant.js";
export * from "./http-client/officer-account.js";
export {
@@ -64,12 +64,12 @@ export * from "./transaction-test-data.js";
export * from "./url.js";
// FIXME: remove all this, needs refactor
+export * from "./types-donau.js";
export * from "./types-taler-bank-conversion.js";
export * from "./types-taler-bank-integration.js";
export * from "./types-taler-exchange.js";
export * from "./types-taler-mailbox.js";
export * from "./types-taler-merchant.js";
-export * from "./types-donau.js";
// end
export * from "./types-taler-common.js";
@@ -88,17 +88,17 @@ export * as TalerMerchantApi from "./types-taler-merchant.js";
export * as TalerRevenueApi from "./types-taler-revenue.js";
export * as TalerWireGatewayApi from "./types-taler-wire-gateway.js";
+export * from "./taler-account-properties.js";
+export * from "./taler-form-attributes.js";
+
export * from "./taler-signatures.js";
export * from "./account-restrictions.js";
-export * from "./aml/events.js";
export * from "./aml/properties.js";
-export * from "./taler-account-properties.js";
-export * from "./taler-form-attributes.js";
+export * from "./aml/events.js";
+export * from "./aml/reporting.js";
export * from "./iso-3166.js";
export * from "./iso-4217.js";
export * from "./iso-639.js";
-
-export * from "./aml/reporting.js";