From ac3807e2d4ad14cda887958c7a3074ddb8ea1d5d Mon Sep 17 00:00:00 2001 From: Yannick1712 <52333989+Yannick1712@users.noreply.github.com> Date: Thu, 29 Jan 2026 14:12:04 +0100 Subject: [PATCH 1/9] [DEV-4546] SupportIssue type VerificationCall --- .../supporting/support-issue/enums/support-issue.enum.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/subdomains/supporting/support-issue/enums/support-issue.enum.ts b/src/subdomains/supporting/support-issue/enums/support-issue.enum.ts index 236ab3890e..794dfae7eb 100644 --- a/src/subdomains/supporting/support-issue/enums/support-issue.enum.ts +++ b/src/subdomains/supporting/support-issue/enums/support-issue.enum.ts @@ -9,6 +9,7 @@ export enum SupportIssueInternalState { export enum SupportIssueType { GENERIC_ISSUE = 'GenericIssue', TRANSACTION_ISSUE = 'TransactionIssue', + VERIFICATION_CALL = 'VerificationCall', KYC_ISSUE = 'KycIssue', LIMIT_REQUEST = 'LimitRequest', PARTNERSHIP_REQUEST = 'PartnershipRequest', @@ -24,6 +25,11 @@ export enum SupportIssueReason { FUNDS_NOT_RECEIVED = 'FundsNotReceived', TRANSACTION_MISSING = 'TransactionMissing', + // verification call + REJECT_CALL = 'RejectCall', + REPEAT_CALL = 'RepeatCall', + CALL_TIME = 'CallTime', + // notification of changes issue NAME_CHANGED = 'NameChanged', ADDRESS_CHANGED = 'AddressChanged', From 1963295bdd803937ae51fd52e6691f692b1a3253 Mon Sep 17 00:00:00 2001 From: Yannick1712 <52333989+Yannick1712@users.noreply.github.com> Date: Sun, 1 Feb 2026 14:30:57 +0100 Subject: [PATCH 2/9] [DEV-4546] Set department --- .../support-issue/services/support-issue.service.ts | 5 +---- .../support-issue/support-issue.controller.ts | 10 +++++++++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/subdomains/supporting/support-issue/services/support-issue.service.ts b/src/subdomains/supporting/support-issue/services/support-issue.service.ts index ad29ba869a..c407540201 100644 --- a/src/subdomains/supporting/support-issue/services/support-issue.service.ts +++ b/src/subdomains/supporting/support-issue/services/support-issue.service.ts @@ -25,7 +25,6 @@ import { SupportIssueDto, SupportMessageDto } from '../dto/support-issue.dto'; import { UpdateSupportIssueDto } from '../dto/update-support-issue.dto'; import { SupportIssue } from '../entities/support-issue.entity'; import { CustomerAuthor, SupportMessage } from '../entities/support-message.entity'; -import { Department } from '../enums/department.enum'; import { SupportIssueInternalState } from '../enums/support-issue.enum'; import { SupportLogType } from '../enums/support-log.enum'; import { SupportIssueRepository } from '../repositories/support-issue.repository'; @@ -142,10 +141,8 @@ export class SupportIssueService { } // create limit request - if (dto.limitRequest) { - newIssue.department = Department.COMPLIANCE; + if (dto.limitRequest) newIssue.limitRequest = await this.limitRequestService.increaseLimitInternal(dto.limitRequest, userData); - } } const entity = existingIssue ?? (await this.supportIssueRepo.save(newIssue)); diff --git a/src/subdomains/supporting/support-issue/support-issue.controller.ts b/src/subdomains/supporting/support-issue/support-issue.controller.ts index 3280a14232..ed68ac3406 100644 --- a/src/subdomains/supporting/support-issue/support-issue.controller.ts +++ b/src/subdomains/supporting/support-issue/support-issue.controller.ts @@ -16,6 +16,7 @@ import { UpdateSupportIssueDto } from './dto/update-support-issue.dto'; import { SupportIssue } from './entities/support-issue.entity'; import { CustomerAuthor } from './entities/support-message.entity'; import { Department } from './enums/department.enum'; +import { SupportIssueType } from './enums/support-issue.enum'; import { SupportIssueService } from './services/support-issue.service'; @ApiTags('Support') @@ -30,7 +31,14 @@ export class SupportIssueController { @GetJwt() jwt: JwtPayload | undefined, @Body() dto: CreateSupportIssueDto, ): Promise { - const input: CreateSupportIssueDto = { ...dto, author: CustomerAuthor, department: Department.SUPPORT }; + const input: CreateSupportIssueDto = { + ...dto, + author: CustomerAuthor, + department: + dto.type === SupportIssueType.VERIFICATION_CALL || dto.limitRequest + ? Department.COMPLIANCE + : Department.SUPPORT, + }; return jwt?.account ? this.supportIssueService.createIssue(jwt.account, input) : this.supportIssueService.createTransactionRequestIssue(input); From 3f55127a1c70e6a5332e845ad198b9341d5a5e8d Mon Sep 17 00:00:00 2001 From: Yannick1712 <52333989+Yannick1712@users.noreply.github.com> Date: Tue, 3 Feb 2026 16:07:49 +0100 Subject: [PATCH 3/9] [DEV-4546] add phoneCallTimes and phoneCallStatus --- .../user/models/user-data/user-data.entity.ts | 8 +++++ .../user/models/user-data/user-data.enum.ts | 20 ++++++++++++ .../user/models/user/dto/user-v2.dto.ts | 31 +++++++++++++++++-- .../user/models/user/user.controller.ts | 10 +++++- .../generic/user/models/user/user.service.ts | 18 +++++++++-- 5 files changed, 82 insertions(+), 5 deletions(-) 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 70c22c1cb2..38ecae6f5e 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 @@ -41,6 +41,8 @@ import { LegalEntity, LimitPeriod, Moderator, + PhoneCallPreferredTimes, + PhoneCallStatus, RiskStatus, SignatoryPower, UserDataStatus, @@ -243,6 +245,12 @@ export class UserData extends IEntity { @Column({ type: 'datetime2', nullable: true }) phoneCallIpCountryCheckDate?: Date; + @Column({ length: 256, nullable: true }) + phoneCallTimes: string; // PhoneCallPreferredTimes array + + @Column({ length: 256, nullable: true }) + phoneCallStatus: PhoneCallStatus; + @Column({ type: 'datetime2', nullable: true }) tradeApprovalDate?: Date; diff --git a/src/subdomains/generic/user/models/user-data/user-data.enum.ts b/src/subdomains/generic/user/models/user-data/user-data.enum.ts index 0a5e2f4aaa..d22621f1a1 100644 --- a/src/subdomains/generic/user/models/user-data/user-data.enum.ts +++ b/src/subdomains/generic/user/models/user-data/user-data.enum.ts @@ -15,6 +15,26 @@ export enum RiskStatus { RELEASED = 'Released', } +export enum PhoneCallPreferredTimes { + BETWEEN_9_AND_10 = 'Between9And10', + BETWEEN_10_AND_11 = 'Between10And11', + BETWEEN_11_AND_12 = 'Between11And12', + BETWEEN_12_AND_13 = 'Between12And13', + BETWEEN_13_AND_14 = 'Between13And14', + BETWEEN_14_AND_15 = 'Between14And15', + BETWEEN_15_AND_16 = 'Between15And16', + BETWEEN_9_AND_16 = 'Between9And16', +} + +export enum PhoneCallStatus { + REPEAT = 'Repeat', + DENIED = 'Denied', + UNAVAILABLE = 'Unavailable', + FAILED = 'Failed', + COMPLETED = 'Completed', + SUSPICIOUS = 'Suspicious', +} + export enum KycLevel { // automatic levels LEVEL_0 = 0, // nothing diff --git a/src/subdomains/generic/user/models/user/dto/user-v2.dto.ts b/src/subdomains/generic/user/models/user/dto/user-v2.dto.ts index 129bc761b6..7b7c59cc71 100644 --- a/src/subdomains/generic/user/models/user/dto/user-v2.dto.ts +++ b/src/subdomains/generic/user/models/user/dto/user-v2.dto.ts @@ -1,6 +1,14 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Type } from 'class-transformer'; -import { IsNotEmptyObject, ValidateNested } from 'class-validator'; +import { + IsBoolean, + IsEnum, + IsNotEmpty, + IsNotEmptyObject, + IsOptional, + ValidateIf, + ValidateNested, +} from 'class-validator'; import { Blockchain } from 'src/integration/blockchain/shared/enums/blockchain.enum'; import { EntityDto } from 'src/shared/dto/entity.dto'; import { Asset } from 'src/shared/models/asset/asset.entity'; @@ -9,7 +17,7 @@ import { FiatDto } from 'src/shared/models/fiat/dto/fiat.dto'; import { LanguageDto } from 'src/shared/models/language/dto/language.dto'; import { HistoryFilterKey } from 'src/subdomains/core/history/dto/history-filter.dto'; import { AccountType } from '../../user-data/account-type.enum'; -import { KycLevel } from '../../user-data/user-data.enum'; +import { KycLevel, PhoneCallPreferredTimes } from '../../user-data/user-data.enum'; import { TradingLimit, VolumeInformation } from './user.dto'; export class VolumesDto { @@ -57,6 +65,25 @@ export class UpdateRefDto { payoutAsset: Asset; } +export class UpdatePhoneCallDto { + @ApiPropertyOptional({ type: String, isArray: true }) + @IsOptional() + @IsEnum(PhoneCallPreferredTimes) + preferredTimes?: PhoneCallPreferredTimes[]; + + @ApiPropertyOptional() + @IsNotEmpty() + @ValidateIf((a: UpdatePhoneCallDto) => Boolean(a.denyCall || !a.repeatCall)) + @IsBoolean() + denyCall?: boolean; + + @ApiPropertyOptional() + @IsNotEmpty() + @ValidateIf((a: UpdatePhoneCallDto) => Boolean(a.repeatCall || !a.denyCall)) + @IsBoolean() + repeatCall?: boolean; +} + export class UserAddressDto { @ApiProperty() wallet: string; diff --git a/src/subdomains/generic/user/models/user/user.controller.ts b/src/subdomains/generic/user/models/user/user.controller.ts index c6ea8037a4..7a1501cede 100644 --- a/src/subdomains/generic/user/models/user/user.controller.ts +++ b/src/subdomains/generic/user/models/user/user.controller.ts @@ -46,7 +46,7 @@ import { UpdateUserInternalDto } from './dto/update-user-admin.dto'; import { UpdateUserDto, UpdateUserMailDto } from './dto/update-user.dto'; import { UserNameDto } from './dto/user-name.dto'; import { UserProfileDto } from './dto/user-profile.dto'; -import { ReferralDto, UpdateRefDto, UserV2Dto } from './dto/user-v2.dto'; +import { ReferralDto, UpdatePhoneCallDto, UpdateRefDto, UserV2Dto } from './dto/user-v2.dto'; import { UserDetailDto, UserDto } from './dto/user.dto'; import { UpdateMailStatus, VerifyMailDto } from './dto/verify-mail.dto'; import { VolumeQuery } from './dto/volume-query.dto'; @@ -334,4 +334,12 @@ export class UserV2Controller { async getProfile(@GetJwt() jwt: JwtPayload): Promise { return this.userService.getUserProfile(jwt.account); } + + @Put('phoneCall') + @ApiBearerAuth() + @UseGuards(AuthGuard(), RoleGuard(UserRole.ACCOUNT), UserActiveGuard()) + @ApiOkResponse({ type: ReferralDto }) + async updatePhoneCall(@GetJwt() jwt: JwtPayload, @Body() dto: UpdatePhoneCallDto): Promise { + return this.userService.updatePhoneCall(jwt.account, dto); + } } diff --git a/src/subdomains/generic/user/models/user/user.service.ts b/src/subdomains/generic/user/models/user/user.service.ts index c848b77a2e..014cc97efa 100644 --- a/src/subdomains/generic/user/models/user/user.service.ts +++ b/src/subdomains/generic/user/models/user/user.service.ts @@ -32,7 +32,7 @@ import { PaymentMethod } from 'src/subdomains/supporting/payment/dto/payment-met import { FeeService } from 'src/subdomains/supporting/payment/services/fee.service'; import { Between, FindOptionsRelations, Not } from 'typeorm'; import { UserData } from '../user-data/user-data.entity'; -import { KycLevel, KycState, KycType, Moderator, UserDataStatus } from '../user-data/user-data.enum'; +import { KycLevel, KycState, KycType, Moderator, PhoneCallStatus, UserDataStatus } from '../user-data/user-data.enum'; import { UserDataRepository } from '../user-data/user-data.repository'; import { WalletService } from '../wallet/wallet.service'; import { LinkedUserOutDto } from './dto/linked-user.dto'; @@ -43,7 +43,7 @@ import { UpdateUserDto, UpdateUserMailDto } from './dto/update-user.dto'; import { UserDtoMapper } from './dto/user-dto.mapper'; import { UserNameDto } from './dto/user-name.dto'; import { UserProfileDto } from './dto/user-profile.dto'; -import { ReferralDto, UpdateRefDto, UserV2Dto } from './dto/user-v2.dto'; +import { ReferralDto, UpdatePhoneCallDto, UpdateRefDto, UserV2Dto } from './dto/user-v2.dto'; import { UserDetailDto, UserDetails } from './dto/user.dto'; import { UpdateMailStatus } from './dto/verify-mail.dto'; import { VolumeQuery } from './dto/volume-query.dto'; @@ -206,6 +206,20 @@ export class UserService { return this.mapRefDtoV2(savedUser); } + async updatePhoneCall(userDataId: number, dto: UpdatePhoneCallDto): Promise { + const userData = await this.userDataRepo.findOne({ where: { id: userDataId } }); + if (!userData) throw new NotFoundException('Account not found'); + if (userData.phoneCallStatus && (dto.denyCall || dto.repeatCall)) + throw new BadRequestException('Phone call status is already set'); + + const update: Partial = { + phoneCallTimes: dto.preferredTimes ? dto.preferredTimes.join(';') : undefined, + phoneCallStatus: dto.denyCall ? PhoneCallStatus.DENIED : dto.repeatCall ? PhoneCallStatus.REPEAT : undefined, + }; + + await this.userDataRepo.update(userData.id, update); + } + private async mapRefDtoV2(user: User): Promise { const { refCount, refCountActive } = await this.getRefUserCounts(user); const payoutAsset = From 0de0ad79e5714df9c91d0ac8748605e087131325 Mon Sep 17 00:00:00 2001 From: Yannick1712 <52333989+Yannick1712@users.noreply.github.com> Date: Tue, 3 Feb 2026 16:09:11 +0100 Subject: [PATCH 4/9] [DEV-4546] remove unused code --- .../supporting/support-issue/services/support-issue.service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/subdomains/supporting/support-issue/services/support-issue.service.ts b/src/subdomains/supporting/support-issue/services/support-issue.service.ts index 18ffbd993b..83e3b403fe 100644 --- a/src/subdomains/supporting/support-issue/services/support-issue.service.ts +++ b/src/subdomains/supporting/support-issue/services/support-issue.service.ts @@ -26,7 +26,6 @@ import { SupportIssueDto, SupportIssueInternalDataDto, SupportMessageDto } from import { UpdateSupportIssueDto } from '../dto/update-support-issue.dto'; import { SupportIssue } from '../entities/support-issue.entity'; import { AutoResponder, CustomerAuthor, SupportMessage } from '../entities/support-message.entity'; -import { Department } from '../enums/department.enum'; import { SupportIssueInternalState } from '../enums/support-issue.enum'; import { SupportLogType } from '../enums/support-log.enum'; import { SupportIssueRepository } from '../repositories/support-issue.repository'; From 2249e59d1aa7be8d964187c32e69e4016e2dfc42 Mon Sep 17 00:00:00 2001 From: Yannick1712 <52333989+Yannick1712@users.noreply.github.com> Date: Tue, 3 Feb 2026 16:12:59 +0100 Subject: [PATCH 5/9] [DEV-4546] remove more unused code --- .../supporting/support-issue/services/support-issue.service.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/subdomains/supporting/support-issue/services/support-issue.service.ts b/src/subdomains/supporting/support-issue/services/support-issue.service.ts index 83e3b403fe..3f50e8b6af 100644 --- a/src/subdomains/supporting/support-issue/services/support-issue.service.ts +++ b/src/subdomains/supporting/support-issue/services/support-issue.service.ts @@ -7,7 +7,6 @@ import { } from '@nestjs/common'; import { Config } from 'src/config/config'; import { BlobContent } from 'src/integration/infrastructure/azure-storage.service'; -import { FiatService } from 'src/shared/models/fiat/fiat.service'; import { Util } from 'src/shared/utils/util'; import { ContentType } from 'src/subdomains/generic/kyc/enums/content-type.enum'; import { BankDataService } from 'src/subdomains/generic/user/models/bank-data/bank-data.service'; @@ -48,7 +47,6 @@ export class SupportIssueService { private readonly transactionRequestService: TransactionRequestService, private readonly supportLogService: SupportLogService, private readonly bankDataService: BankDataService, - private readonly fiatService: FiatService, ) {} async createTransactionRequestIssue(dto: CreateSupportIssueBaseDto): Promise { From b0804a4349e11f35104b58d0b78e4731d400bdb2 Mon Sep 17 00:00:00 2001 From: Yannick1712 <52333989+Yannick1712@users.noreply.github.com> Date: Tue, 3 Feb 2026 16:21:26 +0100 Subject: [PATCH 6/9] [DEV-4546] auto set PhoneCallStatus with supportIssue --- .../user/models/user-data/user-data.enum.ts | 2 +- .../user/models/user/dto/user-v2.dto.ts | 6 +++--- .../generic/user/models/user/user.service.ts | 4 ++-- .../support-issue/enums/support-issue.enum.ts | 2 +- .../services/support-issue.service.ts | 18 +++++++++++++++++- 5 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/subdomains/generic/user/models/user-data/user-data.enum.ts b/src/subdomains/generic/user/models/user-data/user-data.enum.ts index d22621f1a1..b06375bb3a 100644 --- a/src/subdomains/generic/user/models/user-data/user-data.enum.ts +++ b/src/subdomains/generic/user/models/user-data/user-data.enum.ts @@ -28,7 +28,7 @@ export enum PhoneCallPreferredTimes { export enum PhoneCallStatus { REPEAT = 'Repeat', - DENIED = 'Denied', + REJECTED = 'Rejected', UNAVAILABLE = 'Unavailable', FAILED = 'Failed', COMPLETED = 'Completed', diff --git a/src/subdomains/generic/user/models/user/dto/user-v2.dto.ts b/src/subdomains/generic/user/models/user/dto/user-v2.dto.ts index 7b7c59cc71..b2184269be 100644 --- a/src/subdomains/generic/user/models/user/dto/user-v2.dto.ts +++ b/src/subdomains/generic/user/models/user/dto/user-v2.dto.ts @@ -73,13 +73,13 @@ export class UpdatePhoneCallDto { @ApiPropertyOptional() @IsNotEmpty() - @ValidateIf((a: UpdatePhoneCallDto) => Boolean(a.denyCall || !a.repeatCall)) + @ValidateIf((a: UpdatePhoneCallDto) => Boolean(a.rejectCall || !a.repeatCall)) @IsBoolean() - denyCall?: boolean; + rejectCall?: boolean; @ApiPropertyOptional() @IsNotEmpty() - @ValidateIf((a: UpdatePhoneCallDto) => Boolean(a.repeatCall || !a.denyCall)) + @ValidateIf((a: UpdatePhoneCallDto) => Boolean(a.repeatCall || !a.rejectCall)) @IsBoolean() repeatCall?: boolean; } diff --git a/src/subdomains/generic/user/models/user/user.service.ts b/src/subdomains/generic/user/models/user/user.service.ts index 014cc97efa..1430e2d444 100644 --- a/src/subdomains/generic/user/models/user/user.service.ts +++ b/src/subdomains/generic/user/models/user/user.service.ts @@ -209,12 +209,12 @@ export class UserService { async updatePhoneCall(userDataId: number, dto: UpdatePhoneCallDto): Promise { const userData = await this.userDataRepo.findOne({ where: { id: userDataId } }); if (!userData) throw new NotFoundException('Account not found'); - if (userData.phoneCallStatus && (dto.denyCall || dto.repeatCall)) + if (userData.phoneCallStatus && (dto.rejectCall || dto.repeatCall)) throw new BadRequestException('Phone call status is already set'); const update: Partial = { phoneCallTimes: dto.preferredTimes ? dto.preferredTimes.join(';') : undefined, - phoneCallStatus: dto.denyCall ? PhoneCallStatus.DENIED : dto.repeatCall ? PhoneCallStatus.REPEAT : undefined, + phoneCallStatus: dto.rejectCall ? PhoneCallStatus.REJECTED : dto.repeatCall ? PhoneCallStatus.REPEAT : undefined, }; await this.userDataRepo.update(userData.id, update); diff --git a/src/subdomains/supporting/support-issue/enums/support-issue.enum.ts b/src/subdomains/supporting/support-issue/enums/support-issue.enum.ts index 794dfae7eb..e80cc9cf15 100644 --- a/src/subdomains/supporting/support-issue/enums/support-issue.enum.ts +++ b/src/subdomains/supporting/support-issue/enums/support-issue.enum.ts @@ -28,7 +28,7 @@ export enum SupportIssueReason { // verification call REJECT_CALL = 'RejectCall', REPEAT_CALL = 'RepeatCall', - CALL_TIME = 'CallTime', + PREFERRED_CALL_TIME = 'PreferredCallTime', // notification of changes issue NAME_CHANGED = 'NameChanged', diff --git a/src/subdomains/supporting/support-issue/services/support-issue.service.ts b/src/subdomains/supporting/support-issue/services/support-issue.service.ts index 3f50e8b6af..00b353c391 100644 --- a/src/subdomains/supporting/support-issue/services/support-issue.service.ts +++ b/src/subdomains/supporting/support-issue/services/support-issue.service.ts @@ -11,6 +11,7 @@ import { Util } from 'src/shared/utils/util'; import { ContentType } from 'src/subdomains/generic/kyc/enums/content-type.enum'; import { BankDataService } from 'src/subdomains/generic/user/models/bank-data/bank-data.service'; import { UserData } from 'src/subdomains/generic/user/models/user-data/user-data.entity'; +import { PhoneCallStatus } from 'src/subdomains/generic/user/models/user-data/user-data.enum'; import { UserDataService } from 'src/subdomains/generic/user/models/user-data/user-data.service'; import { FindOptionsWhere, In, IsNull, MoreThan, Not } from 'typeorm'; import { TransactionRequestType } from '../../payment/entities/transaction-request.entity'; @@ -25,7 +26,7 @@ import { SupportIssueDto, SupportIssueInternalDataDto, SupportMessageDto } from import { UpdateSupportIssueDto } from '../dto/update-support-issue.dto'; import { SupportIssue } from '../entities/support-issue.entity'; import { AutoResponder, CustomerAuthor, SupportMessage } from '../entities/support-message.entity'; -import { SupportIssueInternalState } from '../enums/support-issue.enum'; +import { SupportIssueInternalState, SupportIssueReason, SupportIssueType } from '../enums/support-issue.enum'; import { SupportLogType } from '../enums/support-log.enum'; import { SupportIssueRepository } from '../repositories/support-issue.repository'; import { SupportMessageRepository } from '../repositories/support-message.repository'; @@ -143,6 +144,21 @@ export class SupportIssueService { // create limit request if (dto.limitRequest) newIssue.limitRequest = await this.limitRequestService.increaseLimitInternal(dto.limitRequest, userData); + + if ( + !userData.phoneCallStatus && + dto.type === SupportIssueType.VERIFICATION_CALL && + [SupportIssueReason.REJECT_CALL, SupportIssueReason.REPEAT_CALL].includes(dto.reason) + ) { + await this.userDataService.updateUserDataInternal(userData, { + phoneCallStatus: + dto.reason === SupportIssueReason.REJECT_CALL + ? PhoneCallStatus.REJECTED + : dto.reason === SupportIssueReason.REPEAT_CALL + ? PhoneCallStatus.REPEAT + : undefined, + }); + } } const entity = existingIssue ?? (await this.supportIssueRepo.save(newIssue)); From 60f757c22ecd77820ad3172083de262d9e6f8755 Mon Sep 17 00:00:00 2001 From: Yannick1712 <52333989+Yannick1712@users.noreply.github.com> Date: Sat, 7 Feb 2026 14:11:11 +0100 Subject: [PATCH 7/9] [DEV-4546] Refactoring --- .../user/models/user-data/user-data.entity.ts | 3 +- .../user/models/user-data/user-data.enum.ts | 2 +- .../models/user-data/user-data.service.ts | 3 ++ .../user/models/user/dto/update-user.dto.ts | 30 +++++++++++++++++- .../user/models/user/dto/user-v2.dto.ts | 31 ++----------------- .../user/models/user/user.controller.ts | 10 +----- .../generic/user/models/user/user.service.ts | 18 ++--------- 7 files changed, 40 insertions(+), 57 deletions(-) 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 38ecae6f5e..74c0c875e6 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 @@ -41,7 +41,6 @@ import { LegalEntity, LimitPeriod, Moderator, - PhoneCallPreferredTimes, PhoneCallStatus, RiskStatus, SignatoryPower, @@ -504,6 +503,8 @@ export class UserData extends IEntity { phone: dto.phone ?? this.phone, language: dto.language ?? this.language, currency: dto.currency ?? this.currency, + phoneCallTimes: dto.preferredTimes ? dto.preferredTimes.join(';') : undefined, + phoneCallStatus: dto.rejectCall ? PhoneCallStatus.REJECTED : dto.repeatCall ? PhoneCallStatus.REPEAT : undefined, }; Object.assign(this, update); diff --git a/src/subdomains/generic/user/models/user-data/user-data.enum.ts b/src/subdomains/generic/user/models/user-data/user-data.enum.ts index b06375bb3a..8a41c236ca 100644 --- a/src/subdomains/generic/user/models/user-data/user-data.enum.ts +++ b/src/subdomains/generic/user/models/user-data/user-data.enum.ts @@ -15,7 +15,7 @@ export enum RiskStatus { RELEASED = 'Released', } -export enum PhoneCallPreferredTimes { +export enum PhoneCallPreferredTime { BETWEEN_9_AND_10 = 'Between9And10', BETWEEN_10_AND_11 = 'Between10And11', BETWEEN_11_AND_12 = 'Between11And12', diff --git a/src/subdomains/generic/user/models/user-data/user-data.service.ts b/src/subdomains/generic/user/models/user-data/user-data.service.ts index 5b15ec7c29..ff08c2aa1e 100644 --- a/src/subdomains/generic/user/models/user-data/user-data.service.ts +++ b/src/subdomains/generic/user/models/user-data/user-data.service.ts @@ -687,6 +687,9 @@ export class UserDataService { if (!dto.currency) throw new BadRequestException('Currency not found'); } + if (userData.phoneCallStatus && (dto.rejectCall || dto.repeatCall)) + throw new BadRequestException('Phone call status is already set'); + const phoneChanged = dto.phone && dto.phone !== userData.phone; const updateSiftAccount: CreateAccount = { $time: Date.now() }; diff --git a/src/subdomains/generic/user/models/user/dto/update-user.dto.ts b/src/subdomains/generic/user/models/user/dto/update-user.dto.ts index 444db51b6b..bf316fc422 100644 --- a/src/subdomains/generic/user/models/user/dto/update-user.dto.ts +++ b/src/subdomains/generic/user/models/user/dto/update-user.dto.ts @@ -1,11 +1,22 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Transform, Type } from 'class-transformer'; -import { IsEmail, IsNotEmpty, IsObject, IsOptional, IsString, ValidateNested } from 'class-validator'; +import { + IsBoolean, + IsEmail, + IsEnum, + IsNotEmpty, + IsObject, + IsOptional, + IsString, + ValidateIf, + ValidateNested, +} from 'class-validator'; import { EntityDto } from 'src/shared/dto/entity.dto'; import { Fiat } from 'src/shared/models/fiat/fiat.entity'; import { Language } from 'src/shared/models/language/language.entity'; import { Util } from 'src/shared/utils/util'; import { DfxPhoneTransform, IsDfxPhone } from '../../user-data/is-dfx-phone.validator'; +import { PhoneCallPreferredTime } from '../../user-data/user-data.enum'; export class UpdateUserDto { @ApiPropertyOptional() @@ -28,6 +39,23 @@ export class UpdateUserDto { @ValidateNested() @Type(() => EntityDto) currency?: Fiat; + + @ApiPropertyOptional({ type: String, isArray: true }) + @IsOptional() + @IsEnum(PhoneCallPreferredTime) + preferredTimes?: PhoneCallPreferredTime[]; + + @ApiPropertyOptional() + @IsNotEmpty() + @ValidateIf((a: UpdateUserDto) => Boolean(a.rejectCall || !a.repeatCall)) + @IsBoolean() + rejectCall?: boolean; + + @ApiPropertyOptional() + @IsNotEmpty() + @ValidateIf((a: UpdateUserDto) => Boolean(a.repeatCall || !a.rejectCall)) + @IsBoolean() + repeatCall?: boolean; } export class UpdateUserMailDto { diff --git a/src/subdomains/generic/user/models/user/dto/user-v2.dto.ts b/src/subdomains/generic/user/models/user/dto/user-v2.dto.ts index b2184269be..129bc761b6 100644 --- a/src/subdomains/generic/user/models/user/dto/user-v2.dto.ts +++ b/src/subdomains/generic/user/models/user/dto/user-v2.dto.ts @@ -1,14 +1,6 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Type } from 'class-transformer'; -import { - IsBoolean, - IsEnum, - IsNotEmpty, - IsNotEmptyObject, - IsOptional, - ValidateIf, - ValidateNested, -} from 'class-validator'; +import { IsNotEmptyObject, ValidateNested } from 'class-validator'; import { Blockchain } from 'src/integration/blockchain/shared/enums/blockchain.enum'; import { EntityDto } from 'src/shared/dto/entity.dto'; import { Asset } from 'src/shared/models/asset/asset.entity'; @@ -17,7 +9,7 @@ import { FiatDto } from 'src/shared/models/fiat/dto/fiat.dto'; import { LanguageDto } from 'src/shared/models/language/dto/language.dto'; import { HistoryFilterKey } from 'src/subdomains/core/history/dto/history-filter.dto'; import { AccountType } from '../../user-data/account-type.enum'; -import { KycLevel, PhoneCallPreferredTimes } from '../../user-data/user-data.enum'; +import { KycLevel } from '../../user-data/user-data.enum'; import { TradingLimit, VolumeInformation } from './user.dto'; export class VolumesDto { @@ -65,25 +57,6 @@ export class UpdateRefDto { payoutAsset: Asset; } -export class UpdatePhoneCallDto { - @ApiPropertyOptional({ type: String, isArray: true }) - @IsOptional() - @IsEnum(PhoneCallPreferredTimes) - preferredTimes?: PhoneCallPreferredTimes[]; - - @ApiPropertyOptional() - @IsNotEmpty() - @ValidateIf((a: UpdatePhoneCallDto) => Boolean(a.rejectCall || !a.repeatCall)) - @IsBoolean() - rejectCall?: boolean; - - @ApiPropertyOptional() - @IsNotEmpty() - @ValidateIf((a: UpdatePhoneCallDto) => Boolean(a.repeatCall || !a.rejectCall)) - @IsBoolean() - repeatCall?: boolean; -} - export class UserAddressDto { @ApiProperty() wallet: string; diff --git a/src/subdomains/generic/user/models/user/user.controller.ts b/src/subdomains/generic/user/models/user/user.controller.ts index 7a1501cede..c6ea8037a4 100644 --- a/src/subdomains/generic/user/models/user/user.controller.ts +++ b/src/subdomains/generic/user/models/user/user.controller.ts @@ -46,7 +46,7 @@ import { UpdateUserInternalDto } from './dto/update-user-admin.dto'; import { UpdateUserDto, UpdateUserMailDto } from './dto/update-user.dto'; import { UserNameDto } from './dto/user-name.dto'; import { UserProfileDto } from './dto/user-profile.dto'; -import { ReferralDto, UpdatePhoneCallDto, UpdateRefDto, UserV2Dto } from './dto/user-v2.dto'; +import { ReferralDto, UpdateRefDto, UserV2Dto } from './dto/user-v2.dto'; import { UserDetailDto, UserDto } from './dto/user.dto'; import { UpdateMailStatus, VerifyMailDto } from './dto/verify-mail.dto'; import { VolumeQuery } from './dto/volume-query.dto'; @@ -334,12 +334,4 @@ export class UserV2Controller { async getProfile(@GetJwt() jwt: JwtPayload): Promise { return this.userService.getUserProfile(jwt.account); } - - @Put('phoneCall') - @ApiBearerAuth() - @UseGuards(AuthGuard(), RoleGuard(UserRole.ACCOUNT), UserActiveGuard()) - @ApiOkResponse({ type: ReferralDto }) - async updatePhoneCall(@GetJwt() jwt: JwtPayload, @Body() dto: UpdatePhoneCallDto): Promise { - return this.userService.updatePhoneCall(jwt.account, dto); - } } diff --git a/src/subdomains/generic/user/models/user/user.service.ts b/src/subdomains/generic/user/models/user/user.service.ts index 1430e2d444..c848b77a2e 100644 --- a/src/subdomains/generic/user/models/user/user.service.ts +++ b/src/subdomains/generic/user/models/user/user.service.ts @@ -32,7 +32,7 @@ import { PaymentMethod } from 'src/subdomains/supporting/payment/dto/payment-met import { FeeService } from 'src/subdomains/supporting/payment/services/fee.service'; import { Between, FindOptionsRelations, Not } from 'typeorm'; import { UserData } from '../user-data/user-data.entity'; -import { KycLevel, KycState, KycType, Moderator, PhoneCallStatus, UserDataStatus } from '../user-data/user-data.enum'; +import { KycLevel, KycState, KycType, Moderator, UserDataStatus } from '../user-data/user-data.enum'; import { UserDataRepository } from '../user-data/user-data.repository'; import { WalletService } from '../wallet/wallet.service'; import { LinkedUserOutDto } from './dto/linked-user.dto'; @@ -43,7 +43,7 @@ import { UpdateUserDto, UpdateUserMailDto } from './dto/update-user.dto'; import { UserDtoMapper } from './dto/user-dto.mapper'; import { UserNameDto } from './dto/user-name.dto'; import { UserProfileDto } from './dto/user-profile.dto'; -import { ReferralDto, UpdatePhoneCallDto, UpdateRefDto, UserV2Dto } from './dto/user-v2.dto'; +import { ReferralDto, UpdateRefDto, UserV2Dto } from './dto/user-v2.dto'; import { UserDetailDto, UserDetails } from './dto/user.dto'; import { UpdateMailStatus } from './dto/verify-mail.dto'; import { VolumeQuery } from './dto/volume-query.dto'; @@ -206,20 +206,6 @@ export class UserService { return this.mapRefDtoV2(savedUser); } - async updatePhoneCall(userDataId: number, dto: UpdatePhoneCallDto): Promise { - const userData = await this.userDataRepo.findOne({ where: { id: userDataId } }); - if (!userData) throw new NotFoundException('Account not found'); - if (userData.phoneCallStatus && (dto.rejectCall || dto.repeatCall)) - throw new BadRequestException('Phone call status is already set'); - - const update: Partial = { - phoneCallTimes: dto.preferredTimes ? dto.preferredTimes.join(';') : undefined, - phoneCallStatus: dto.rejectCall ? PhoneCallStatus.REJECTED : dto.repeatCall ? PhoneCallStatus.REPEAT : undefined, - }; - - await this.userDataRepo.update(userData.id, update); - } - private async mapRefDtoV2(user: User): Promise { const { refCount, refCountActive } = await this.getRefUserCounts(user); const payoutAsset = From b200e42caabad807d55c495ecd7ca181f3f39184 Mon Sep 17 00:00:00 2001 From: Yannick1712 <52333989+Yannick1712@users.noreply.github.com> Date: Sat, 7 Feb 2026 14:15:29 +0100 Subject: [PATCH 8/9] [DEV-4546] add migration --- ...0071165-AddUserDataVerificationCallTime.js | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 migration/1770470071165-AddUserDataVerificationCallTime.js diff --git a/migration/1770470071165-AddUserDataVerificationCallTime.js b/migration/1770470071165-AddUserDataVerificationCallTime.js new file mode 100644 index 0000000000..48ae8a5240 --- /dev/null +++ b/migration/1770470071165-AddUserDataVerificationCallTime.js @@ -0,0 +1,28 @@ +/** + * @typedef {import('typeorm').MigrationInterface} MigrationInterface + * @typedef {import('typeorm').QueryRunner} QueryRunner + */ + +/** + * @class + * @implements {MigrationInterface} + */ +module.exports = class AddUserDataVerificationCallTime1770470071165 { + name = 'AddUserDataVerificationCallTime1770470071165' + + /** + * @param {QueryRunner} queryRunner + */ + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_data" ADD "phoneCallTimes" nvarchar(256)`); + await queryRunner.query(`ALTER TABLE "user_data" ADD "phoneCallStatus" nvarchar(256)`); + } + + /** + * @param {QueryRunner} queryRunner + */ + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_data" DROP COLUMN "phoneCallStatus"`); + await queryRunner.query(`ALTER TABLE "user_data" DROP COLUMN "phoneCallTimes"`); + } +} From 693af3c342160c2f17bab66f7aff9cf1380b2a12 Mon Sep 17 00:00:00 2001 From: Yannick1712 <52333989+Yannick1712@users.noreply.github.com> Date: Sat, 7 Feb 2026 15:16:25 +0100 Subject: [PATCH 9/9] [DEV-4546] add new amlReason --- src/integration/sift/dto/sift.dto.ts | 1 + src/shared/i18n/de/mail.json | 13 ++++--- src/shared/i18n/en/mail.json | 13 ++++--- src/shared/i18n/es/mail.json | 13 ++++--- src/shared/i18n/fr/mail.json | 13 ++++--- src/shared/i18n/it/mail.json | 13 ++++--- src/shared/i18n/pt/mail.json | 13 ++++--- .../core/aml/enums/aml-error.enum.ts | 6 +++ .../core/aml/enums/aml-reason.enum.ts | 1 + .../core/aml/services/aml-helper.service.ts | 37 ++++++++++++++++--- .../supporting/payment/dto/transaction.dto.ts | 1 + 11 files changed, 82 insertions(+), 42 deletions(-) diff --git a/src/integration/sift/dto/sift.dto.ts b/src/integration/sift/dto/sift.dto.ts index 843a139f0b..b566de0c69 100644 --- a/src/integration/sift/dto/sift.dto.ts +++ b/src/integration/sift/dto/sift.dto.ts @@ -1034,6 +1034,7 @@ export const SiftAmlDeclineMap: { [method in AmlReason]: DeclineCategory } = { [AmlReason.MANUAL_CHECK_PHONE]: DeclineCategory.RISKY, [AmlReason.MANUAL_CHECK_IP_PHONE]: DeclineCategory.RISKY, [AmlReason.MANUAL_CHECK_IP_COUNTRY_PHONE]: DeclineCategory.RISKY, + [AmlReason.MANUAL_CHECK_PHONE_REJECTED]: DeclineCategory.RISKY, [AmlReason.BANK_RELEASE_PENDING]: DeclineCategory.OTHER, [AmlReason.VIRTUAL_IBAN_USER_MISMATCH]: DeclineCategory.RISKY, [AmlReason.INTERMEDIARY_WITHOUT_SENDER]: DeclineCategory.RISKY, diff --git a/src/shared/i18n/de/mail.json b/src/shared/i18n/de/mail.json index aaa27b0fe0..71fbcbf791 100644 --- a/src/shared/i18n/de/mail.json +++ b/src/shared/i18n/de/mail.json @@ -91,9 +91,10 @@ "test_only": "Test", "kyc_data_needed": "Für die Transaktion werden deine KYC Daten benötigt", "bank_tx_needed": "Um diese Transaktion auszuführen ist zuvor eine Banktransaktion erforderlich", - "manual_check_phone": "Wir konnten dich unter deiner angegebenen Telefonnummer nicht erreichen", - "manual_check_ip_phone": "Wir konnten dich unter deiner angegebenen Telefonnummer nicht erreichen", - "manual_check_ip_country_phone": "Wir konnten dich unter deiner angegebenen Telefonnummer nicht erreichen", + "manual_check_phone": "Wir konnten dich unter deiner angegebenen Telefonnummer nicht erreichen. Du kannst einen erneuten Anruf und eine bevorzugte Uhrzeit für das Telefongespräch selber hier beantragen: [url:https://app.dfx.swiss/settings]", + "manual_check_ip_phone": "Wir konnten dich unter deiner angegebenen Telefonnummer nicht erreichen. Du kannst einen erneuten Anruf und eine bevorzugte Uhrzeit für das Telefongespräch selber hier beantragen: [url:https://app.dfx.swiss/settings]", + "manual_check_ip_country_phone": "Wir konnten dich unter deiner angegebenen Telefonnummer nicht erreichen. Du kannst einen erneuten Anruf und eine bevorzugte Uhrzeit für das Telefongespräch selber hier beantragen: [url:https://app.dfx.swiss/settings]", + "manual_check_phone_rejected": "Das Telefonat war nicht erfolgreich oder wurde abgelehnt", "merge_incomplete": "Die Email Bestätigung wurde nicht akzeptiert", "intermediary_without_sender": "Die Absenderbank (Wise/Revolut) hat nur den Banknamen übermittelt, nicht aber den Namen des Kontoinhabers. DFX kann daher den tatsächlichen Absender nicht verifizieren und die Transaktion nicht verarbeiten.", "name_too_short": "Dein Name ist zu kurz für die Bankverarbeitung. Banken benötigen mindestens 4 Buchstaben im Namen des Kontoinhabers." @@ -215,7 +216,7 @@ "line1": "Wir haben deine Einzahlung erhalten.", "line2": "Wir werden uns in Kürze auf der Telefonnummer {phone} bei dir melden.", "line3": "Wenn alle Fragen geklärt sind, wird deine Transaktion automatisch weiterverarbeitet.", - "line4": "", + "line4": "Du kannst eine bevorzugte Uhrzeit für das Telefongespräch selber hier angeben: [url:https://app.dfx.swiss/settings]", "line5": "Wenn du stattdessen eine Rückzahlung anfordern möchtest:
[url:Klick hier]" }, "merge_incomplete": { @@ -242,7 +243,7 @@ "line1": "Wir haben deine Einzahlung erhalten.", "line2": "Wir werden uns in Kürze auf der Telefonnummer {phone} bei dir melden.", "line3": "Wenn alle Fragen geklärt sind, wird deine Transaktion automatisch weiterverarbeitet.", - "line4": "", + "line4": "Du kannst eine bevorzugte Uhrzeit für das Telefongespräch selber hier angeben: [url:https://app.dfx.swiss/settings]", "line5": "Wenn du stattdessen eine Rückzahlung anfordern möchtest:
[url:Klick hier]" }, "manual_check_ip_country_phone": { @@ -251,7 +252,7 @@ "line1": "Wir haben deine Einzahlung erhalten.", "line2": "Wir werden uns in Kürze auf der Telefonnummer {phone} bei dir melden.", "line3": "Wenn alle Fragen geklärt sind, wird deine Transaktion automatisch weiterverarbeitet.", - "line4": "", + "line4": "Du kannst eine bevorzugte Uhrzeit für das Telefongespräch selber hier angeben: [url:https://app.dfx.swiss/settings]", "line5": "Wenn du stattdessen eine Rückzahlung anfordern möchtest:
[url:Klick hier]" } }, diff --git a/src/shared/i18n/en/mail.json b/src/shared/i18n/en/mail.json index db40214c22..868cb87c2e 100644 --- a/src/shared/i18n/en/mail.json +++ b/src/shared/i18n/en/mail.json @@ -91,9 +91,10 @@ "test_only": "Test", "kyc_data_needed": "Your KYC data is required for the transaction", "bank_tx_needed": "A bank transaction is required before this transaction can be carried out", - "manual_check_phone": "We were unable to reach you at the phone number you provided", - "manual_check_ip_phone": "We were unable to reach you at the phone number you provided", - "manual_check_ip_country_phone": "We were unable to reach you at the phone number you provided", + "manual_check_phone": "We were unable to reach you at the phone number you provided. You can request a callback and a preferred time for the phone call yourself here: [url:https://app.dfx.swiss/settings]", + "manual_check_ip_phone": "We were unable to reach you at the phone number you provided. You can request a callback and a preferred time for the phone call yourself here: [url:https://app.dfx.swiss/settings]", + "manual_check_ip_country_phone": "We were unable to reach you at the phone number you provided. You can request a callback and a preferred time for the phone call yourself here: [url:https://app.dfx.swiss/settings]", + "manual_check_phone_rejected": "The phone call was unsuccessful or rejected", "merge_incomplete": "The email confirmation was not accepted", "intermediary_without_sender": "The sender bank (Wise/Revolut) only transmitted the bank name, not the account holder's name. DFX is therefore unable to verify the actual sender and cannot process the transaction.", "name_too_short": "Your name is too short for bank processing. Banks require at least 4 letters in the account holder name." @@ -215,7 +216,7 @@ "line1": "We have received your deposit.", "line2": "We will contact you shortly at {phone}.", "line3": "Once all questions have been clarified, your transaction will be processed automatically.", - "line4": "", + "line4": "You can specify your preferred time for the phone call here: [url:https://app.dfx.swiss/settings]", "line5": "If you would like to request a refund instead:
[url:click here]" }, "merge_incomplete": { @@ -242,7 +243,7 @@ "line1": "We have received your deposit.", "line2": "We will contact you shortly at {phone}.", "line3": "Once all questions have been clarified, your transaction will be processed automatically.", - "line4": "", + "line4": "You can specify your preferred time for the phone call here: [url:https://app.dfx.swiss/settings]", "line5": "If you would like to request a refund instead:
[url:click here]" }, "manual_check_ip_country_phone": { @@ -251,7 +252,7 @@ "line1": "We have received your deposit.", "line2": "We will contact you shortly at {phone}.", "line3": "Once all questions have been clarified, your transaction will be processed automatically.", - "line4": "", + "line4": "You can specify your preferred time for the phone call here: [url:https://app.dfx.swiss/settings]", "line5": "If you would like to request a refund instead:
[url:click here]" } }, diff --git a/src/shared/i18n/es/mail.json b/src/shared/i18n/es/mail.json index d1a3aa8c6e..76c11bded5 100644 --- a/src/shared/i18n/es/mail.json +++ b/src/shared/i18n/es/mail.json @@ -91,9 +91,10 @@ "test_only": "Prueba", "kyc_data_needed": "Sus datos KYC son necesarios para la transacción", "bank_tx_needed": "Para poder realizar esta operación es necesaria una transacción bancaria", - "manual_check_phone": "No hemos podido contactar con usted al número de teléfono que nos facilitó", - "manual_check_ip_phone": "No hemos podido contactar con usted al número de teléfono que nos facilitó", - "manual_check_ip_country_phone": "No hemos podido contactar con usted al número de teléfono que nos facilitó", + "manual_check_phone": "No hemos podido contactar con usted al número de teléfono que nos facilitó. Aquí puede solicitar una llamada y elegir la hora que prefiera para recibirla: [url:https://app.dfx.swiss/settings]", + "manual_check_ip_phone": "No hemos podido contactar con usted al número de teléfono que nos facilitó. Aquí puede solicitar una llamada y elegir la hora que prefiera para recibirla: [url:https://app.dfx.swiss/settings]", + "manual_check_ip_country_phone": "No hemos podido contactar con usted al número de teléfono que nos facilitó. Aquí puede solicitar una llamada y elegir la hora que prefiera para recibirla: [url:https://app.dfx.swiss/settings]", + "manual_check_phone_rejected": "La llamada telefónica no se ha podido realizar o ha sido rechazada.", "merge_incomplete": "El correo electrónico de confirmación no fue aceptado", "intermediary_without_sender": "El banco emisor (Wise/Revolut) solo transmitió el nombre del banco, no el nombre del titular de la cuenta. Por lo tanto, DFX no puede verificar el remitente real y no puede procesar la transacción.", "name_too_short": "Tu nombre es demasiado corto para el procesamiento bancario. Los bancos requieren al menos 4 letras en el nombre del titular de la cuenta." @@ -215,7 +216,7 @@ "line1": "Hemos recibido su depósito.", "line2": "Nos pondremos en contacto con usted en breve en el {phone}.", "line3": "Una vez que se hayan aclarado todas las preguntas, su transacción se procesará automáticamente.", - "line4": "", + "line4": "Aquí puede especificar la hora que prefiera para la llamada telefónica: [url:https://app.dfx.swiss/settings]", "line5": "Si desea solicitar un reembolso en su lugar:
[url:haga clic aquí]" }, "merge_incomplete": { @@ -242,7 +243,7 @@ "line1": "Hemos recibido su depósito.", "line2": "Nos pondremos en contacto con usted en breve en el {phone}.", "line3": "Una vez que se hayan aclarado todas las preguntas, su transacción se procesará automáticamente.", - "line4": "", + "line4": "Aquí puede especificar la hora que prefiera para la llamada telefónica: [url:https://app.dfx.swiss/settings]", "line5": "Si desea solicitar un reembolso en su lugar:
[url:haga clic aquí]" }, "manual_check_ip_country_phone": { @@ -251,7 +252,7 @@ "line1": "Hemos recibido su depósito.", "line2": "Nos pondremos en contacto con usted en breve en el {phone}.", "line3": "Una vez que se hayan aclarado todas las preguntas, su transacción se procesará automáticamente.", - "line4": "", + "line4": "Aquí puede especificar la hora que prefiera para la llamada telefónica: [url:https://app.dfx.swiss/settings]", "line5": "Si desea solicitar un reembolso en su lugar:
[url:haga clic aquí]" } }, diff --git a/src/shared/i18n/fr/mail.json b/src/shared/i18n/fr/mail.json index a4c7ef800d..a855e0cfc8 100644 --- a/src/shared/i18n/fr/mail.json +++ b/src/shared/i18n/fr/mail.json @@ -91,9 +91,10 @@ "test_only": "Test", "kyc_data_needed": "Vos données KYC sont nécessaires pour la transaction", "bank_tx_needed": "Une transaction bancaire est nécessaire pour que cette opération puisse être effectuée", - "manual_check_phone": "Nous n'avons pas réussi à vous joindre au numéro de téléphone que vous avez fourni", - "manual_check_ip_phone": "Nous n'avons pas réussi à vous joindre au numéro de téléphone que vous avez fourni", - "manual_check_ip_country_phone": "Nous n'avons pas réussi à vous joindre au numéro de téléphone que vous avez fourni", + "manual_check_phone": "Nous n'avons pas réussi à vous joindre au numéro de téléphone que vous avez fourni. Vous pouvez demander ici à être rappelé et indiquer l'heure à laquelle vous souhaitez recevoir l'appel: [url:https://app.dfx.swiss/settings]", + "manual_check_ip_phone": "Nous n'avons pas réussi à vous joindre au numéro de téléphone que vous avez fourni. Vous pouvez demander ici à être rappelé et indiquer l'heure à laquelle vous souhaitez recevoir l'appel: [url:https://app.dfx.swiss/settings]", + "manual_check_ip_country_phone": "Nous n'avons pas réussi à vous joindre au numéro de téléphone que vous avez fourni. Vous pouvez demander ici à être rappelé et indiquer l'heure à laquelle vous souhaitez recevoir l'appel: [url:https://app.dfx.swiss/settings]", + "manual_check_phone_rejected": "L'appel téléphonique n'a pas abouti ou a été rejeté", "merge_incomplete": "L'e-mail de confirmation n'a pas été accepté", "intermediary_without_sender": "La banque émettrice (Wise/Revolut) n'a transmis que le nom de la banque, et non le nom du titulaire du compte. DFX ne peut donc pas vérifier l'expéditeur réel et ne peut pas traiter la transaction.", "name_too_short": "Votre nom est trop court pour le traitement bancaire. Les banques exigent au moins 4 lettres dans le nom du titulaire du compte." @@ -215,7 +216,7 @@ "line1": "Nous avons bien reçu votre acompte.", "line2": "Nous vous contacterons sous peu au {phone}.", "line3": "Une fois toutes les questions clarifiées, votre transaction sera traitée automatiquement.", - "line4": "", + "line4": "Vous pouvez indiquer ici l'heure à laquelle vous souhaitez recevoir l'appel téléphonique: [url:https://app.dfx.swiss/settings]", "line5": "Si vous souhaitez demander un remboursement:
[url:cliquez ici]" }, "merge_incomplete": { @@ -242,7 +243,7 @@ "line1": "Nous avons bien reçu votre acompte.", "line2": "Nous vous contacterons sous peu au {phone}.", "line3": "Une fois toutes les questions clarifiées, votre transaction sera traitée automatiquement.", - "line4": "", + "line4": "Vous pouvez indiquer ici l'heure à laquelle vous souhaitez recevoir l'appel téléphonique: [url:https://app.dfx.swiss/settings]", "line5": "Si vous souhaitez demander un remboursement:
[url:cliquez ici]" }, "manual_check_ip_country_phone": { @@ -251,7 +252,7 @@ "line1": "Nous avons bien reçu votre acompte.", "line2": "Nous vous contacterons sous peu au {phone}.", "line3": "Une fois toutes les questions clarifiées, votre transaction sera traitée automatiquement.", - "line4": "", + "line4": "Vous pouvez indiquer ici l'heure à laquelle vous souhaitez recevoir l'appel téléphonique: [url:https://app.dfx.swiss/settings]", "line5": "Si vous souhaitez demander un remboursement:
[url:cliquez ici]" } }, diff --git a/src/shared/i18n/it/mail.json b/src/shared/i18n/it/mail.json index 1f341d742b..17143376c3 100644 --- a/src/shared/i18n/it/mail.json +++ b/src/shared/i18n/it/mail.json @@ -91,9 +91,10 @@ "test_only": "Test", "kyc_data_needed": "I dati KYC sono necessari per la transazione", "bank_tx_needed": "Per effettuare questa transazione è necessaria una transazione bancaria", - "manual_check_phone": "Non siamo riusciti a contattarti al numero di telefono che ci hai fornito", - "manual_check_ip_phone": "Non siamo riusciti a contattarti al numero di telefono che ci hai fornito", - "manual_check_ip_country_phone": "Non siamo riusciti a contattarti al numero di telefono che ci hai fornito", + "manual_check_phone": "Non siamo riusciti a contattarti al numero di telefono che ci hai fornito. Puoi richiedere tu stesso una richiamata e indicare l'orario che preferisci per la telefonata qui: [url:https://app.dfx.swiss/settings]", + "manual_check_ip_phone": "Non siamo riusciti a contattarti al numero di telefono che ci hai fornito. Puoi richiedere tu stesso una richiamata e indicare l'orario che preferisci per la telefonata qui: [url:https://app.dfx.swiss/settings]", + "manual_check_ip_country_phone": "Non siamo riusciti a contattarti al numero di telefono che ci hai fornito. Puoi richiedere tu stesso una richiamata e indicare l'orario che preferisci per la telefonata qui: [url:https://app.dfx.swiss/settings]", + "manual_check_phone_rejected": "La telefonata non è andata a buon fine o è stata rifiutata.", "merge_incomplete": "L'e-mail di conferma non è stata accettata", "intermediary_without_sender": "La banca mittente (Wise/Revolut) ha trasmesso solo il nome della banca, non il nome del titolare del conto. DFX non può quindi verificare il mittente effettivo e non può elaborare la transazione.", "name_too_short": "Il tuo nome è troppo corto per l'elaborazione bancaria. Le banche richiedono almeno 4 lettere nel nome del titolare del conto." @@ -215,7 +216,7 @@ "line1": "Abbiamo ricevuto il tuo deposito.", "line2": "Ti contatteremo a breve al numero {phone}.", "line3": "Una volta chiariti tutti i dubbi, la transazione verrà elaborata automaticamente.", - "line4": "", + "line4": "Qui puoi specificare l'orario che preferisci per la telefonata: [url:https://app.dfx.swiss/settings]", "line5": "Se invece si desidera richiedere un rimborso:
[url:clicca qui]" }, "merge_incomplete": { @@ -242,7 +243,7 @@ "line1": "Abbiamo ricevuto il tuo deposito.", "line2": "Ti contatteremo a breve al numero {phone}.", "line3": "Una volta chiariti tutti i dubbi, la transazione verrà elaborata automaticamente.", - "line4": "", + "line4": "Qui puoi specificare l'orario che preferisci per la telefonata: [url:https://app.dfx.swiss/settings]", "line5": "Se invece si desidera richiedere un rimborso:
[url:clicca qui]" }, "manual_check_ip_country_phone": { @@ -251,7 +252,7 @@ "line1": "Abbiamo ricevuto il tuo deposito.", "line2": "Ti contatteremo a breve al numero {phone}.", "line3": "Una volta chiariti tutti i dubbi, la transazione verrà elaborata automaticamente.", - "line4": "", + "line4": "Qui puoi specificare l'orario che preferisci per la telefonata: [url:https://app.dfx.swiss/settings]", "line5": "Se invece si desidera richiedere un rimborso:
[url:clicca qui]" } }, diff --git a/src/shared/i18n/pt/mail.json b/src/shared/i18n/pt/mail.json index 192b00f9a9..170ef4b445 100644 --- a/src/shared/i18n/pt/mail.json +++ b/src/shared/i18n/pt/mail.json @@ -91,9 +91,10 @@ "test_only": "Test", "kyc_data_needed": "Your KYC data is required for the transaction", "bank_tx_needed": "A bank transaction is required before this transaction can be carried out", - "manual_check_phone": "We were unable to reach you at the phone number you provided", - "manual_check_ip_phone": "We were unable to reach you at the phone number you provided", - "manual_check_ip_country_phone": "We were unable to reach you at the phone number you provided", + "manual_check_phone": "We were unable to reach you at the phone number you provided. You can request a callback and a preferred time for the phone call yourself here: [url:https://app.dfx.swiss/settings]", + "manual_check_ip_phone": "We were unable to reach you at the phone number you provided. You can request a callback and a preferred time for the phone call yourself here: [url:https://app.dfx.swiss/settings]", + "manual_check_ip_country_phone": "We were unable to reach you at the phone number you provided. You can request a callback and a preferred time for the phone call yourself here: [url:https://app.dfx.swiss/settings]", + "manual_check_phone_rejected": "The phone call was unsuccessful or rejected", "merge_incomplete": "The email confirmation was not accepted", "intermediary_without_sender": "O banco remetente (Wise/Revolut) transmitiu apenas o nome do banco, não o nome do titular da conta. Portanto, a DFX não pode verificar o remetente real e não pode processar a transação.", "name_too_short": "O seu nome é muito curto para o processamento bancário. Os bancos exigem pelo menos 4 letras no nome do titular da conta." @@ -215,7 +216,7 @@ "line1": "We have received your deposit.", "line2": "We will contact you shortly at {phone}.", "line3": "Once all questions have been clarified, your transaction will be processed automatically.", - "line4": "", + "line4": "You can specify your preferred time for the phone call here: [url:https://app.dfx.swiss/settings]", "line5": "If you would like to request a refund instead:
[url:click here]" }, "merge_incomplete": { @@ -242,7 +243,7 @@ "line1": "We have received your deposit.", "line2": "We will contact you shortly at {phone}.", "line3": "Once all questions have been clarified, your transaction will be processed automatically.", - "line4": "", + "line4": "You can specify your preferred time for the phone call here: [url:https://app.dfx.swiss/settings]", "line5": "If you would like to request a refund instead:
[url:click here]" }, "manual_check_ip_country_phone": { @@ -251,7 +252,7 @@ "line1": "We have received your deposit.", "line2": "We will contact you shortly at {phone}.", "line3": "Once all questions have been clarified, your transaction will be processed automatically.", - "line4": "", + "line4": "You can specify your preferred time for the phone call here: [url:https://app.dfx.swiss/settings]", "line5": "If you would like to request a refund instead:
[url:click here]" } }, diff --git a/src/subdomains/core/aml/enums/aml-error.enum.ts b/src/subdomains/core/aml/enums/aml-error.enum.ts index 367bee6b8d..8fc473f82c 100644 --- a/src/subdomains/core/aml/enums/aml-error.enum.ts +++ b/src/subdomains/core/aml/enums/aml-error.enum.ts @@ -62,6 +62,7 @@ export enum AmlError { IP_BLACKLISTED_WITHOUT_KYC = 'IpBlacklistedWithoutKyc', BANK_RELEASE_DATE_MISSING = 'BankReleaseDateMissing', IP_COUNTRY_MISMATCH = 'IpCountryMismatch', + USER_DATA_REJECTED_CALL = 'UserDataRejectedCall', TRADE_APPROVAL_DATE_MISSING = 'TradeApprovalDateMissing', BANK_TX_CUSTOMER_NAME_MISSING = 'BankTxCustomerNameMissing', FORCE_MANUAL_CHECK = 'ForceManualCheck', @@ -301,6 +302,11 @@ export const AmlErrorResult: { amlCheck: CheckStatus.PENDING, amlReason: AmlReason.MANUAL_CHECK_IP_COUNTRY_PHONE, }, + [AmlError.USER_DATA_REJECTED_CALL]: { + type: AmlErrorType.CRUCIAL, + amlCheck: CheckStatus.FAIL, + amlReason: AmlReason.MANUAL_CHECK_PHONE_REJECTED, + }, [AmlError.TRADE_APPROVAL_DATE_MISSING]: { type: AmlErrorType.CRUCIAL, amlCheck: CheckStatus.PENDING, diff --git a/src/subdomains/core/aml/enums/aml-reason.enum.ts b/src/subdomains/core/aml/enums/aml-reason.enum.ts index 5f55419276..dafbf87657 100644 --- a/src/subdomains/core/aml/enums/aml-reason.enum.ts +++ b/src/subdomains/core/aml/enums/aml-reason.enum.ts @@ -36,6 +36,7 @@ export enum AmlReason { MANUAL_CHECK_PHONE = 'ManualCheckPhone', MANUAL_CHECK_IP_PHONE = 'ManualCheckIpPhone', MANUAL_CHECK_IP_COUNTRY_PHONE = 'ManualCheckIpCountryPhone', + MANUAL_CHECK_PHONE_REJECTED = 'ManualCheckPhoneRejected', BANK_RELEASE_PENDING = 'BankReleasePending', VIRTUAL_IBAN_USER_MISMATCH = 'VirtualIbanUserMismatch', INTERMEDIARY_WITHOUT_SENDER = 'IntermediaryWithoutSender', diff --git a/src/subdomains/core/aml/services/aml-helper.service.ts b/src/subdomains/core/aml/services/aml-helper.service.ts index bb338e680b..22e2b5c265 100644 --- a/src/subdomains/core/aml/services/aml-helper.service.ts +++ b/src/subdomains/core/aml/services/aml-helper.service.ts @@ -6,7 +6,12 @@ import { Util } from 'src/shared/utils/util'; import { ReviewStatus } from 'src/subdomains/generic/kyc/enums/review-status.enum'; import { BankData, BankDataVerificationError } from 'src/subdomains/generic/user/models/bank-data/bank-data.entity'; import { AccountType } from 'src/subdomains/generic/user/models/user-data/account-type.enum'; -import { KycLevel, KycType, UserDataStatus } from 'src/subdomains/generic/user/models/user-data/user-data.enum'; +import { + KycLevel, + KycType, + PhoneCallStatus, + UserDataStatus, +} from 'src/subdomains/generic/user/models/user-data/user-data.enum'; import { User } from 'src/subdomains/generic/user/models/user/user.entity'; import { UserStatus } from 'src/subdomains/generic/user/models/user/user.enum'; import { Bank } from 'src/subdomains/supporting/bank/bank/bank.entity'; @@ -56,7 +61,11 @@ export class AmlHelperService { !entity.userData.tradeApprovalDate && !entity.wallet.autoTradeApproval ) - errors.push(AmlError.TRADE_APPROVAL_DATE_MISSING); + errors.push( + [PhoneCallStatus.REJECTED, PhoneCallStatus.FAILED].includes(entity.userData.phoneCallStatus) + ? AmlError.USER_DATA_REJECTED_CALL + : AmlError.TRADE_APPROVAL_DATE_MISSING, + ); if (entity.inputReferenceAmount < minVolume * 0.9) errors.push(AmlError.MIN_VOLUME_NOT_REACHED); if (entity.user.isBlocked) errors.push(AmlError.USER_BLOCKED); if (entity.user.isDeleted) errors.push(AmlError.USER_DELETED); @@ -88,7 +97,11 @@ export class AmlHelperService { errors.push(AmlError.YEARLY_LIMIT_WO_KYC_REACHED); if (entity.userData.hasIpRisk && !entity.userData.phoneCallIpCheckDate) { if (entity.userData.kycLevel >= KycLevel.LEVEL_50) { - errors.push(AmlError.IP_PHONE_VERIFICATION_NEEDED); + errors.push( + [PhoneCallStatus.REJECTED, PhoneCallStatus.FAILED].includes(entity.userData.phoneCallStatus) + ? AmlError.USER_DATA_REJECTED_CALL + : AmlError.IP_PHONE_VERIFICATION_NEEDED, + ); } else { errors.push(AmlError.IP_BLACKLISTED_WITHOUT_KYC); } @@ -194,7 +207,11 @@ export class AmlHelperService { ![l, entity.userData.country.symbol].every((c) => Config.allowedBorderRegions.includes(c)), ) ) - errors.push(AmlError.IP_COUNTRY_MISMATCH); + errors.push( + [PhoneCallStatus.REJECTED, PhoneCallStatus.FAILED].includes(entity.userData.phoneCallStatus) + ? AmlError.USER_DATA_REJECTED_CALL + : AmlError.IP_COUNTRY_MISMATCH, + ); if ( entity.userData.hasSuspiciousMail && @@ -220,7 +237,11 @@ export class AmlHelperService { (!entity.userData.accountType || entity.userData.accountType === AccountType.PERSONAL) && Util.yearsDiff(entity.userData.birthday) > 55 ) - errors.push(AmlError.PHONE_VERIFICATION_NEEDED); + errors.push( + [PhoneCallStatus.REJECTED, PhoneCallStatus.FAILED].includes(entity.userData.phoneCallStatus) + ? AmlError.USER_DATA_REJECTED_CALL + : AmlError.PHONE_VERIFICATION_NEEDED, + ); if (entity.bankTx) { // bank @@ -438,7 +459,11 @@ export class AmlHelperService { case AmlRule.RULE_16: if (entity.userData.accountType === AccountType.PERSONAL && !entity.userData.phoneCallCheckDate) - errors.push(AmlError.PHONE_VERIFICATION_NEEDED); + errors.push( + [PhoneCallStatus.REJECTED, PhoneCallStatus.FAILED].includes(entity.userData.phoneCallStatus) + ? AmlError.USER_DATA_REJECTED_CALL + : AmlError.PHONE_VERIFICATION_NEEDED, + ); break; } diff --git a/src/subdomains/supporting/payment/dto/transaction.dto.ts b/src/subdomains/supporting/payment/dto/transaction.dto.ts index 2f7876c026..d05afbfd82 100644 --- a/src/subdomains/supporting/payment/dto/transaction.dto.ts +++ b/src/subdomains/supporting/payment/dto/transaction.dto.ts @@ -111,6 +111,7 @@ export const TransactionReasonMapper: { [AmlReason.MANUAL_CHECK_PHONE]: TransactionReason.PHONE_VERIFICATION_NEEDED, [AmlReason.MANUAL_CHECK_IP_PHONE]: TransactionReason.PHONE_VERIFICATION_NEEDED, [AmlReason.MANUAL_CHECK_IP_COUNTRY_PHONE]: TransactionReason.PHONE_VERIFICATION_NEEDED, + [AmlReason.MANUAL_CHECK_PHONE_REJECTED]: TransactionReason.PHONE_VERIFICATION_NEEDED, [AmlReason.BANK_RELEASE_PENDING]: TransactionReason.BANK_RELEASE_PENDING, [AmlReason.VIRTUAL_IBAN_USER_MISMATCH]: TransactionReason.UNKNOWN, [AmlReason.INTERMEDIARY_WITHOUT_SENDER]: TransactionReason.BANK_NOT_ALLOWED,