From 03833d378ea9021a2b12357c175b61657fea1035 Mon Sep 17 00:00:00 2001 From: bernd2022 <104787072+bernd2022@users.noreply.github.com> Date: Wed, 25 Feb 2026 15:40:15 +0100 Subject: [PATCH 1/6] fix: round Firo balance to prevent floating-point precision errors (#3261) Summing UTXO amounts with reduce() can accumulate IEEE 754 rounding errors (e.g. 64.07538018000001 instead of 64.07538018). Apply roundAmount() to the result, consistent with all other amount calculations in the client. --- src/integration/blockchain/firo/firo-client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/integration/blockchain/firo/firo-client.ts b/src/integration/blockchain/firo/firo-client.ts index ca23ddc31c..55cbd8cfc3 100644 --- a/src/integration/blockchain/firo/firo-client.ts +++ b/src/integration/blockchain/firo/firo-client.ts @@ -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) From 7b698c8514a7c2a10c004695cb5bbb3a67ddaf6f Mon Sep 17 00:00:00 2001 From: bernd2022 <104787072+bernd2022@users.noreply.github.com> Date: Wed, 25 Feb 2026 15:54:08 +0100 Subject: [PATCH 2/6] feat: add feeAmountInChf column to TransactionEntity (#3262) --- ...26123966-AddFeeAmountInChfToTransaction.js | 26 +++++++++++++++++++ .../core/aml/services/aml.service.ts | 1 + .../payment/dto/update-transaction.dto.ts | 4 +++ .../payment/entities/transaction.entity.ts | 3 +++ 4 files changed, 34 insertions(+) create mode 100644 migration/1772026123966-AddFeeAmountInChfToTransaction.js diff --git a/migration/1772026123966-AddFeeAmountInChfToTransaction.js b/migration/1772026123966-AddFeeAmountInChfToTransaction.js new file mode 100644 index 0000000000..5b583a6a49 --- /dev/null +++ b/migration/1772026123966-AddFeeAmountInChfToTransaction.js @@ -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"`); + } +} diff --git a/src/subdomains/core/aml/services/aml.service.ts b/src/subdomains/core/aml/services/aml.service.ts index c4f23c1a0e..b759c8c401 100644 --- a/src/subdomains/core/aml/services/aml.service.ts +++ b/src/subdomains/core/aml/services/aml.service.ts @@ -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, diff --git a/src/subdomains/supporting/payment/dto/update-transaction.dto.ts b/src/subdomains/supporting/payment/dto/update-transaction.dto.ts index 4c3f3cf287..07f9076e65 100644 --- a/src/subdomains/supporting/payment/dto/update-transaction.dto.ts +++ b/src/subdomains/supporting/payment/dto/update-transaction.dto.ts @@ -21,6 +21,10 @@ export class UpdateTransactionDto { @IsNumber() amountInChf?: number; + @IsOptional() + @IsNumber() + feeAmountInChf?: number; + @IsOptional() @IsString() amlType?: string; diff --git a/src/subdomains/supporting/payment/entities/transaction.entity.ts b/src/subdomains/supporting/payment/entities/transaction.entity.ts index 43c37dcb0e..5befce087e 100644 --- a/src/subdomains/supporting/payment/entities/transaction.entity.ts +++ b/src/subdomains/supporting/payment/entities/transaction.entity.ts @@ -68,6 +68,9 @@ export class Transaction extends IEntity { @Column({ type: 'float', nullable: true }) amountInChf: number; + @Column({ type: 'float', nullable: true }) + feeAmountInChf: number; + @Column({ type: 'datetime2', nullable: true }) eventDate: Date; From 0592998cf3120dbe8d593f020c65345fbfd56738 Mon Sep 17 00:00:00 2001 From: Yannick <52333989+Yannick1712@users.noreply.github.com> Date: Wed, 25 Feb 2026 16:57:52 +0100 Subject: [PATCH 3/6] [NOTASK] recommendation refactoring (#3250) * [NOTASK] recommendation refacotring * [NOTASK] Refactoring 2 * [NOTASK] Refactoring * [NOTASK] Renaming * [NOTASK] AutoTradeApproval reason --- .../generic/kyc/services/kyc.service.ts | 7 ++-- .../generic/user/models/auth/auth.service.ts | 14 ++++++-- .../recommendation/recommendation.service.ts | 14 ++++++-- .../user/models/user-data/user-data.enum.ts | 10 ++++++ .../models/user-data/user-data.service.ts | 36 +++++++++++++++---- .../generic/user/models/user/user.service.ts | 15 +++++++- 6 files changed, 81 insertions(+), 15 deletions(-) diff --git a/src/subdomains/generic/kyc/services/kyc.service.ts b/src/subdomains/generic/kyc/services/kyc.service.ts index 3626acc275..cbc725621d 100644 --- a/src/subdomains/generic/kyc/services/kyc.service.ts +++ b/src/subdomains/generic/kyc/services/kyc.service.ts @@ -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'; @@ -1355,7 +1355,10 @@ export class KycService { } async completeRecommendation(userData: UserData): Promise { - 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[] { diff --git a/src/subdomains/generic/user/models/auth/auth.service.ts b/src/subdomains/generic/user/models/auth/auth.service.ts index af1dd580ca..644f538a7e 100644 --- a/src/subdomains/generic/user/models/auth/auth.service.ts +++ b/src/subdomains/generic/user/models/auth/auth.service.ts @@ -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'; @@ -402,10 +402,18 @@ export class AuthService { // --- HELPER METHODS --- // private async checkPendingRecommendation(userData: UserData, userWallet?: Wallet): Promise { - 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 { diff --git a/src/subdomains/generic/user/models/recommendation/recommendation.service.ts b/src/subdomains/generic/user/models/recommendation/recommendation.service.ts index 3e4d3309ad..e7342faeed 100644 --- a/src/subdomains/generic/user/models/recommendation/recommendation.service.ts +++ b/src/subdomains/generic/user/models/recommendation/recommendation.service.ts @@ -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'; @@ -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, @@ -213,11 +216,18 @@ export class RecommendationService { async updateRecommendationInternal(entity: Recommendation, update: Partial): Promise { 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().key 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..b8ab98e9b7 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 @@ -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', +} 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 7377f4e62f..0f2193c108 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 @@ -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 '; @@ -262,7 +262,7 @@ export class UserDataService { // --- CREATE / UPDATE --- async createUserData(dto: CreateUserDataDto): Promise { - 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)), @@ -270,9 +270,9 @@ export class UserDataService { 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 { @@ -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 = @@ -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); @@ -626,6 +629,18 @@ export class UserDataService { await this.userDataRepo.update(...userData.refreshLastCheckedTimestamp()); } + async createTradeApprovalLog( + userData: UserData, + reason: TradeApprovalReason, + tradeApprovalDate?: Date, + ): Promise { + 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 { @@ -828,6 +843,7 @@ export class UserDataService { } // --- HELPER METHODS --- // + private async loadRelationsAndVerify( userData: Partial | UserData, dto: UpdateUserDataDto | CreateUserDataDto, @@ -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)); diff --git a/src/subdomains/generic/user/models/user/user.service.ts b/src/subdomains/generic/user/models/user/user.service.ts index e92432e3ea..bb0a5147be 100644 --- a/src/subdomains/generic/user/models/user/user.service.ts +++ b/src/subdomains/generic/user/models/user/user.service.ts @@ -32,7 +32,14 @@ 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, + TradeApprovalReason, + 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'; @@ -259,6 +266,12 @@ export class UserService { tradeApprovalDate: user.wallet?.autoTradeApproval ? new Date() : undefined, })); + if (!data.userData && user.userData.tradeApprovalDate) + await this.userDataService.createTradeApprovalLog( + user.userData, + TradeApprovalReason.AUTO_TRADE_APPROVAL_USER_DATA_CREATED, + ); + if (user.userData.status === UserDataStatus.KYC_ONLY) await this.userDataService.updateUserDataInternal(user.userData, { status: UserDataStatus.NA }); From 6e1eb997b5b682f5ff2235107745235bfc992151 Mon Sep 17 00:00:00 2001 From: Lam Nguyen <32935491+xlamn@users.noreply.github.com> Date: Wed, 25 Feb 2026 17:16:17 +0100 Subject: [PATCH 4/6] feat: show latest price for historical prices. (#3223) --- .../supporting/realunit/realunit.service.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/subdomains/supporting/realunit/realunit.service.ts b/src/subdomains/supporting/realunit/realunit.service.ts index c537f267e4..2f0f270d09 100644 --- a/src/subdomains/supporting/realunit/realunit.service.ts +++ b/src/subdomains/supporting/realunit/realunit.service.ts @@ -195,12 +195,24 @@ export class RealUnitService { } async getHistoricalPrice(timeFrame: TimeFrame): Promise { - return this.historicalPriceCache.get(timeFrame, async () => { + const historicalPrices = await this.historicalPriceCache.get(timeFrame, async () => { const startDate = await this.getHistoricalPriceStartDate(timeFrame); const prices = await this.assetPricesService.getAssetPrices([await this.getRealuAsset()], startDate); const filledPrices = TimeseriesUtils.fillMissingDates(prices); return RealUnitDtoMapper.assetPricesToHistoricalPricesDto(filledPrices); }); + + if (historicalPrices.length > 0) { + const currentPrice = await this.getRealUnitPrice(); + historicalPrices[historicalPrices.length - 1] = { + timestamp: currentPrice.timestamp, + chf: currentPrice.chf, + eur: currentPrice.eur, + usd: currentPrice.usd, + }; + } + + return historicalPrices; } async getRealUnitInfo(): Promise { From 60395f893042d864b15d8e7866f1be4276d32bf3 Mon Sep 17 00:00:00 2001 From: David May <85513542+davidleomay@users.noreply.github.com> Date: Wed, 25 Feb 2026 17:51:04 +0100 Subject: [PATCH 5/6] fix: added TX hash for Scrypt (#3265) --- src/integration/exchange/services/scrypt.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/integration/exchange/services/scrypt.service.ts b/src/integration/exchange/services/scrypt.service.ts index 139ed61a48..cbbb11f0ae 100644 --- a/src/integration/exchange/services/scrypt.service.ts +++ b/src/integration/exchange/services/scrypt.service.ts @@ -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 })), }, ], }; From 951a13b54b515a16fbf5e793819ba9739f6eb666 Mon Sep 17 00:00:00 2001 From: Yannick <52333989+Yannick1712@users.noreply.github.com> Date: Wed, 25 Feb 2026 18:58:56 +0100 Subject: [PATCH 6/6] [DEV-4551] DfxApproval kycStep outdated check (#3216) * [DEV-4551] DfxApproval kycStep outdated check * [DEV-4551] Refactoring * [DEV-4551] Refactoring 2 * [DEV-4551] Small refactoring --- src/config/config.ts | 1 + .../generic/kyc/entities/kyc-step.entity.ts | 6 +- .../kyc/services/kyc-notification.service.ts | 66 ++++++++++--------- .../generic/kyc/services/kyc.service.ts | 23 +++++++ 4 files changed, 63 insertions(+), 33 deletions(-) diff --git a/src/config/config.ts b/src/config/config.ts index 70907aba4e..2ea061eb21 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -273,6 +273,7 @@ export class Configuration { residencePermitCountries: ['RU'], maxIdentTries: 7, maxRecommendationTries: 3, + kycStepExpiry: 90, // days }; fileDownloadConfig: { diff --git a/src/subdomains/generic/kyc/entities/kyc-step.entity.ts b/src/subdomains/generic/kyc/entities/kyc-step.entity.ts index 6aad7eff0a..251902d301 100644 --- a/src/subdomains/generic/kyc/entities/kyc-step.entity.ts +++ b/src/subdomains/generic/kyc/entities/kyc-step.entity.ts @@ -212,7 +212,7 @@ export class KycStep extends IEntity { const update: Partial = { status, result: this.setResult(result), - comment: comment ?? this.comment, + comment: this.addComment(comment), sequenceNumber, }; @@ -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; diff --git a/src/subdomains/generic/kyc/services/kyc-notification.service.ts b/src/subdomains/generic/kyc/services/kyc-notification.service.ts index b703f7279f..4bae8c48f7 100644 --- a/src/subdomains/generic/kyc/services/kyc-notification.service.ts +++ b/src/subdomains/generic/kyc/services/kyc-notification.service.ts @@ -28,10 +28,10 @@ export class KycNotificationService { @DfxCron(CronExpression.EVERY_HOUR, { process: Process.KYC_MAIL, timeout: 1800 }) async sendNotificationMails(): Promise { - await this.kycStepReminder(); + await this.autoKycStepReminder(); } - private async kycStepReminder(): Promise { + private async autoKycStepReminder(): Promise { const entities = await this.kycStepRepo.find({ where: { reminderSentDate: IsNull(), @@ -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) { @@ -88,6 +59,37 @@ export class KycNotificationService { } } + async kycStepReminder(userData: UserData): Promise { + 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 { try { if ((userData.mail, !DisabledProcess(Process.KYC_MAIL))) { diff --git a/src/subdomains/generic/kyc/services/kyc.service.ts b/src/subdomains/generic/kyc/services/kyc.service.ts index cbc725621d..6faf3cdf52 100644 --- a/src/subdomains/generic/kyc/services/kyc.service.ts +++ b/src/subdomains/generic/kyc/services/kyc.service.ts @@ -382,6 +382,29 @@ export class KycService { } async checkDfxApproval(kycStep: KycStep): Promise { + 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), );