diff --git a/src/subdomains/generic/kyc/dto/input/kyc-data.dto.ts b/src/subdomains/generic/kyc/dto/input/kyc-data.dto.ts index f5cca578ac..9ac15e4064 100644 --- a/src/subdomains/generic/kyc/dto/input/kyc-data.dto.ts +++ b/src/subdomains/generic/kyc/dto/input/kyc-data.dto.ts @@ -238,7 +238,9 @@ export class KycOperationalData { } export class KycRecommendationData { - @ApiProperty({ description: 'Recommendation data: ref-code or recommendation-code or mail of existing user' }) + @ApiProperty({ + description: 'Recommendation data: ref-code or recommendation-code or mail of existing user or wallet name', + }) @IsNotEmpty() @IsString() key: string; diff --git a/src/subdomains/generic/kyc/services/kyc.service.ts b/src/subdomains/generic/kyc/services/kyc.service.ts index b6338915fc..9e3e592df6 100644 --- a/src/subdomains/generic/kyc/services/kyc.service.ts +++ b/src/subdomains/generic/kyc/services/kyc.service.ts @@ -21,7 +21,7 @@ import { Util } from 'src/shared/utils/util'; import { CheckStatus } from 'src/subdomains/core/aml/enums/check-status.enum'; import { PaymentLinkRecipientDto } from 'src/subdomains/core/payment-link/dto/payment-link-recipient.dto'; import { MailFactory, MailTranslationKey } from 'src/subdomains/supporting/notification/factories/mail.factory'; -import { FindOptionsWhere, IsNull, LessThan, MoreThan, Not } from 'typeorm'; +import { FindOptionsRelations, FindOptionsWhere, IsNull, LessThan, MoreThan, Not } from 'typeorm'; import { MergeReason } from '../../user/models/account-merge/account-merge.entity'; import { AccountMergeService } from '../../user/models/account-merge/account-merge.service'; import { BankDataType } from '../../user/models/bank-data/bank-data.entity'; @@ -639,7 +639,11 @@ export class KycService { } async updateRecommendationData(kycHash: string, stepId: number, data: KycRecommendationData) { - const user = await this.getUser(kycHash); + const user = await this.getUser(kycHash, { + users: { wallet: true }, + kycSteps: { userData: true }, + wallet: true, + }); const kycStep = user.getPendingStepOrThrow(stepId); await this.recommendationService.handleRecommendationRequest(kycStep, user, data.key); @@ -1678,12 +1682,15 @@ export class KycService { return KycInfoMapper.toDto(user, withSession, kycClients, currentStep); } - private async getUser(kycHash: string): Promise { - return this.userDataService.getByKycHashOrThrow(kycHash, { + private async getUser( + kycHash: string, + relations: FindOptionsRelations = { users: true, kycSteps: { userData: true }, wallet: true, - }); + }, + ): Promise { + return this.userDataService.getByKycHashOrThrow(kycHash, relations); } private async getUserByTransactionOrThrow( diff --git a/src/subdomains/generic/user/models/auth/auth.service.ts b/src/subdomains/generic/user/models/auth/auth.service.ts index 9ea6e69cf6..d297ddcff6 100644 --- a/src/subdomains/generic/user/models/auth/auth.service.ts +++ b/src/subdomains/generic/user/models/auth/auth.service.ts @@ -414,7 +414,6 @@ export class AuthService { private async checkPendingRecommendation(userData: UserData, userWallet?: Wallet): Promise { if (!userData.tradeApprovalDate && (userData.wallet?.autoTradeApproval || userWallet?.autoTradeApproval)) { await this.userDataService.updateUserDataInternal(userData, { tradeApprovalDate: new Date() }); - await this.userDataService.createTradeApprovalLog(userData, TradeApprovalReason.AUTO_TRADE_APPROVAL_LOGIN); const recommendationStep = await this.kycAdminService diff --git a/src/subdomains/generic/user/models/recommendation/recommendation.entity.ts b/src/subdomains/generic/user/models/recommendation/recommendation.entity.ts index ea0baadf49..384c7b8343 100644 --- a/src/subdomains/generic/user/models/recommendation/recommendation.entity.ts +++ b/src/subdomains/generic/user/models/recommendation/recommendation.entity.ts @@ -10,6 +10,7 @@ export enum RecommendationType { } export enum RecommendationMethod { + WALLET = 'Wallet', REF_CODE = 'RefCode', MAIL = 'Mail', RECOMMENDATION_CODE = 'RecommendationCode', diff --git a/src/subdomains/generic/user/models/recommendation/recommendation.service.ts b/src/subdomains/generic/user/models/recommendation/recommendation.service.ts index a7d48fdc41..60846b0025 100644 --- a/src/subdomains/generic/user/models/recommendation/recommendation.service.ts +++ b/src/subdomains/generic/user/models/recommendation/recommendation.service.ts @@ -15,6 +15,7 @@ import { UserData } from '../user-data/user-data.entity'; import { KycLevel, KycType, TradeApprovalReason, UserDataStatus } from '../user-data/user-data.enum'; import { UserDataService } from '../user-data/user-data.service'; import { UserService } from '../user/user.service'; +import { WalletService } from '../wallet/wallet.service'; import { CreateRecommendationDto } from './dto/recommendation.dto'; import { Recommendation, RecommendationMethod, RecommendationType } from './recommendation.entity'; import { RecommendationRepository } from './recommendation.repository'; @@ -31,6 +32,7 @@ export class RecommendationService { private readonly userService: UserService, @Inject(forwardRef(() => KycService)) private readonly kycService: KycService, + private readonly walletService: WalletService, ) {} async createRecommendationByRecommender(userDataId: number, dto: CreateRecommendationDto): Promise { @@ -128,50 +130,79 @@ export class RecommendationService { confirmationDate: new Date(), }); } else { - // create new recommendation - const recommender = Config.formats.ref.test(key) - ? await this.userService.getRefUser(key).then((u) => u?.userData) + const method = Config.formats.ref.test(key) + ? RecommendationMethod.REF_CODE : key.includes('@') - ? await this.userDataService.getUsersByMail(key, true).then((u) => u.find((us) => us.tradeApprovalDate)) - : undefined; - if ( - !recommender || - recommender.isBlocked || - recommender.hasAnyRiskStatus || - recommender.kycLevel < KycLevel.LEVEL_50 || - !recommender.tradeApprovalDate - ) - throw new NotFoundException('Recommender not found'); - - const existingRecommendations = await this.recommendationRepo.countBy({ - recommender: { id: recommender.id }, - recommended: { id: userData.id }, - }); - if (existingRecommendations > Config.recommendation.maxRecommendationPerMail) - throw new BadRequestException('Max amount of recommendations for this account reached'); - - const entity = await this.createRecommendationInternal( - RecommendationType.REQUEST, - Config.formats.ref.test(key) ? RecommendationMethod.REF_CODE : RecommendationMethod.MAIL, - recommender, - userData, - kycStep, - ); + ? RecommendationMethod.MAIL + : RecommendationMethod.WALLET; + + if (method === RecommendationMethod.WALLET) { + const recommender = await this.walletService.getByIdOrName(undefined, key); + if (!recommender || !recommender.autoTradeApproval) throw new NotFoundException('Recommender not found'); + + await this.userDataService.updateUserDataInternal(userData, { + tradeApprovalDate: new Date(), + wallet: !userData.wallet || userData.wallet.id === Config.defaultWalletId ? recommender : undefined, + }); + await this.userDataService.createTradeApprovalLog(userData, TradeApprovalReason.KYC_STEP_COMPLETED); - await this.sendPendingConfirmationMail(entity); + for (const user of userData.users) { + if (user.wallet.id === Config.defaultWalletId) + await this.userService.updateUserInternal(user, { wallet: recommender }); + } + + const existingRecommendations = await this.recommendationRepo.countBy({ recommended: { id: userData.id } }); + if (existingRecommendations > Config.recommendation.maxRecommendationPerMail) + throw new BadRequestException('Max amount of recommendations for this account reached'); + + await this.createRecommendationInternal(RecommendationType.REQUEST, method, undefined, userData, kycStep); + } else { + // create new recommendation + const recommender = Config.formats.ref.test(key) + ? await this.userService.getRefUser(key).then((u) => u?.userData) + : key.includes('@') + ? await this.userDataService.getUsersByMail(key, true).then((u) => u.find((us) => us.tradeApprovalDate)) + : undefined; + + if ( + !recommender || + recommender.isBlocked || + recommender.hasAnyRiskStatus || + recommender.kycLevel < KycLevel.LEVEL_50 || + !recommender.tradeApprovalDate + ) + throw new NotFoundException('Recommender not found'); + + const existingRecommendations = await this.recommendationRepo.countBy({ + recommender: { id: recommender.id }, + recommended: { id: userData.id }, + }); + if (existingRecommendations > Config.recommendation.maxRecommendationPerMail) + throw new BadRequestException('Max amount of recommendations for this account reached'); + + const entity = await this.createRecommendationInternal( + RecommendationType.REQUEST, + method, + recommender, + userData, + kycStep, + ); + + await this.sendPendingConfirmationMail(entity); + } } } private async createRecommendationInternal( type: RecommendationType, method: RecommendationMethod, - recommender: UserData, + recommender: UserData | undefined, recommended?: UserData, kycStep?: KycStep, recommendedAlias?: string, recommendedMail?: string, ): Promise { - const hash = Util.createHash(new Date().toISOString() + recommender.id).toUpperCase(); + const hash = Util.createHash(new Date().toISOString() + (recommender?.id ?? Util.randomId())).toUpperCase(); const entity = this.recommendationRepo.create({ kycStep, diff --git a/src/subdomains/generic/user/models/user/dto/update-user-admin.dto.ts b/src/subdomains/generic/user/models/user/dto/update-user-admin.dto.ts index d43a22b246..b1428a1365 100644 --- a/src/subdomains/generic/user/models/user/dto/update-user-admin.dto.ts +++ b/src/subdomains/generic/user/models/user/dto/update-user-admin.dto.ts @@ -1,7 +1,9 @@ import { Type } from 'class-transformer'; -import { IsBoolean, IsDate, IsEnum, IsOptional, IsString } from 'class-validator'; +import { IsBoolean, IsDate, IsEnum, IsObject, IsOptional, IsString, ValidateNested } from 'class-validator'; import { UserRole } from 'src/shared/auth/user-role.enum'; +import { EntityDto } from 'src/shared/dto/entity.dto'; import { Moderator } from '../../user-data/user-data.enum'; +import { Wallet } from '../../wallet/wallet.entity'; import { UserAddressType, UserStatus, WalletType } from '../user.enum'; export class UpdateUserInternalDto { @@ -45,4 +47,10 @@ export class UpdateUserInternalDto { @IsOptional() @IsEnum(Moderator) moderator?: Moderator; + + @IsOptional() + @IsObject() + @ValidateNested() + @Type(() => EntityDto) + wallet?: Wallet; }