Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions migration/1772026123966-AddFeeAmountInChfToTransaction.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* @typedef {import('typeorm').MigrationInterface} MigrationInterface
* @typedef {import('typeorm').QueryRunner} QueryRunner
*/

/**
* @class
* @implements {MigrationInterface}
*/
module.exports = class AddFeeAmountInChfToTransaction1772026123966 {
name = 'AddFeeAmountInChfToTransaction1772026123966'

/**
* @param {QueryRunner} queryRunner
*/
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "transaction" ADD "feeAmountInChf" float`);
}

/**
* @param {QueryRunner} queryRunner
*/
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "transaction" DROP COLUMN "feeAmountInChf"`);
}
}
1 change: 1 addition & 0 deletions src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@ export class Configuration {
residencePermitCountries: ['RU'],
maxIdentTries: 7,
maxRecommendationTries: 3,
kycStepExpiry: 90, // days
};

fileDownloadConfig: {
Expand Down
2 changes: 1 addition & 1 deletion src/integration/blockchain/firo/firo-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export class FiroClient extends BitcoinBasedClient {
true,
);

return utxos?.reduce((sum, u) => sum + u.amount, 0) ?? 0;
return this.roundAmount(utxos?.reduce((sum, u) => sum + u.amount, 0) ?? 0);
}

// Firo's getblock uses boolean verbose, not int verbosity (0/1/2)
Expand Down
2 changes: 1 addition & 1 deletion src/integration/exchange/services/scrypt.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ export class ScryptService extends PricingProvider {
ClReqID: params.reqId,
Quantity: params.amount.toString(),
TransactTime: params.timeStamp.toISOString(),
TxHashes: (params.txHashes ?? []).map((hash) => ({ TxHash: hash })),
TxHashes: (params.txHashes?.length ? params.txHashes : [params.reqId]).map((hash) => ({ TxHash: hash })),
},
],
};
Expand Down
1 change: 1 addition & 0 deletions src/subdomains/core/aml/services/aml.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export class AmlService {
amlCheck: entity.amlCheck,
assets: `${entity.inputReferenceAsset}-${entity.outputAsset.name}`,
amountInChf: entity.amountInChf,
feeAmountInChf: entity.feeAmountChf,
highRisk: entity.highRisk == true,
eventDate: entity.created,
amlType: entity.transaction.type,
Expand Down
6 changes: 5 additions & 1 deletion src/subdomains/generic/kyc/entities/kyc-step.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ export class KycStep extends IEntity {
const update: Partial<KycStep> = {
status,
result: this.setResult(result),
comment: comment ?? this.comment,
comment: this.addComment(comment),
sequenceNumber,
};

Expand Down Expand Up @@ -354,6 +354,10 @@ export class KycStep extends IEntity {
return this.result;
}

addComment(comment: string): string | undefined {
return [this.comment, comment].filter((c) => c).join(';');
}

get resultData(): IdentResultData {
if (!this.result) return undefined;

Expand Down
66 changes: 34 additions & 32 deletions src/subdomains/generic/kyc/services/kyc-notification.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ export class KycNotificationService {

@DfxCron(CronExpression.EVERY_HOUR, { process: Process.KYC_MAIL, timeout: 1800 })
async sendNotificationMails(): Promise<void> {
await this.kycStepReminder();
await this.autoKycStepReminder();
}

private async kycStepReminder(): Promise<void> {
private async autoKycStepReminder(): Promise<void> {
const entities = await this.kycStepRepo.find({
where: {
reminderSentDate: IsNull(),
Expand All @@ -50,36 +50,7 @@ export class KycNotificationService {

for (const entity of entities) {
try {
const recipientMail = entity.userData.mail;

if (recipientMail) {
await this.notificationService.sendMail({
type: MailType.USER_V2,
context: MailContext.KYC_REMINDER,
input: {
userData: entity.userData,
wallet: entity.userData.wallet,
title: `${MailTranslationKey.KYC_REMINDER}.title`,
salutation: { key: `${MailTranslationKey.KYC_REMINDER}.salutation` },
texts: [
{ key: MailKey.SPACE, params: { value: '1' } },
{ key: `${MailTranslationKey.KYC_REMINDER}.message` },
{ key: MailKey.SPACE, params: { value: '2' } },
{
key: `${MailTranslationKey.GENERAL}.button`,
params: { url: entity.userData.kycUrl, button: 'true' },
},
{
key: `${MailTranslationKey.KYC}.next_step`,
params: { url: entity.userData.kycUrl, urlText: entity.userData.kycUrl },
},
{ key: MailKey.DFX_TEAM_CLOSING },
],
},
});
} else {
this.logger.warn(`Failed to send KYC reminder mail for user data ${entity.userData.id}: user has no email`);
}
await this.kycStepReminder(entity.userData);

await this.kycStepRepo.update(...entity.reminderSent());
} catch (e) {
Expand All @@ -88,6 +59,37 @@ export class KycNotificationService {
}
}

async kycStepReminder(userData: UserData): Promise<void> {
if (userData.mail) {
await this.notificationService.sendMail({
type: MailType.USER_V2,
context: MailContext.KYC_REMINDER,
input: {
userData,
wallet: userData.wallet,
title: `${MailTranslationKey.KYC_REMINDER}.title`,
salutation: { key: `${MailTranslationKey.KYC_REMINDER}.salutation` },
texts: [
{ key: MailKey.SPACE, params: { value: '1' } },
{ key: `${MailTranslationKey.KYC_REMINDER}.message` },
{ key: MailKey.SPACE, params: { value: '2' } },
{
key: `${MailTranslationKey.GENERAL}.button`,
params: { url: userData.kycUrl, button: 'true' },
},
{
key: `${MailTranslationKey.KYC}.next_step`,
params: { url: userData.kycUrl, urlText: userData.kycUrl },
},
{ key: MailKey.DFX_TEAM_CLOSING },
],
},
});
} else {
this.logger.warn(`Failed to send KYC reminder mail for user data ${userData.id}: user has no email`);
}
}

async kycStepFailed(userData: UserData, stepName: string, reason: string): Promise<void> {
try {
if ((userData.mail, !DisabledProcess(Process.KYC_MAIL))) {
Expand Down
30 changes: 28 additions & 2 deletions src/subdomains/generic/kyc/services/kyc.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import { UserDataRelationService } from '../../user/models/user-data-relation/us
import { AccountType } from '../../user/models/user-data/account-type.enum';
import { KycIdentificationType } from '../../user/models/user-data/kyc-identification-type.enum';
import { UserData } from '../../user/models/user-data/user-data.entity';
import { KycLevel, KycType, UserDataStatus } from '../../user/models/user-data/user-data.enum';
import { KycLevel, KycType, TradeApprovalReason, UserDataStatus } from '../../user/models/user-data/user-data.enum';
import { UserDataService } from '../../user/models/user-data/user-data.service';
import { WalletService } from '../../user/models/wallet/wallet.service';
import { WebhookService } from '../../user/services/webhook/webhook.service';
Expand Down Expand Up @@ -382,6 +382,29 @@ export class KycService {
}

async checkDfxApproval(kycStep: KycStep): Promise<void> {
const expiredSteps = [
...kycStep.userData.getStepsWith(KycStepName.IDENT, KycStepType.SUMSUB_AUTO),
...kycStep.userData.getStepsWith(KycStepName.IDENT, KycStepType.AUTO),
...kycStep.userData.getStepsWith(KycStepName.IDENT, KycStepType.VIDEO),
...kycStep.userData.getStepsWith(KycStepName.FINANCIAL_DATA),
].filter(
(s) =>
(s?.isInProgress || s?.isInReview || s?.isCompleted) && Util.daysDiff(s.created) > Config.kyc.kycStepExpiry,
);

if (expiredSteps.length) {
for (const expiredStep of expiredSteps) {
await this.kycStepRepo.update(...expiredStep.update(ReviewStatus.OUTDATED, undefined, KycError.EXPIRED_STEP));
}

kycStep.userData = await this.userDataService.getUserData(kycStep.userData.id, { kycSteps: true });

// initiate next step
await this.updateProgress(kycStep.userData, true, false);

return this.kycNotificationService.kycStepReminder(kycStep.userData);
}

const missingCompletedSteps = requiredKycSteps(kycStep.userData).filter(
(rs) => !kycStep.userData.hasCompletedStep(rs),
);
Expand Down Expand Up @@ -1355,7 +1378,10 @@ export class KycService {
}

async completeRecommendation(userData: UserData): Promise<void> {
await this.userDataService.updateUserDataInternal(userData, { tradeApprovalDate: new Date() });
if (!userData.tradeApprovalDate) {
await this.userDataService.updateUserDataInternal(userData, { tradeApprovalDate: new Date() });
await this.userDataService.createTradeApprovalLog(userData, TradeApprovalReason.KYC_STEP_COMPLETED);
}
}

private getStepDefaultErrors(entity: KycStep): KycError[] {
Expand Down
14 changes: 11 additions & 3 deletions src/subdomains/generic/user/models/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import { FeeService } from 'src/subdomains/supporting/payment/services/fee.servi
import { CustodyProviderService } from '../custody-provider/custody-provider.service';
import { RecommendationService } from '../recommendation/recommendation.service';
import { UserData } from '../user-data/user-data.entity';
import { KycType, UserDataStatus } from '../user-data/user-data.enum';
import { KycType, TradeApprovalReason, UserDataStatus } from '../user-data/user-data.enum';
import { UserDataService } from '../user-data/user-data.service';
import { LinkedUserInDto } from '../user/dto/linked-user.dto';
import { User } from '../user/user.entity';
Expand Down Expand Up @@ -402,10 +402,18 @@ export class AuthService {
// --- HELPER METHODS --- //

private async checkPendingRecommendation(userData: UserData, userWallet?: Wallet): Promise<void> {
if (userData.wallet?.autoTradeApproval || userWallet?.autoTradeApproval)
if (!userData.tradeApprovalDate && (userData.wallet?.autoTradeApproval || userWallet?.autoTradeApproval)) {
await this.userDataService.updateUserDataInternal(userData, { tradeApprovalDate: new Date() });

await this.recommendationService.checkAndConfirmRecommendInvitation(userData.id);
await this.userDataService.createTradeApprovalLog(userData, TradeApprovalReason.AUTO_TRADE_APPROVAL_LOGIN);

const recommendationStep = await this.kycAdminService
.getKycSteps(userData.id)
.then((k) => k.find((s) => s.name === KycStepName.RECOMMENDATION && !s.isCompleted));
if (recommendationStep) await this.kycAdminService.updateKycStepInternal(recommendationStep.cancel());

await this.recommendationService.checkAndConfirmRecommendInvitation(userData.id);
}
}

private async confirmRecommendationCode(code: string, userData: UserData): Promise<void> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { MailKey, MailTranslationKey } from 'src/subdomains/supporting/notificat
import { NotificationService } from 'src/subdomains/supporting/notification/services/notification.service';
import { IsNull, MoreThan } from 'typeorm';
import { UserData } from '../user-data/user-data.entity';
import { KycLevel, KycType, UserDataStatus } from '../user-data/user-data.enum';
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 { CreateRecommendationDto } from './dto/recommendation.dto';
Expand Down Expand Up @@ -75,6 +75,9 @@ export class RecommendationService {
})
: undefined;

if (recommended?.tradeApprovalDate)
await this.userDataService.createTradeApprovalLog(recommended, TradeApprovalReason.MAIL_INVITATION);

const entity = await this.createRecommendationInternal(
RecommendationType.INVITATION,
dto.recommendedMail ? RecommendationMethod.MAIL : RecommendationMethod.RECOMMENDATION_CODE,
Expand Down Expand Up @@ -213,11 +216,18 @@ export class RecommendationService {
async updateRecommendationInternal(entity: Recommendation, update: Partial<Recommendation>): Promise<Recommendation> {
Object.assign(entity, update);

if (update.isConfirmed && entity.recommended) {
if (entity.isConfirmed !== null && update.isConfirmed !== entity.isConfirmed)
throw new BadRequestException('Recommendation already completed');
if (update.isConfirmed && entity.recommended && !entity.recommended.tradeApprovalDate) {
await this.userDataService.updateUserDataInternal(entity.recommended, {
tradeApprovalDate: new Date(),
});

await this.userDataService.createTradeApprovalLog(
entity.recommended,
TradeApprovalReason.RECOMMENDATION_CONFIRMED,
);

const refCode =
entity.kycStep && entity.method === RecommendationMethod.REF_CODE
? entity.kycStep.getResult<KycRecommendationData>().key
Expand Down
10 changes: 10 additions & 0 deletions src/subdomains/generic/user/models/user-data/user-data.enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,13 @@ export enum UserDataStatus {
export enum Moderator {
WENDEL = 'Wendel',
}

export enum TradeApprovalReason {
USER_DATA_MERGE = 'UserDataMerge',
ORGANIZATION = 'Organization',
KYC_STEP_COMPLETED = 'KycStepCompleted',
MAIL_INVITATION = 'MailInvitation',
RECOMMENDATION_CONFIRMED = 'RecommendationConfirmed',
AUTO_TRADE_APPROVAL_USER_DATA_CREATED = 'AutoTradeApprovalUserDataCreated',
AUTO_TRADE_APPROVAL_LOGIN = 'AutoTradeApprovalLogin',
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ import { UpdateUserDataDto } from './dto/update-user-data.dto';
import { KycIdentificationType } from './kyc-identification-type.enum';
import { UserDataNotificationService } from './user-data-notification.service';
import { UserData } from './user-data.entity';
import { KycLevel, UserDataStatus } from './user-data.enum';
import { KycLevel, TradeApprovalReason, UserDataStatus } from './user-data.enum';
import { UserDataRepository } from './user-data.repository';

export const MergedPrefix = 'Merged into ';
Expand Down Expand Up @@ -262,17 +262,17 @@ export class UserDataService {

// --- CREATE / UPDATE ---
async createUserData(dto: CreateUserDataDto): Promise<UserData> {
const userData = this.userDataRepo.create({
const entity = this.userDataRepo.create({
...dto,
language: dto.language ?? (await this.languageService.getLanguageBySymbol(Config.defaults.language)),
currency: dto.currency ?? (await this.fiatService.getFiatByName(Config.defaults.currency)),
kycHash: randomUUID().toUpperCase(),
kycSteps: [],
});

await this.loadRelationsAndVerify(userData, dto);
await this.loadRelationsAndVerify(entity, dto);

return this.userDataRepo.save(userData);
return this.userDataRepo.save(entity);
}

async updateUserData(userDataId: number, dto: UpdateUserDataDto): Promise<UserData> {
Expand Down Expand Up @@ -520,7 +520,8 @@ export class UserDataService {
organizationLocation: data.organizationAddress?.city,
organizationZip: data.organizationAddress?.zip,
organizationCountry: data.organizationAddress?.country,
tradeApprovalDate: data.accountType === AccountType.ORGANIZATION ? new Date() : undefined,
tradeApprovalDate:
!userData.tradeApprovalDate && data.accountType === AccountType.ORGANIZATION ? new Date() : undefined,
};

const isPersonalAccount =
Expand Down Expand Up @@ -579,6 +580,8 @@ export class UserDataService {
}

if (update.mail) await this.kycLogService.createMailChangeLog(userData, userData.mail, update.mail);
if (update.tradeApprovalDate)
await this.createTradeApprovalLog(userData, TradeApprovalReason.ORGANIZATION, update.tradeApprovalDate);

await this.userDataRepo.update(userData.id, update);

Expand Down Expand Up @@ -626,6 +629,18 @@ export class UserDataService {
await this.userDataRepo.update(...userData.refreshLastCheckedTimestamp());
}

async createTradeApprovalLog(
userData: UserData,
reason: TradeApprovalReason,
tradeApprovalDate?: Date,
): Promise<void> {
return this.kycLogService.createLogInternal(
userData,
KycLogType.KYC,
`TradeApprovalDate set to ${(tradeApprovalDate ?? userData.tradeApprovalDate).toISOString()}, reason: ${reason}`,
);
}

// --- MAIL UPDATE --- //

async updateUserMail(userData: UserData, dto: UpdateUserMailDto, ip: string): Promise<UpdateMailStatus> {
Expand Down Expand Up @@ -828,6 +843,7 @@ export class UserDataService {
}

// --- HELPER METHODS --- //

private async loadRelationsAndVerify(
userData: Partial<UserData> | UserData,
dto: UpdateUserDataDto | CreateUserDataDto,
Expand Down Expand Up @@ -1164,9 +1180,15 @@ export class UserDataService {
}
if (!master.verifiedName && slave.verifiedName) master.verifiedName = slave.verifiedName;
master.mail = mail ?? slave.mail ?? master.mail;
if (!master.tradeApprovalDate && slave.tradeApprovalDate) master.tradeApprovalDate = slave.tradeApprovalDate;
if (!master.tradeApprovalDate && slave.tradeApprovalDate) {
master.tradeApprovalDate = slave.tradeApprovalDate;

await this.createTradeApprovalLog(master, TradeApprovalReason.USER_DATA_MERGE);
}

const pendingRecommendation = master.kycSteps.find((k) => k.name === KycStepName.RECOMMENDATION && !k.isDone);
const pendingRecommendation = master.kycSteps.find(
(k) => k.name === KycStepName.RECOMMENDATION && (k.isInProgress || k.isInReview),
);
if (master.tradeApprovalDate && pendingRecommendation)
await this.kycAdminService.updateKycStepInternal(pendingRecommendation.update(ReviewStatus.COMPLETED));

Expand Down
Loading
Loading