commit c7b40bca285bc6a53fad65e1e3e0b38029f7d57d
parent 65359049ef9c5f2d8af1223a0f81c21975d24a46
Author: Sebastian <sebasjm@gmail.com>
Date: Mon, 19 May 2025 15:19:14 -0300
fix #9777 #9778
Diffstat:
2 files changed, 471 insertions(+), 48 deletions(-)
diff --git a/packages/taler-util/src/aml/events.ts b/packages/taler-util/src/aml/events.ts
@@ -1,10 +1,7 @@
import { Amounts } from "../amounts.js";
+import { isOneOf, TalerAmlProperties } from "../index.node.js";
import { LimitOperationType } from "../types-taler-exchange.js";
-import {
- AccountProperties,
- KycRule,
- LegitimizationRuleSet,
-} from "../types-taler-kyc-aml.js";
+import { AccountProperties, KycRule } from "../types-taler-kyc-aml.js";
/**
* List of events triggered by TOPS
@@ -38,6 +35,21 @@ export enum GLS_AmlEventsName {
ACCOUNT_CLOSED = "ACCOUNT_CLOSED",
}
+enum KnownForms {
+ vqf_902_1_customer,
+ vqf_902_1_officer,
+ vqf_902_4,
+ vqf_902_5,
+ vqf_902_9_customer,
+ vqf_902_9_officer,
+ vqf_902_11_customer,
+ vqf_902_11_officer,
+ vqf_902_12,
+ vqf_902_13,
+ vqf_902_14,
+ vqf_902_15,
+}
+
export type EventMapInfo<T> = {
[name in keyof T]: {
/**
@@ -55,6 +67,7 @@ export type EventMapInfo<T> = {
* @returns
*/
shouldBeTriggered: (
+ formId: string,
prevLimits: KycRule[] | undefined,
nextLimits: KycRule[] | undefined,
prevState: AccountProperties | undefined,
@@ -77,106 +90,455 @@ function isAllowToMakeDeposits(limits: KycRule[]) {
return true;
}
+function propBecameTrue(
+ prevState: AccountProperties | undefined,
+ nextState: AccountProperties,
+ prop: string,
+): boolean {
+ const wasFalse = prevState === undefined || !prevState[prop];
+ const isTrue = !!nextState[prop];
+ return wasFalse && isTrue;
+}
+
+function propBecameFalse(
+ prevState: AccountProperties | undefined,
+ nextState: AccountProperties,
+ prop: string,
+): boolean {
+ const wasTrue = prevState !== undefined && !!prevState[prop];
+ const isFalse = !nextState[prop];
+ return wasTrue && isFalse;
+}
+
+function isAnyKindOfPep(state: AccountProperties): boolean {
+ return (
+ !!state[TalerAmlProperties.PEP_INTERNATIONAL_ORGANIZATION] ||
+ !!state[TalerAmlProperties.PEP_DOMESTIC] ||
+ !!state[TalerAmlProperties.PEP_FOREIGN]
+ );
+}
+
/**
* 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) {
+ shouldBeTriggered(formId, pL, nL, prevState, nextState, attr) {
+ if (nextState === undefined) {
+ return false;
+ }
+ if (
+ isOneOf(
+ formId,
+ KnownForms.vqf_902_1_customer,
+ KnownForms.vqf_902_1_officer,
+ ) &&
+ propBecameTrue(prevState, nextState, TalerAmlProperties.ACCOUNT_OPEN)
+ ) {
+ return true; // # event-rule 1
+ }
+
return false;
},
},
DECR_ACCOUNT_OPEN: {
- shouldBeTriggered(pL, nL, pS, nS, attr) {
+ shouldBeTriggered(formId, pL, nL, prevState, nextState, attr) {
return false;
},
},
INCR_HIGH_RISK_CUSTOMER: {
- shouldBeTriggered(pL, nL, pS, nS, attr) {
+ shouldBeTriggered(formId, pL, nL, prevState, nextState, attr) {
+ if (nextState === undefined) {
+ return false;
+ }
+ if (
+ isOneOf(
+ formId,
+ KnownForms.vqf_902_1_customer,
+ KnownForms.vqf_902_1_officer,
+ ) &&
+ propBecameTrue(prevState, nextState, TalerAmlProperties.ACCOUNT_OPEN) &&
+ !!nextState[TalerAmlProperties.HIGH_RISK_CUSTOMER]
+ ) {
+ return true; // # event-rule 6
+ }
+ // only accounts with history after this
+ if (prevState === undefined) {
+ return false;
+ }
+ if (
+ isOneOf(formId, KnownForms.vqf_902_4) &&
+ !!prevState[TalerAmlProperties.ACCOUNT_OPEN] &&
+ propBecameTrue(
+ prevState,
+ nextState,
+ TalerAmlProperties.HIGH_RISK_CUSTOMER,
+ )
+ ) {
+ return true; // # event-rule 18
+ }
return false;
},
},
DECR_HIGH_RISK_CUSTOMER: {
- shouldBeTriggered(pL, nL, pS, nS, attr) {
+ shouldBeTriggered(formId, pL, nL, prevState, nextState, attr) {
+ if (nextState === undefined) {
+ return false;
+ }
+ // only accounts with history after this
+ if (prevState === undefined) {
+ return false;
+ }
+ if (
+ isOneOf(formId, KnownForms.vqf_902_4) &&
+ !!prevState[TalerAmlProperties.ACCOUNT_OPEN] &&
+ propBecameFalse(
+ prevState,
+ nextState,
+ TalerAmlProperties.HIGH_RISK_CUSTOMER,
+ )
+ ) {
+ return true; // # event-rule 19
+ }
return false;
},
},
INCR_HIGH_RISK_COUNTRY: {
- shouldBeTriggered(pL, nL, pS, nS, attr) {
+ shouldBeTriggered(formId, pL, nL, prevState, nextState, attr) {
+ if (nextState === undefined) {
+ return false;
+ }
+ if (
+ isOneOf(
+ formId,
+ KnownForms.vqf_902_1_customer,
+ KnownForms.vqf_902_1_officer,
+ ) &&
+ propBecameTrue(prevState, nextState, TalerAmlProperties.ACCOUNT_OPEN) &&
+ !!nextState[TalerAmlProperties.HIGH_RISK_COUNTRY]
+ ) {
+ return true; // # event-rule 7
+ }
+ // only accounts with history after this
+ if (prevState === undefined) {
+ return false;
+ }
+ if (
+ isOneOf(formId, KnownForms.vqf_902_4) &&
+ !!prevState[TalerAmlProperties.ACCOUNT_OPEN] &&
+ propBecameTrue(
+ prevState,
+ nextState,
+ TalerAmlProperties.HIGH_RISK_CUSTOMER,
+ )
+ ) {
+ return true; // # event-rule 16
+ }
return false;
},
},
DECR_HIGH_RISK_COUNTRY: {
- shouldBeTriggered(pL, nL, pS, nS, attr) {
+ shouldBeTriggered(formId, pL, nL, prevState, nextState, attr) {
+ if (nextState === undefined) {
+ return false;
+ }
+ // only accounts with history after this
+ if (prevState === undefined) {
+ return false;
+ }
+ if (
+ isOneOf(formId, KnownForms.vqf_902_4) &&
+ !!prevState[TalerAmlProperties.ACCOUNT_OPEN] &&
+ propBecameFalse(
+ prevState,
+ nextState,
+ TalerAmlProperties.HIGH_RISK_CUSTOMER,
+ )
+ ) {
+ return true; // # event-rule 17
+ }
return false;
},
},
INCR_PEP: {
- shouldBeTriggered(pL, nL, pS, nS, attr) {
+ shouldBeTriggered(formId, pL, nL, prevState, nextState, attr) {
+ if (nextState === undefined) {
+ return false;
+ }
+ const isPep = isAnyKindOfPep(nextState);
+ if (
+ isOneOf(
+ formId,
+ KnownForms.vqf_902_1_customer,
+ KnownForms.vqf_902_1_officer,
+ ) &&
+ propBecameTrue(prevState, nextState, TalerAmlProperties.ACCOUNT_OPEN) &&
+ isPep
+ ) {
+ return true; // # event-rule 2
+ }
+ // only accounts with history after this
+ if (prevState === undefined) {
+ return false;
+ }
+ const wasPep = isAnyKindOfPep(prevState);
+ if (
+ isOneOf(formId, KnownForms.vqf_902_4) &&
+ !!prevState[TalerAmlProperties.ACCOUNT_OPEN] &&
+ !wasPep &&
+ isPep
+ ) {
+ return true; // # event-rule 15
+ }
return false;
},
},
DECR_PEP: {
- shouldBeTriggered(pL, nL, pS, nS, attr) {
+ shouldBeTriggered(formId, pL, nL, prevState, nextState, attr) {
+ if (nextState === undefined) {
+ return false;
+ }
+ // only accounts with history after this
+ if (prevState === undefined) {
+ return false;
+ }
+ const wasPep = isAnyKindOfPep(prevState);
+ const isPep = isAnyKindOfPep(nextState);
+ if (
+ isOneOf(formId, KnownForms.vqf_902_4) &&
+ !!prevState[TalerAmlProperties.ACCOUNT_OPEN] &&
+ wasPep &&
+ !isPep
+ ) {
+ return true; // # event-rule 14
+ }
return false;
},
},
INCR_PEP_FOREIGN: {
- shouldBeTriggered(pL, nL, pS, nS, attr) {
+ shouldBeTriggered(formId, pL, nL, prevState, nextState, attr) {
+ if (nextState === undefined) {
+ return false;
+ }
+ if (
+ isOneOf(
+ formId,
+ KnownForms.vqf_902_1_customer,
+ KnownForms.vqf_902_1_officer,
+ ) &&
+ propBecameTrue(prevState, nextState, TalerAmlProperties.ACCOUNT_OPEN) &&
+ !!nextState[TalerAmlProperties.PEP_FOREIGN]
+ ) {
+ return true; // # event-rule 3
+ }
+ // only accounts with history after this
+ if (prevState === undefined) {
+ return false;
+ }
+ if (
+ isOneOf(formId, KnownForms.vqf_902_4) &&
+ !!prevState[TalerAmlProperties.ACCOUNT_OPEN] &&
+ propBecameTrue(prevState, nextState, TalerAmlProperties.PEP_FOREIGN)
+ ) {
+ return true; // # event-rule 8
+ }
return false;
},
},
DECR_PEP_FOREIGN: {
- shouldBeTriggered(pL, nL, pS, nS, attr) {
+ shouldBeTriggered(formId, pL, nL, prevState, nextState, attr) {
+ if (nextState === undefined) {
+ return false;
+ }
+ // only accounts with history after this
+ if (prevState === undefined) {
+ return false;
+ }
+ if (
+ isOneOf(formId, KnownForms.vqf_902_4) &&
+ !!prevState[TalerAmlProperties.ACCOUNT_OPEN] &&
+ propBecameFalse(prevState, nextState, TalerAmlProperties.PEP_FOREIGN)
+ ) {
+ return true; // # event-rule 11
+ }
return false;
},
},
INCR_PEP_DOMESTIC: {
- shouldBeTriggered(pL, nL, pS, nS, attr) {
+ shouldBeTriggered(formId, pL, nL, prevState, nextState, attr) {
+ if (nextState === undefined) {
+ return false;
+ }
+ if (
+ isOneOf(
+ formId,
+ KnownForms.vqf_902_1_customer,
+ KnownForms.vqf_902_1_officer,
+ ) &&
+ propBecameTrue(prevState, nextState, TalerAmlProperties.ACCOUNT_OPEN) &&
+ !!nextState[TalerAmlProperties.PEP_DOMESTIC]
+ ) {
+ return true; // # event-rule 4
+ }
+ // only accounts with history after this
+ if (prevState === undefined) {
+ return false;
+ }
+ if (
+ isOneOf(formId, KnownForms.vqf_902_4) &&
+ !!prevState[TalerAmlProperties.ACCOUNT_OPEN] &&
+ propBecameTrue(prevState, nextState, TalerAmlProperties.PEP_DOMESTIC)
+ ) {
+ return true; // # event-rule 10
+ }
return false;
},
},
DECR_PEP_DOMESTIC: {
- shouldBeTriggered(pL, nL, pS, nS, attr) {
+ shouldBeTriggered(formId, pL, nL, prevState, nextState, attr) {
+ if (nextState === undefined) {
+ return false;
+ }
+ // only accounts with history after this
+ if (prevState === undefined) {
+ return false;
+ }
+ if (
+ isOneOf(formId, KnownForms.vqf_902_4) &&
+ !!prevState[TalerAmlProperties.ACCOUNT_OPEN] &&
+ propBecameFalse(prevState, nextState, TalerAmlProperties.PEP_DOMESTIC)
+ ) {
+ return true; // # event-rule 13
+ }
return false;
},
},
INCR_PEP_INTERNATIONAL_ORGANIZATION: {
- shouldBeTriggered(pL, nL, pS, nS, attr) {
+ shouldBeTriggered(formId, pL, nL, prevState, nextState, attr) {
+ if (nextState === undefined) {
+ return false;
+ }
+ if (
+ isOneOf(
+ formId,
+ KnownForms.vqf_902_1_customer,
+ KnownForms.vqf_902_1_officer,
+ ) &&
+ propBecameTrue(prevState, nextState, TalerAmlProperties.ACCOUNT_OPEN) &&
+ !!nextState[TalerAmlProperties.PEP_INTERNATIONAL_ORGANIZATION]
+ ) {
+ return true; // # event-rule 5
+ }
+ // only accounts with history after this
+ if (prevState === undefined) {
+ return false;
+ }
+ if (
+ isOneOf(formId, KnownForms.vqf_902_4) &&
+ !!prevState[TalerAmlProperties.ACCOUNT_OPEN] &&
+ propBecameTrue(
+ prevState,
+ nextState,
+ TalerAmlProperties.PEP_INTERNATIONAL_ORGANIZATION,
+ )
+ ) {
+ return true; // # event-rule 9
+ }
return false;
},
},
DECR_PEP_INTERNATIONAL_ORGANIZATION: {
- shouldBeTriggered(pL, nL, pS, nS, attr) {
+ shouldBeTriggered(formId, pL, nL, prevState, nextState, attr) {
+ if (nextState === undefined) {
+ return false;
+ }
+ // only accounts with history after this
+ if (prevState === undefined) {
+ return false;
+ }
+ if (
+ isOneOf(formId, KnownForms.vqf_902_4) &&
+ !!prevState[TalerAmlProperties.ACCOUNT_OPEN] &&
+ propBecameFalse(
+ prevState,
+ nextState,
+ TalerAmlProperties.PEP_INTERNATIONAL_ORGANIZATION,
+ )
+ ) {
+ return true; // # event-rule 12
+ }
return false;
},
},
MROS_REPORTED_SUSPICION_SIMPLE: {
- shouldBeTriggered(pL, nL, pS, nS, attr) {
+ shouldBeTriggered(formId, pL, nL, prevState, nextState, attr) {
+ if (prevState === undefined || nextState === undefined) {
+ return false;
+ }
+ if (
+ prevState[TalerAmlProperties.INVESTIGATION_STATE] === "NONE" ||
+ prevState[TalerAmlProperties.INVESTIGATION_STATE] ===
+ "INVESTIGATION_PENDING" ||
+ !prevState[TalerAmlProperties.INVESTIGATION_STATE]
+ ) {
+ if (
+ nextState[TalerAmlProperties.INVESTIGATION_STATE] ===
+ "REPORTED_SUSPICION_SIMPLE"
+ ) {
+ return true; // # event-rule 22
+ }
+ }
return false;
},
},
MROS_REPORTED_SUSPICION_SUBSTANTIATED: {
- shouldBeTriggered(pL, nL, pS, nS, attr) {
+ shouldBeTriggered(formId, pL, nL, prevState, nextState, attr) {
+ if (prevState === undefined || nextState === undefined) {
+ return false;
+ }
+ if (
+ prevState[TalerAmlProperties.INVESTIGATION_STATE] === "NONE" ||
+ prevState[TalerAmlProperties.INVESTIGATION_STATE] ===
+ "INVESTIGATION_PENDING" ||
+ !prevState[TalerAmlProperties.INVESTIGATION_STATE]
+ ) {
+ if (
+ nextState[TalerAmlProperties.INVESTIGATION_STATE] ===
+ "REPORTED_SUSPICION_SUBSTANTIATED"
+ ) {
+ return true; // # event-rule 21
+ }
+ }
return false;
},
},
INCR_INVESTIGATION_CONCLUDED: {
- shouldBeTriggered(pL, nL, pS, nS, attr) {
+ shouldBeTriggered(formId, pL, nL, prevState, nextState, attr) {
+ if (prevState === undefined || nextState === undefined) {
+ return false;
+ }
+ if (
+ prevState[TalerAmlProperties.INVESTIGATION_STATE] === "NONE" ||
+ prevState[TalerAmlProperties.INVESTIGATION_STATE] ===
+ "INVESTIGATION_PENDING" ||
+ !prevState[TalerAmlProperties.INVESTIGATION_STATE]
+ ) {
+ if (
+ nextState[TalerAmlProperties.INVESTIGATION_STATE] ===
+ "REPORTED_SUSPICION_SIMPLE" ||
+ nextState[TalerAmlProperties.INVESTIGATION_STATE] ===
+ "REPORTED_SUSPICION_SUBSTANTIATED" ||
+ nextState[TalerAmlProperties.INVESTIGATION_STATE] ===
+ "INVESTIGATION_COMPLETED_WITHOUT_SUSPICION"
+ ) {
+ return true; // # event-rule 20
+ }
+ }
return false;
},
},
DECR_INVESTIGATION_CONCLUDED: {
- shouldBeTriggered(pL, nL, pS, nS, attr) {
+ shouldBeTriggered(formId, pL, nL, prevState, nextState, attr) {
return false;
},
},
@@ -184,12 +546,12 @@ export const EventsDerivation_TOPS: EventMapInfo<typeof TOPS_AmlEventsName> = {
export const GLS_AML_EVENTS: EventMapInfo<typeof GLS_AmlEventsName> = {
ACCOUNT_OPENED: {
- shouldBeTriggered(pL, nL, pS, nS, attr) {
+ shouldBeTriggered(formId, pL, nL, prevState, nextState, attr) {
return false;
},
},
ACCOUNT_CLOSED: {
- shouldBeTriggered(pL, nL, pS, nS, attr) {
+ shouldBeTriggered(formId, pL, nL, prevState, nextState, attr) {
return false;
},
},
diff --git a/packages/taler-util/src/aml/properties.ts b/packages/taler-util/src/aml/properties.ts
@@ -40,7 +40,7 @@ export type PropertiesDerivationFunctionByPropertyName<T extends string> = {
*
* @param formId the current form being filled by the officer
* @param newAttributes the values of the current form
- * @param limits
+ * @param limits
* @param state the current state of the account
* @returns
*/
@@ -53,6 +53,30 @@ export type PropertiesDerivationFunctionByPropertyName<T extends string> = {
};
};
+export enum KnownForms {
+ vqf_902_1_customer,
+ vqf_902_1_officer,
+ vqf_902_4,
+ vqf_902_5,
+ vqf_902_9_customer,
+ vqf_902_9_officer,
+ vqf_902_11_customer,
+ vqf_902_11_officer,
+ vqf_902_12,
+ vqf_902_13,
+ vqf_902_14,
+ vqf_902_15,
+}
+
+export function isOneOf(formId: string, ...allowedForms: KnownForms[]) {
+ return (
+ -1 !==
+ allowedForms.findIndex((af) => {
+ return formId === KnownForms[af];
+ })
+ );
+}
+
/**
* Calculate the value of the propertiy for TOPS account properties
*/
@@ -61,12 +85,22 @@ export const PropertiesDerivation_TOPS: PropertiesDerivationFunctionByPropertyNa
> = {
ACCOUNT_OPEN: {
deriveProperty(formId, attributes, limits, state) {
- return undefined;
+ if (
+ isOneOf(
+ formId,
+ KnownForms.vqf_902_1_customer,
+ KnownForms.vqf_902_1_officer,
+ )
+ ) {
+ // if one of the vqf 902.1 then the account is being open
+ return true;
+ }
+ return false;
},
},
PEP_DOMESTIC: {
deriveProperty(formId, attributes, limits, state) {
- if (formId === "vqf_902_4") {
+ if (isOneOf(formId, KnownForms.vqf_902_4)) {
return !!attributes[TalerFormAttributes.PEP_DOMESTIC];
}
return undefined;
@@ -74,7 +108,7 @@ export const PropertiesDerivation_TOPS: PropertiesDerivationFunctionByPropertyNa
},
PEP_FOREIGN: {
deriveProperty(formId, attributes, limits, state) {
- if (formId === "vqf_902_4") {
+ if (isOneOf(formId, KnownForms.vqf_902_4)) {
return !!attributes[TalerFormAttributes.PEP_FOREIGN];
}
return undefined;
@@ -82,7 +116,7 @@ export const PropertiesDerivation_TOPS: PropertiesDerivationFunctionByPropertyNa
},
PEP_INTERNATIONAL_ORGANIZATION: {
deriveProperty(formId, attributes, limits, state) {
- if (formId === "vqf_902_4") {
+ if (isOneOf(formId, KnownForms.vqf_902_4)) {
return !!attributes[TalerFormAttributes.PEP_INTERNATIONAL_ORGANIZATION];
}
return undefined;
@@ -90,18 +124,24 @@ export const PropertiesDerivation_TOPS: PropertiesDerivationFunctionByPropertyNa
},
HIGH_RISK_CUSTOMER: {
deriveProperty(formId, attributes, limits, state) {
- return (
- attributes[TalerFormAttributes.RISK_CLASSIFICATION_LEVEL] ===
- "HIGH_RISK"
- );
+ if (isOneOf(formId, KnownForms.vqf_902_4)) {
+ return (
+ attributes[TalerFormAttributes.RISK_CLASSIFICATION_LEVEL] ===
+ "HIGH_RISK"
+ );
+ }
+ return undefined;
},
},
HIGH_RISK_COUNTRY: {
deriveProperty(formId, attributes, limits, state) {
- return (
- attributes[TalerFormAttributes.COUNTRY_RISK_NATIONALITY_LEVEL] ===
- "HIGH"
- );
+ if (isOneOf(formId, KnownForms.vqf_902_4)) {
+ return (
+ attributes[TalerFormAttributes.COUNTRY_RISK_NATIONALITY_LEVEL] ===
+ "HIGH"
+ );
+ }
+ return undefined;
},
},
ACCOUNT_IDLE: {
@@ -121,7 +161,28 @@ export const PropertiesDerivation_TOPS: PropertiesDerivationFunctionByPropertyNa
},
INVESTIGATION_STATE: {
deriveProperty(formId, attributes, limits, state) {
- // https://bugs.gnunet.org/view.php?id=9677
+ if (isOneOf(formId, KnownForms.vqf_902_14)) {
+ if (
+ attributes[TalerFormAttributes.INCRISK_RESULT] === "SIMPLE_SUSPICION"
+ ) {
+ return "REPORTED_SUSPICION_SIMPLE";
+ }
+ if (
+ attributes[TalerFormAttributes.INCRISK_RESULT] === "SUBSTANTIATED_SUSPICION"
+ ) {
+ return "REPORTED_SUSPICION_SUBSTANTIATED";
+ }
+ if (
+ attributes[TalerFormAttributes.INCRISK_RESULT] === "NO_SUSPICION"
+ ) {
+ return "INVESTIGATION_COMPLETED_WITHOUT_SUSPICION";
+ }
+ if (
+ attributes[TalerFormAttributes.INCRISK_RESULT] === "OTHER"
+ ) {
+ return "INVESTIGATION_PENDING";
+ }
+ }
return undefined;
},