Skip to content

Commit a57a5a2

Browse files
authored
Merge pull request #3263 from DFXswiss/develop
Release: develop -> main
2 parents 70810de + 951a13b commit a57a5a2

16 files changed

Lines changed: 193 additions & 51 deletions

File tree

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/**
2+
* @typedef {import('typeorm').MigrationInterface} MigrationInterface
3+
* @typedef {import('typeorm').QueryRunner} QueryRunner
4+
*/
5+
6+
/**
7+
* @class
8+
* @implements {MigrationInterface}
9+
*/
10+
module.exports = class AddFeeAmountInChfToTransaction1772026123966 {
11+
name = 'AddFeeAmountInChfToTransaction1772026123966'
12+
13+
/**
14+
* @param {QueryRunner} queryRunner
15+
*/
16+
async up(queryRunner) {
17+
await queryRunner.query(`ALTER TABLE "transaction" ADD "feeAmountInChf" float`);
18+
}
19+
20+
/**
21+
* @param {QueryRunner} queryRunner
22+
*/
23+
async down(queryRunner) {
24+
await queryRunner.query(`ALTER TABLE "transaction" DROP COLUMN "feeAmountInChf"`);
25+
}
26+
}

src/config/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,7 @@ export class Configuration {
273273
residencePermitCountries: ['RU'],
274274
maxIdentTries: 7,
275275
maxRecommendationTries: 3,
276+
kycStepExpiry: 90, // days
276277
};
277278

278279
fileDownloadConfig: {

src/integration/blockchain/firo/firo-client.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export class FiroClient extends BitcoinBasedClient {
5656
true,
5757
);
5858

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

6262
// Firo's getblock uses boolean verbose, not int verbosity (0/1/2)

src/integration/exchange/services/scrypt.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ export class ScryptService extends PricingProvider {
172172
ClReqID: params.reqId,
173173
Quantity: params.amount.toString(),
174174
TransactTime: params.timeStamp.toISOString(),
175-
TxHashes: (params.txHashes ?? []).map((hash) => ({ TxHash: hash })),
175+
TxHashes: (params.txHashes?.length ? params.txHashes : [params.reqId]).map((hash) => ({ TxHash: hash })),
176176
},
177177
],
178178
};

src/subdomains/core/aml/services/aml.service.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ export class AmlService {
7171
amlCheck: entity.amlCheck,
7272
assets: `${entity.inputReferenceAsset}-${entity.outputAsset.name}`,
7373
amountInChf: entity.amountInChf,
74+
feeAmountInChf: entity.feeAmountChf,
7475
highRisk: entity.highRisk == true,
7576
eventDate: entity.created,
7677
amlType: entity.transaction.type,

src/subdomains/generic/kyc/entities/kyc-step.entity.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ export class KycStep extends IEntity {
212212
const update: Partial<KycStep> = {
213213
status,
214214
result: this.setResult(result),
215-
comment: comment ?? this.comment,
215+
comment: this.addComment(comment),
216216
sequenceNumber,
217217
};
218218

@@ -354,6 +354,10 @@ export class KycStep extends IEntity {
354354
return this.result;
355355
}
356356

357+
addComment(comment: string): string | undefined {
358+
return [this.comment, comment].filter((c) => c).join(';');
359+
}
360+
357361
get resultData(): IdentResultData {
358362
if (!this.result) return undefined;
359363

src/subdomains/generic/kyc/services/kyc-notification.service.ts

Lines changed: 34 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,10 @@ export class KycNotificationService {
2828

2929
@DfxCron(CronExpression.EVERY_HOUR, { process: Process.KYC_MAIL, timeout: 1800 })
3030
async sendNotificationMails(): Promise<void> {
31-
await this.kycStepReminder();
31+
await this.autoKycStepReminder();
3232
}
3333

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

5151
for (const entity of entities) {
5252
try {
53-
const recipientMail = entity.userData.mail;
54-
55-
if (recipientMail) {
56-
await this.notificationService.sendMail({
57-
type: MailType.USER_V2,
58-
context: MailContext.KYC_REMINDER,
59-
input: {
60-
userData: entity.userData,
61-
wallet: entity.userData.wallet,
62-
title: `${MailTranslationKey.KYC_REMINDER}.title`,
63-
salutation: { key: `${MailTranslationKey.KYC_REMINDER}.salutation` },
64-
texts: [
65-
{ key: MailKey.SPACE, params: { value: '1' } },
66-
{ key: `${MailTranslationKey.KYC_REMINDER}.message` },
67-
{ key: MailKey.SPACE, params: { value: '2' } },
68-
{
69-
key: `${MailTranslationKey.GENERAL}.button`,
70-
params: { url: entity.userData.kycUrl, button: 'true' },
71-
},
72-
{
73-
key: `${MailTranslationKey.KYC}.next_step`,
74-
params: { url: entity.userData.kycUrl, urlText: entity.userData.kycUrl },
75-
},
76-
{ key: MailKey.DFX_TEAM_CLOSING },
77-
],
78-
},
79-
});
80-
} else {
81-
this.logger.warn(`Failed to send KYC reminder mail for user data ${entity.userData.id}: user has no email`);
82-
}
53+
await this.kycStepReminder(entity.userData);
8354

8455
await this.kycStepRepo.update(...entity.reminderSent());
8556
} catch (e) {
@@ -88,6 +59,37 @@ export class KycNotificationService {
8859
}
8960
}
9061

62+
async kycStepReminder(userData: UserData): Promise<void> {
63+
if (userData.mail) {
64+
await this.notificationService.sendMail({
65+
type: MailType.USER_V2,
66+
context: MailContext.KYC_REMINDER,
67+
input: {
68+
userData,
69+
wallet: userData.wallet,
70+
title: `${MailTranslationKey.KYC_REMINDER}.title`,
71+
salutation: { key: `${MailTranslationKey.KYC_REMINDER}.salutation` },
72+
texts: [
73+
{ key: MailKey.SPACE, params: { value: '1' } },
74+
{ key: `${MailTranslationKey.KYC_REMINDER}.message` },
75+
{ key: MailKey.SPACE, params: { value: '2' } },
76+
{
77+
key: `${MailTranslationKey.GENERAL}.button`,
78+
params: { url: userData.kycUrl, button: 'true' },
79+
},
80+
{
81+
key: `${MailTranslationKey.KYC}.next_step`,
82+
params: { url: userData.kycUrl, urlText: userData.kycUrl },
83+
},
84+
{ key: MailKey.DFX_TEAM_CLOSING },
85+
],
86+
},
87+
});
88+
} else {
89+
this.logger.warn(`Failed to send KYC reminder mail for user data ${userData.id}: user has no email`);
90+
}
91+
}
92+
9193
async kycStepFailed(userData: UserData, stepName: string, reason: string): Promise<void> {
9294
try {
9395
if ((userData.mail, !DisabledProcess(Process.KYC_MAIL))) {

src/subdomains/generic/kyc/services/kyc.service.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import { UserDataRelationService } from '../../user/models/user-data-relation/us
3232
import { AccountType } from '../../user/models/user-data/account-type.enum';
3333
import { KycIdentificationType } from '../../user/models/user-data/kyc-identification-type.enum';
3434
import { UserData } from '../../user/models/user-data/user-data.entity';
35-
import { KycLevel, KycType, UserDataStatus } from '../../user/models/user-data/user-data.enum';
35+
import { KycLevel, KycType, TradeApprovalReason, UserDataStatus } from '../../user/models/user-data/user-data.enum';
3636
import { UserDataService } from '../../user/models/user-data/user-data.service';
3737
import { WalletService } from '../../user/models/wallet/wallet.service';
3838
import { WebhookService } from '../../user/services/webhook/webhook.service';
@@ -382,6 +382,29 @@ export class KycService {
382382
}
383383

384384
async checkDfxApproval(kycStep: KycStep): Promise<void> {
385+
const expiredSteps = [
386+
...kycStep.userData.getStepsWith(KycStepName.IDENT, KycStepType.SUMSUB_AUTO),
387+
...kycStep.userData.getStepsWith(KycStepName.IDENT, KycStepType.AUTO),
388+
...kycStep.userData.getStepsWith(KycStepName.IDENT, KycStepType.VIDEO),
389+
...kycStep.userData.getStepsWith(KycStepName.FINANCIAL_DATA),
390+
].filter(
391+
(s) =>
392+
(s?.isInProgress || s?.isInReview || s?.isCompleted) && Util.daysDiff(s.created) > Config.kyc.kycStepExpiry,
393+
);
394+
395+
if (expiredSteps.length) {
396+
for (const expiredStep of expiredSteps) {
397+
await this.kycStepRepo.update(...expiredStep.update(ReviewStatus.OUTDATED, undefined, KycError.EXPIRED_STEP));
398+
}
399+
400+
kycStep.userData = await this.userDataService.getUserData(kycStep.userData.id, { kycSteps: true });
401+
402+
// initiate next step
403+
await this.updateProgress(kycStep.userData, true, false);
404+
405+
return this.kycNotificationService.kycStepReminder(kycStep.userData);
406+
}
407+
385408
const missingCompletedSteps = requiredKycSteps(kycStep.userData).filter(
386409
(rs) => !kycStep.userData.hasCompletedStep(rs),
387410
);
@@ -1355,7 +1378,10 @@ export class KycService {
13551378
}
13561379

13571380
async completeRecommendation(userData: UserData): Promise<void> {
1358-
await this.userDataService.updateUserDataInternal(userData, { tradeApprovalDate: new Date() });
1381+
if (!userData.tradeApprovalDate) {
1382+
await this.userDataService.updateUserDataInternal(userData, { tradeApprovalDate: new Date() });
1383+
await this.userDataService.createTradeApprovalLog(userData, TradeApprovalReason.KYC_STEP_COMPLETED);
1384+
}
13591385
}
13601386

13611387
private getStepDefaultErrors(entity: KycStep): KycError[] {

src/subdomains/generic/user/models/auth/auth.service.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import { FeeService } from 'src/subdomains/supporting/payment/services/fee.servi
3535
import { CustodyProviderService } from '../custody-provider/custody-provider.service';
3636
import { RecommendationService } from '../recommendation/recommendation.service';
3737
import { UserData } from '../user-data/user-data.entity';
38-
import { KycType, UserDataStatus } from '../user-data/user-data.enum';
38+
import { KycType, TradeApprovalReason, UserDataStatus } from '../user-data/user-data.enum';
3939
import { UserDataService } from '../user-data/user-data.service';
4040
import { LinkedUserInDto } from '../user/dto/linked-user.dto';
4141
import { User } from '../user/user.entity';
@@ -402,10 +402,18 @@ export class AuthService {
402402
// --- HELPER METHODS --- //
403403

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

408-
await this.recommendationService.checkAndConfirmRecommendInvitation(userData.id);
408+
await this.userDataService.createTradeApprovalLog(userData, TradeApprovalReason.AUTO_TRADE_APPROVAL_LOGIN);
409+
410+
const recommendationStep = await this.kycAdminService
411+
.getKycSteps(userData.id)
412+
.then((k) => k.find((s) => s.name === KycStepName.RECOMMENDATION && !s.isCompleted));
413+
if (recommendationStep) await this.kycAdminService.updateKycStepInternal(recommendationStep.cancel());
414+
415+
await this.recommendationService.checkAndConfirmRecommendInvitation(userData.id);
416+
}
409417
}
410418

411419
private async confirmRecommendationCode(code: string, userData: UserData): Promise<void> {

src/subdomains/generic/user/models/recommendation/recommendation.service.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { MailKey, MailTranslationKey } from 'src/subdomains/supporting/notificat
1212
import { NotificationService } from 'src/subdomains/supporting/notification/services/notification.service';
1313
import { IsNull, MoreThan } from 'typeorm';
1414
import { UserData } from '../user-data/user-data.entity';
15-
import { KycLevel, KycType, UserDataStatus } from '../user-data/user-data.enum';
15+
import { KycLevel, KycType, TradeApprovalReason, UserDataStatus } from '../user-data/user-data.enum';
1616
import { UserDataService } from '../user-data/user-data.service';
1717
import { UserService } from '../user/user.service';
1818
import { CreateRecommendationDto } from './dto/recommendation.dto';
@@ -75,6 +75,9 @@ export class RecommendationService {
7575
})
7676
: undefined;
7777

78+
if (recommended?.tradeApprovalDate)
79+
await this.userDataService.createTradeApprovalLog(recommended, TradeApprovalReason.MAIL_INVITATION);
80+
7881
const entity = await this.createRecommendationInternal(
7982
RecommendationType.INVITATION,
8083
dto.recommendedMail ? RecommendationMethod.MAIL : RecommendationMethod.RECOMMENDATION_CODE,
@@ -213,11 +216,18 @@ export class RecommendationService {
213216
async updateRecommendationInternal(entity: Recommendation, update: Partial<Recommendation>): Promise<Recommendation> {
214217
Object.assign(entity, update);
215218

216-
if (update.isConfirmed && entity.recommended) {
219+
if (entity.isConfirmed !== null && update.isConfirmed !== entity.isConfirmed)
220+
throw new BadRequestException('Recommendation already completed');
221+
if (update.isConfirmed && entity.recommended && !entity.recommended.tradeApprovalDate) {
217222
await this.userDataService.updateUserDataInternal(entity.recommended, {
218223
tradeApprovalDate: new Date(),
219224
});
220225

226+
await this.userDataService.createTradeApprovalLog(
227+
entity.recommended,
228+
TradeApprovalReason.RECOMMENDATION_CONFIRMED,
229+
);
230+
221231
const refCode =
222232
entity.kycStep && entity.method === RecommendationMethod.REF_CODE
223233
? entity.kycStep.getResult<KycRecommendationData>().key

0 commit comments

Comments
 (0)