From bb2749f86c6a62488460a934aae4dbcc602502e0 Mon Sep 17 00:00:00 2001 From: Yannick <52333989+Yannick1712@users.noreply.github.com> Date: Fri, 27 Feb 2026 15:31:32 +0100 Subject: [PATCH] [NOTASK] phoneCall aml refactoring (#3257) * [NOTASK] phoneCall aml refactoring * [NOTASK] bankTx save bic from bankAccount * [NOTASK] Refactoring * [NOTASK] fix build * [NOTASK] remove bic loading * [NOTASK] add helper getter * [NOTASK] Refactoring * [NOTASK] remove unused --- .../core/aml/services/aml-helper.service.ts | 76 ++++++++++++------- .../transaction/transaction-util.service.ts | 12 ++- .../generic/kyc/services/kyc.service.ts | 4 +- .../kyc/services/name-check.service.ts | 3 +- .../models/bank-data/bank-data.service.ts | 8 +- .../user/models/user-data/user-data.entity.ts | 10 ++- .../bank-account/is-dfx-iban.validator.ts | 31 +++++++- .../special-external-account.entity.ts | 5 ++ .../special-external-account.service.ts | 5 ++ 9 files changed, 109 insertions(+), 45 deletions(-) diff --git a/src/subdomains/core/aml/services/aml-helper.service.ts b/src/subdomains/core/aml/services/aml-helper.service.ts index de2bbc2dab..515a441c85 100644 --- a/src/subdomains/core/aml/services/aml-helper.service.ts +++ b/src/subdomains/core/aml/services/aml-helper.service.ts @@ -1,3 +1,4 @@ +import * as IbanTools from 'ibantools'; import { Config, Environment } from 'src/config/config'; import { Active, isAsset } from 'src/shared/models/active'; import { Country } from 'src/shared/models/country/country.entity'; @@ -220,7 +221,7 @@ export class AmlHelperService { (entity.bankTx || entity.checkoutTx) && entity.userData.phone && entity.userData.birthday && - (!entity.userData.accountType || entity.userData.accountType === AccountType.PERSONAL) && + entity.userData.isPersonalAccount && Util.yearsDiff(entity.userData.birthday) > 55 ) errors.push(AmlError.PHONE_VERIFICATION_NEEDED); @@ -254,28 +255,46 @@ export class AmlHelperService { ) errors.push(AmlError.BIC_BLACKLISTED); if ( - blacklist.some((b) => - b.matches( - [ - SpecialExternalAccountType.BANNED_IBAN, - SpecialExternalAccountType.BANNED_IBAN_BUY, - SpecialExternalAccountType.BANNED_IBAN_AML, - ], - entity.bankTx.iban, - ), + blacklist.some( + (b) => + b.matches( + [ + SpecialExternalAccountType.BANNED_IBAN, + SpecialExternalAccountType.BANNED_IBAN_BUY, + SpecialExternalAccountType.BANNED_IBAN_AML, + ], + entity.bankTx.iban, + ) || + b.matches( + [ + SpecialExternalAccountType.BANNED_BLZ, + SpecialExternalAccountType.BANNED_BLZ_BUY, + SpecialExternalAccountType.BANNED_BLZ_AML, + ], + IbanTools.extractIBAN(entity.bankTx.iban).bankIdentifier, + ), ) ) errors.push(AmlError.IBAN_BLACKLISTED); if ( + !entity.userData.phoneCallCheckDate && + entity.userData.isPersonalAccount && phoneCallList.some((b) => b.matches([SpecialExternalAccountType.AML_PHONE_CALL_NEEDED_BIC_BUY], entity.bankTx.bic), ) ) errors.push(AmlError.BIC_PHONE_VERIFICATION_NEEDED); if ( - phoneCallList.some((b) => - b.matches([SpecialExternalAccountType.AML_PHONE_CALL_NEEDED_IBAN_BUY], entity.bankTx.iban), + !entity.userData.phoneCallCheckDate && + entity.userData.isPersonalAccount && + phoneCallList.some( + (b) => + b.matches([SpecialExternalAccountType.AML_PHONE_CALL_NEEDED_IBAN_BUY], entity.bankTx.iban) || + b.matches( + [SpecialExternalAccountType.AML_PHONE_CALL_NEEDED_BLZ_BUY], + IbanTools.extractIBAN(entity.bankTx.iban).bankIdentifier, + ), ) ) errors.push(AmlError.IBAN_PHONE_VERIFICATION_NEEDED); @@ -330,15 +349,24 @@ export class AmlHelperService { if (entity.sell.fiat.name === 'CHF' && !Config.isDomesticIban(entity.sell.iban)) errors.push(AmlError.ABROAD_CHF_NOT_ALLOWED); if ( - blacklist.some((b) => - b.matches( - [ - SpecialExternalAccountType.BANNED_IBAN, - SpecialExternalAccountType.BANNED_IBAN_SELL, - SpecialExternalAccountType.BANNED_IBAN_AML, - ], - entity.sell.iban, - ), + blacklist.some( + (b) => + b.matches( + [ + SpecialExternalAccountType.BANNED_IBAN, + SpecialExternalAccountType.BANNED_IBAN_SELL, + SpecialExternalAccountType.BANNED_IBAN_AML, + ], + entity.sell.iban, + ) || + b.matches( + [ + SpecialExternalAccountType.BANNED_BLZ, + SpecialExternalAccountType.BANNED_BLZ_SELL, + SpecialExternalAccountType.BANNED_BLZ_AML, + ], + IbanTools.extractIBAN(entity.sell.iban).bankIdentifier, + ), ) ) errors.push(AmlError.IBAN_BLACKLISTED); @@ -440,11 +468,7 @@ export class AmlHelperService { break; case AmlRule.RULE_16: - if ( - entity instanceof BuyCrypto && - entity.userData.accountType === AccountType.PERSONAL && - !entity.userData.phoneCallCheckDate - ) + if (entity instanceof BuyCrypto && entity.userData.isPersonalAccount && !entity.userData.phoneCallCheckDate) errors.push(AmlError.PHONE_VERIFICATION_NEEDED); break; } diff --git a/src/subdomains/core/transaction/transaction-util.service.ts b/src/subdomains/core/transaction/transaction-util.service.ts index d352cd1994..9a3096a6a2 100644 --- a/src/subdomains/core/transaction/transaction-util.service.ts +++ b/src/subdomains/core/transaction/transaction-util.service.ts @@ -108,12 +108,20 @@ export class TransactionUtilService { if ( blockedAccounts.some( (b) => - [ + ([ SpecialExternalAccountType.BANNED_IBAN, SpecialExternalAccountType.BANNED_IBAN_BUY, SpecialExternalAccountType.BANNED_IBAN_SELL, SpecialExternalAccountType.BANNED_IBAN_AML, - ].includes(b.type) && b.value === iban, + ].includes(b.type) && + b.value === iban) || + ([ + SpecialExternalAccountType.BANNED_BLZ, + SpecialExternalAccountType.BANNED_BLZ_BUY, + SpecialExternalAccountType.BANNED_BLZ_SELL, + SpecialExternalAccountType.BANNED_BLZ_AML, + ].includes(b.type) && + b.value === IbanTools.extractIBAN(iban).bankIdentifier), ) ) throw new BadRequestException('Iban not allowed'); diff --git a/src/subdomains/generic/kyc/services/kyc.service.ts b/src/subdomains/generic/kyc/services/kyc.service.ts index 967614b38a..9ee2411337 100644 --- a/src/subdomains/generic/kyc/services/kyc.service.ts +++ b/src/subdomains/generic/kyc/services/kyc.service.ts @@ -259,7 +259,7 @@ export class KycService { } else if ( errors.includes(KycError.VERIFIED_NAME_MISSING) && errors.length === 1 && - entity.userData.accountType === AccountType.PERSONAL + entity.userData.isPersonalAccount ) { await this.userDataService.updateUserDataInternal(entity.userData, { verifiedName: `${entity.userData.firstname} ${entity.userData.surname}`, @@ -1513,7 +1513,7 @@ export class KycService { identStep.userData.verifiedCountry ?? identStep.userData.country; - if (identStep.userData.accountType === AccountType.PERSONAL) { + if (identStep.userData.isPersonalAccount) { // Personal Account if (!userCountry.dfxEnable) errors.push(KycError.COUNTRY_NOT_ALLOWED); if (userCountry.manualReviewRequired) errors.push(KycError.MANUAL_REVIEW_REQUIRED); diff --git a/src/subdomains/generic/kyc/services/name-check.service.ts b/src/subdomains/generic/kyc/services/name-check.service.ts index 0ae30c10e5..0d3c3b1ab6 100644 --- a/src/subdomains/generic/kyc/services/name-check.service.ts +++ b/src/subdomains/generic/kyc/services/name-check.service.ts @@ -9,7 +9,6 @@ import { import { Util } from 'src/shared/utils/util'; import { IsNull } from 'typeorm'; import { BankData, BankDataType } from '../../user/models/bank-data/bank-data.entity'; -import { AccountType } from '../../user/models/user-data/account-type.enum'; import { UserData } from '../../user/models/user-data/user-data.entity'; import { UserDataService } from '../../user/models/user-data/user-data.service'; import { DilisenseApiData } from '../dto/input/dilisense-data.dto'; @@ -55,7 +54,7 @@ export class NameCheckService implements OnModuleInit { // ); // Personal name check - if (!bankData.userData.accountType || bankData.userData.accountType === AccountType.PERSONAL) { + if (bankData.userData.isPersonalAccount) { const { data, file } = await this.getRiskDataAndUploadPdf( bankData.userData, false, diff --git a/src/subdomains/generic/user/models/bank-data/bank-data.service.ts b/src/subdomains/generic/user/models/bank-data/bank-data.service.ts index cc391c82e4..2ee4ed1d1f 100644 --- a/src/subdomains/generic/user/models/bank-data/bank-data.service.ts +++ b/src/subdomains/generic/user/models/bank-data/bank-data.service.ts @@ -23,7 +23,6 @@ import { SpecialExternalAccountService } from 'src/subdomains/supporting/payment import { FindOptionsRelations, FindOptionsWhere, IsNull, Like, Not } from 'typeorm'; import { AccountMerge, MergeReason } from '../account-merge/account-merge.entity'; import { AccountMergeService } from '../account-merge/account-merge.service'; -import { AccountType } from '../user-data/account-type.enum'; import { KycType, UserDataStatus } from '../user-data/user-data.enum'; import { BankData, BankDataType, BankDataVerificationError } from './bank-data.entity'; import { UpdateBankDataDto } from './dto/update-bank-data.dto'; @@ -70,17 +69,14 @@ export class BankDataService { async verifyBankData(entity: BankData): Promise { try { - if ( - !entity.userData.verifiedName && - (entity.userData.accountType === AccountType.PERSONAL || entity.type === BankDataType.BANK_IN) - ) + if (!entity.userData.verifiedName && (entity.userData.isPersonalAccount || entity.type === BankDataType.BANK_IN)) await this.userDataRepo.update(...entity.userData.setVerifiedName(entity.name)); if (entity.type === BankDataType.USER) return; if ([BankDataType.IDENT, BankDataType.NAME_CHECK].includes(entity.type)) { if ( - entity.userData.accountType === AccountType.PERSONAL || + entity.userData.isPersonalAccount || entity.userData.hasCompletedStep(KycStepName.LEGAL_ENTITY) || entity.userData.hasCompletedStep(KycStepName.SOLE_PROPRIETORSHIP_CONFIRMATION) ) { diff --git a/src/subdomains/generic/user/models/user-data/user-data.entity.ts b/src/subdomains/generic/user/models/user-data/user-data.entity.ts index d5430deb17..aa6f840252 100644 --- a/src/subdomains/generic/user/models/user-data/user-data.entity.ts +++ b/src/subdomains/generic/user/models/user-data/user-data.entity.ts @@ -639,6 +639,10 @@ export class UserData extends IEntity { // --- KYC PROCESS --- // + get isPersonalAccount(): boolean { + return !this.accountType || this.accountType === AccountType.PERSONAL; + } + get hasSuspiciousMail(): boolean { return (this.mail?.split('@')[0].match(/\d/g) ?? []).length > 2; } @@ -741,7 +745,7 @@ export class UserData extends IEntity { get requiredKycFields(): string[] { return ['accountType', 'mail', 'phone', 'firstname', 'surname', 'street', 'location', 'zip', 'country'].concat( - !this.accountType || this.accountType === AccountType.PERSONAL + this.isPersonalAccount ? [] : ['organizationName', 'organizationStreet', 'organizationLocation', 'organizationZip', 'organizationCountry'], ); @@ -752,9 +756,7 @@ export class UserData extends IEntity { } get requiredInvoiceFields(): string[] { - return ['accountType'].concat( - !this.accountType || this.accountType === AccountType.PERSONAL ? ['firstname', 'surname'] : ['organizationName'], - ); + return ['accountType'].concat(this.isPersonalAccount ? ['firstname', 'surname'] : ['organizationName']); } get isInvoiceDataComplete(): boolean { diff --git a/src/subdomains/supporting/bank/bank-account/is-dfx-iban.validator.ts b/src/subdomains/supporting/bank/bank-account/is-dfx-iban.validator.ts index 6c9e19f331..b7226c76f7 100644 --- a/src/subdomains/supporting/bank/bank-account/is-dfx-iban.validator.ts +++ b/src/subdomains/supporting/bank/bank-account/is-dfx-iban.validator.ts @@ -30,6 +30,7 @@ export class IsDfxIbanValidator implements ValidatorConstraintInterface { ) {} private blockedIbans: string[] = []; + private blockedBLZs: string[] = []; private blockedBICs: string[] = []; private dfxBanks: Bank[] = []; private currentBIC: string = undefined; @@ -37,11 +38,23 @@ export class IsDfxIbanValidator implements ValidatorConstraintInterface { async validate(_: string, args: ValidationArguments) { // blacklist types const type = args.constraints[0]; - const types = [SpecialExternalAccountType.BANNED_IBAN, SpecialExternalAccountType.BANNED_BIC]; + const types = [ + SpecialExternalAccountType.BANNED_IBAN, + SpecialExternalAccountType.BANNED_BLZ, + SpecialExternalAccountType.BANNED_BIC, + ]; if ([IbanType.BUY, IbanType.BOTH].includes(type)) - types.push(SpecialExternalAccountType.BANNED_IBAN_BUY, SpecialExternalAccountType.BANNED_BIC_BUY); + types.push( + SpecialExternalAccountType.BANNED_IBAN_BUY, + SpecialExternalAccountType.BANNED_BLZ_BUY, + SpecialExternalAccountType.BANNED_BIC_BUY, + ); if ([IbanType.SELL, IbanType.BOTH].includes(type)) - types.push(SpecialExternalAccountType.BANNED_IBAN_SELL, SpecialExternalAccountType.BANNED_BIC_SELL); + types.push( + SpecialExternalAccountType.BANNED_IBAN_SELL, + SpecialExternalAccountType.BANNED_BLZ_SELL, + SpecialExternalAccountType.BANNED_BIC_SELL, + ); const blacklists = await this.specialExternalAccountService.getBlacklist(types); @@ -56,6 +69,15 @@ export class IsDfxIbanValidator implements ValidatorConstraintInterface { ].includes(b.type), ) .map((b) => b.value); + this.blockedBLZs = blacklists + .filter((b) => + [ + SpecialExternalAccountType.BANNED_BLZ, + SpecialExternalAccountType.BANNED_BLZ_BUY, + SpecialExternalAccountType.BANNED_BLZ_SELL, + ].includes(b.type), + ) + .map((b) => b.value); this.blockedBICs = blacklists .filter((b) => [ @@ -86,6 +108,9 @@ export class IsDfxIbanValidator implements ValidatorConstraintInterface { const isBlocked = this.blockedIbans.some((i) => new RegExp(i.toLowerCase()).test(iban.toLowerCase())); if (isBlocked) return `${args.property} not allowed`; + if (this.blockedBLZs.some((i) => new RegExp(i).test(IbanTools.extractIBAN(iban).bankIdentifier))) + return `${args.property} not allowed`; + if (this.blockedBICs.some((b) => new RegExp(b.toLowerCase()).test(this.currentBIC?.toLowerCase()))) return `${args.property} BIC not allowed`; diff --git a/src/subdomains/supporting/payment/entities/special-external-account.entity.ts b/src/subdomains/supporting/payment/entities/special-external-account.entity.ts index ead402339a..bbe31599a2 100644 --- a/src/subdomains/supporting/payment/entities/special-external-account.entity.ts +++ b/src/subdomains/supporting/payment/entities/special-external-account.entity.ts @@ -12,9 +12,14 @@ export enum SpecialExternalAccountType { BANNED_BIC_BUY = 'BannedBicBuy', BANNED_BIC_SELL = 'BannedBicSell', BANNED_BIC_AML = 'BannedBicAml', + BANNED_BLZ = 'BannedBlz', + BANNED_BLZ_BUY = 'BannedBlzBuy', + BANNED_BLZ_SELL = 'BannedBlzSell', + BANNED_BLZ_AML = 'BannedBlzAml', BANNED_MAIL = 'BannedMail', BANNED_ACCOUNT_IBAN = 'BannedAccountIban', AML_PHONE_CALL_NEEDED_BIC_BUY = 'AmlPhoneCallNeededBicBuy', + AML_PHONE_CALL_NEEDED_BLZ_BUY = 'AmlPhoneCallNeededBlzBuy', AML_PHONE_CALL_NEEDED_IBAN_BUY = 'AmlPhoneCallNeededIbanBuy', } diff --git a/src/subdomains/supporting/payment/services/special-external-account.service.ts b/src/subdomains/supporting/payment/services/special-external-account.service.ts index 43429f56c9..f52210f405 100644 --- a/src/subdomains/supporting/payment/services/special-external-account.service.ts +++ b/src/subdomains/supporting/payment/services/special-external-account.service.ts @@ -49,6 +49,7 @@ export class SpecialExternalAccountService { type: In([ SpecialExternalAccountType.AML_PHONE_CALL_NEEDED_BIC_BUY, SpecialExternalAccountType.AML_PHONE_CALL_NEEDED_IBAN_BUY, + SpecialExternalAccountType.AML_PHONE_CALL_NEEDED_BLZ_BUY, ]), }); } @@ -67,6 +68,10 @@ export class SpecialExternalAccountService { SpecialExternalAccountType.BANNED_BIC_AML, SpecialExternalAccountType.BANNED_MAIL, SpecialExternalAccountType.BANNED_ACCOUNT_IBAN, + SpecialExternalAccountType.BANNED_BLZ, + SpecialExternalAccountType.BANNED_BLZ_BUY, + SpecialExternalAccountType.BANNED_BLZ_SELL, + SpecialExternalAccountType.BANNED_BLZ_AML, ], ), });