taler-typescript-core

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

commit c7b40bca285bc6a53fad65e1e3e0b38029f7d57d
parent 65359049ef9c5f2d8af1223a0f81c21975d24a46
Author: Sebastian <sebasjm@gmail.com>
Date:   Mon, 19 May 2025 15:19:14 -0300

fix #9777 #9778

Diffstat:
Mpackages/taler-util/src/aml/events.ts | 430++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Mpackages/taler-util/src/aml/properties.ts | 89++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
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; },