From 8e38387db2a1c4150cbd0496bbc1d478f9af55f0 Mon Sep 17 00:00:00 2001 From: Bernd Date: Wed, 13 May 2026 14:12:25 +0200 Subject: [PATCH 1/4] feat(compliance): add manual AML pass endpoints for BuyCrypto and BuyFiat Adds PUT :id/amlCheck/pass endpoints (COMPLIANCE role) that allow a compliance clerk to manually pass a pending AML check when all errors on the transaction comment are on the whitelist (phone-/referral-/ country-related). The ManualPassWhitelistErrors list and canManualPass helper are kept in sync with packages/core/src/definitions/compliance.ts. --- .../core/aml/enums/aml-error.enum.ts | 21 +++++++++++++++++++ .../process/buy-crypto.controller.ts | 9 ++++++++ .../process/dto/manual-pass-aml-check.dto.ts | 7 +++++++ .../process/services/buy-crypto.service.ts | 11 ++++++++++ .../process/buy-fiat.controller.ts | 9 ++++++++ .../process/dto/manual-pass-aml-check.dto.ts | 7 +++++++ .../process/services/buy-fiat.service.ts | 11 ++++++++++ 7 files changed, 75 insertions(+) create mode 100644 src/subdomains/core/buy-crypto/process/dto/manual-pass-aml-check.dto.ts create mode 100644 src/subdomains/core/sell-crypto/process/dto/manual-pass-aml-check.dto.ts diff --git a/src/subdomains/core/aml/enums/aml-error.enum.ts b/src/subdomains/core/aml/enums/aml-error.enum.ts index 30c74e084c..43ecce0e8d 100644 --- a/src/subdomains/core/aml/enums/aml-error.enum.ts +++ b/src/subdomains/core/aml/enums/aml-error.enum.ts @@ -83,6 +83,27 @@ export const DelayResultError = [ AmlError.BANK_RELEASE_DATE_MISSING, ]; +// Keep in sync with packages/core/src/definitions/compliance.ts (ManualPassWhitelistErrors / canManualPass) +export const ManualPassWhitelistErrors: AmlError[] = [ + AmlError.PHONE_VERIFICATION_NEEDED, + AmlError.IP_PHONE_VERIFICATION_NEEDED, + AmlError.BIC_PHONE_VERIFICATION_NEEDED, + AmlError.IBAN_PHONE_VERIFICATION_NEEDED, + AmlError.IP_COUNTRY_MISMATCH, + AmlError.TRADE_APPROVAL_DATE_MISSING, + AmlError.USER_DATA_FAILED_CALL, + AmlError.USER_DATA_REJECTED_CALL, + AmlError.REFERRAL_NO_TRADE_HISTORY, +]; + +export function canManualPass(comment: string | null | undefined): boolean { + const errors = (comment ?? '') + .split(';') + .map((e) => e.trim()) + .filter(Boolean); + return errors.length > 0 && errors.every((e) => ManualPassWhitelistErrors.includes(e as AmlError)); +} + export enum AmlErrorType { SINGLE = 'Single', // Only one error may occur MULTI = 'Multi', // All errors must have the same amlCheck diff --git a/src/subdomains/core/buy-crypto/process/buy-crypto.controller.ts b/src/subdomains/core/buy-crypto/process/buy-crypto.controller.ts index 6e36bef904..3ed08b1cb5 100644 --- a/src/subdomains/core/buy-crypto/process/buy-crypto.controller.ts +++ b/src/subdomains/core/buy-crypto/process/buy-crypto.controller.ts @@ -5,6 +5,7 @@ import { RoleGuard } from 'src/shared/auth/role.guard'; import { UserActiveGuard } from 'src/shared/auth/user-active.guard'; import { UserRole } from 'src/shared/auth/user-role.enum'; import { RefundInternalDto } from '../../history/dto/refund-internal.dto'; +import { ManualPassAmlCheckDto } from './dto/manual-pass-aml-check.dto'; import { UpdateBuyCryptoDto } from './dto/update-buy-crypto.dto'; import { BuyCrypto } from './entities/buy-crypto.entity'; import { BuyCryptoWebhookService } from './services/buy-crypto-webhook.service'; @@ -65,4 +66,12 @@ export class BuyCryptoController { async resetAmlCheck(@Param('id') id: string): Promise { return this.buyCryptoService.resetAmlCheck(+id); } + + @Put(':id/amlCheck/pass') + @ApiBearerAuth() + @ApiExcludeEndpoint() + @UseGuards(AuthGuard(), RoleGuard(UserRole.COMPLIANCE), UserActiveGuard()) + async manualPassAmlCheck(@Param('id') id: string, @Body() dto: ManualPassAmlCheckDto): Promise { + return this.buyCryptoService.manualPassAmlCheck(+id, dto.responsible); + } } diff --git a/src/subdomains/core/buy-crypto/process/dto/manual-pass-aml-check.dto.ts b/src/subdomains/core/buy-crypto/process/dto/manual-pass-aml-check.dto.ts new file mode 100644 index 0000000000..a894331cce --- /dev/null +++ b/src/subdomains/core/buy-crypto/process/dto/manual-pass-aml-check.dto.ts @@ -0,0 +1,7 @@ +import { IsNotEmpty, IsString } from 'class-validator'; + +export class ManualPassAmlCheckDto { + @IsNotEmpty() + @IsString() + responsible: string; +} diff --git a/src/subdomains/core/buy-crypto/process/services/buy-crypto.service.ts b/src/subdomains/core/buy-crypto/process/services/buy-crypto.service.ts index bd974e7fdb..008f853752 100644 --- a/src/subdomains/core/buy-crypto/process/services/buy-crypto.service.ts +++ b/src/subdomains/core/buy-crypto/process/services/buy-crypto.service.ts @@ -53,6 +53,7 @@ import { TransactionRequestService } from 'src/subdomains/supporting/payment/ser import { TransactionService } from 'src/subdomains/supporting/payment/services/transaction.service'; import { PriceValidity } from 'src/subdomains/supporting/pricing/services/pricing.service'; import { Between, FindOptionsRelations, In, IsNull, MoreThan, Not } from 'typeorm'; +import { canManualPass } from '../../../aml/enums/aml-error.enum'; import { AmlReason } from '../../../aml/enums/aml-reason.enum'; import { CheckStatus } from '../../../aml/enums/check-status.enum'; import { Buy } from '../../routes/buy/buy.entity'; @@ -682,6 +683,16 @@ export class BuyCryptoService { if (fiatOutputId) await this.fiatOutputService.delete(fiatOutputId); } + async manualPassAmlCheck(id: number, responsible: string): Promise { + const entity = await this.buyCryptoRepo.findOneBy({ id }); + if (!entity) throw new NotFoundException('BuyCrypto not found'); + if (entity.amlCheck !== CheckStatus.PENDING) throw new BadRequestException('BuyCrypto amlCheck must be Pending'); + if (!canManualPass(entity.comment)) + throw new BadRequestException('Manual pass only allowed when all errors are phone-related'); + + return this.update(id, { amlCheck: CheckStatus.PASS, amlResponsible: responsible } as UpdateBuyCryptoDto); + } + async getUserVolume( userIds: number[], dateFrom: Date = new Date(0), diff --git a/src/subdomains/core/sell-crypto/process/buy-fiat.controller.ts b/src/subdomains/core/sell-crypto/process/buy-fiat.controller.ts index 3d2d7a0859..1be9d4c0de 100644 --- a/src/subdomains/core/sell-crypto/process/buy-fiat.controller.ts +++ b/src/subdomains/core/sell-crypto/process/buy-fiat.controller.ts @@ -6,6 +6,7 @@ import { UserActiveGuard } from 'src/shared/auth/user-active.guard'; import { UserRole } from 'src/shared/auth/user-role.enum'; import { RefundInternalDto } from '../../history/dto/refund-internal.dto'; import { BuyFiat } from './buy-fiat.entity'; +import { ManualPassAmlCheckDto } from './dto/manual-pass-aml-check.dto'; import { UpdateBuyFiatDto } from './dto/update-buy-fiat.dto'; import { BuyFiatService } from './services/buy-fiat.service'; @@ -61,4 +62,12 @@ export class BuyFiatController { async resetAmlCheck(@Param('id') id: string): Promise { return this.buyFiatService.resetAmlCheck(+id); } + + @Put(':id/amlCheck/pass') + @ApiBearerAuth() + @ApiExcludeEndpoint() + @UseGuards(AuthGuard(), RoleGuard(UserRole.COMPLIANCE), UserActiveGuard()) + async manualPassAmlCheck(@Param('id') id: string, @Body() dto: ManualPassAmlCheckDto): Promise { + return this.buyFiatService.manualPassAmlCheck(+id, dto.responsible); + } } diff --git a/src/subdomains/core/sell-crypto/process/dto/manual-pass-aml-check.dto.ts b/src/subdomains/core/sell-crypto/process/dto/manual-pass-aml-check.dto.ts new file mode 100644 index 0000000000..a894331cce --- /dev/null +++ b/src/subdomains/core/sell-crypto/process/dto/manual-pass-aml-check.dto.ts @@ -0,0 +1,7 @@ +import { IsNotEmpty, IsString } from 'class-validator'; + +export class ManualPassAmlCheckDto { + @IsNotEmpty() + @IsString() + responsible: string; +} diff --git a/src/subdomains/core/sell-crypto/process/services/buy-fiat.service.ts b/src/subdomains/core/sell-crypto/process/services/buy-fiat.service.ts index 925582e953..030037d0d9 100644 --- a/src/subdomains/core/sell-crypto/process/services/buy-fiat.service.ts +++ b/src/subdomains/core/sell-crypto/process/services/buy-fiat.service.ts @@ -27,6 +27,7 @@ import { SupportLogType } from 'src/subdomains/supporting/support-issue/enums/su import { SupportLogService } from 'src/subdomains/supporting/support-issue/services/support-log.service'; import { Between, FindOptionsRelations, In, MoreThan } from 'typeorm'; import { FiatOutputService } from '../../../../supporting/fiat-output/fiat-output.service'; +import { canManualPass } from '../../../aml/enums/aml-error.enum'; import { AmlReason } from '../../../aml/enums/aml-reason.enum'; import { CheckStatus } from '../../../aml/enums/check-status.enum'; import { BuyCryptoService } from '../../../buy-crypto/process/services/buy-crypto.service'; @@ -415,6 +416,16 @@ export class BuyFiatService { } } + async manualPassAmlCheck(id: number, responsible: string): Promise { + const entity = await this.buyFiatRepo.findOneBy({ id }); + if (!entity) throw new NotFoundException('BuyFiat not found'); + if (entity.amlCheck !== CheckStatus.PENDING) throw new BadRequestException('BuyFiat amlCheck must be Pending'); + if (!canManualPass(entity.comment)) + throw new BadRequestException('Manual pass only allowed when all errors are phone-related'); + + return this.update(id, { amlCheck: CheckStatus.PASS, amlResponsible: responsible } as UpdateBuyFiatDto); + } + async updateVolumes(start = 1, end = 100000): Promise { const sellIds = await this.buyFiatRepo .find({ From 0c31fc34b5e79095a2dd18de66ebc1a36d985de7 Mon Sep 17 00:00:00 2001 From: Bernd Date: Mon, 18 May 2026 08:47:57 +0200 Subject: [PATCH 2/4] refactor(compliance): apply review feedback on manual AML check endpoint - merge endpoint into PUT :id/amlCheck (drop /pass suffix) - add amlCheck field to DTO and gate canManualPass on PASS only - centralize DTO under aml/dto and rename to ManualAmlCheckDto --- src/subdomains/core/aml/dto/manual-aml-check.dto.ts | 12 ++++++++++++ .../core/buy-crypto/process/buy-crypto.controller.ts | 8 ++++---- .../process/dto/manual-pass-aml-check.dto.ts | 7 ------- .../process/services/buy-crypto.service.ts | 7 ++++--- .../core/sell-crypto/process/buy-fiat.controller.ts | 8 ++++---- .../process/dto/manual-pass-aml-check.dto.ts | 7 ------- .../sell-crypto/process/services/buy-fiat.service.ts | 7 ++++--- 7 files changed, 28 insertions(+), 28 deletions(-) create mode 100644 src/subdomains/core/aml/dto/manual-aml-check.dto.ts delete mode 100644 src/subdomains/core/buy-crypto/process/dto/manual-pass-aml-check.dto.ts delete mode 100644 src/subdomains/core/sell-crypto/process/dto/manual-pass-aml-check.dto.ts diff --git a/src/subdomains/core/aml/dto/manual-aml-check.dto.ts b/src/subdomains/core/aml/dto/manual-aml-check.dto.ts new file mode 100644 index 0000000000..05156befb9 --- /dev/null +++ b/src/subdomains/core/aml/dto/manual-aml-check.dto.ts @@ -0,0 +1,12 @@ +import { IsEnum, IsNotEmpty, IsString } from 'class-validator'; +import { CheckStatus } from '../enums/check-status.enum'; + +export class ManualAmlCheckDto { + @IsNotEmpty() + @IsEnum(CheckStatus) + amlCheck: CheckStatus; + + @IsNotEmpty() + @IsString() + responsible: string; +} diff --git a/src/subdomains/core/buy-crypto/process/buy-crypto.controller.ts b/src/subdomains/core/buy-crypto/process/buy-crypto.controller.ts index 3ed08b1cb5..282687d3d3 100644 --- a/src/subdomains/core/buy-crypto/process/buy-crypto.controller.ts +++ b/src/subdomains/core/buy-crypto/process/buy-crypto.controller.ts @@ -5,7 +5,7 @@ import { RoleGuard } from 'src/shared/auth/role.guard'; import { UserActiveGuard } from 'src/shared/auth/user-active.guard'; import { UserRole } from 'src/shared/auth/user-role.enum'; import { RefundInternalDto } from '../../history/dto/refund-internal.dto'; -import { ManualPassAmlCheckDto } from './dto/manual-pass-aml-check.dto'; +import { ManualAmlCheckDto } from '../../aml/dto/manual-aml-check.dto'; import { UpdateBuyCryptoDto } from './dto/update-buy-crypto.dto'; import { BuyCrypto } from './entities/buy-crypto.entity'; import { BuyCryptoWebhookService } from './services/buy-crypto-webhook.service'; @@ -67,11 +67,11 @@ export class BuyCryptoController { return this.buyCryptoService.resetAmlCheck(+id); } - @Put(':id/amlCheck/pass') + @Put(':id/amlCheck') @ApiBearerAuth() @ApiExcludeEndpoint() @UseGuards(AuthGuard(), RoleGuard(UserRole.COMPLIANCE), UserActiveGuard()) - async manualPassAmlCheck(@Param('id') id: string, @Body() dto: ManualPassAmlCheckDto): Promise { - return this.buyCryptoService.manualPassAmlCheck(+id, dto.responsible); + async manualPassAmlCheck(@Param('id') id: string, @Body() dto: ManualAmlCheckDto): Promise { + return this.buyCryptoService.manualPassAmlCheck(+id, dto); } } diff --git a/src/subdomains/core/buy-crypto/process/dto/manual-pass-aml-check.dto.ts b/src/subdomains/core/buy-crypto/process/dto/manual-pass-aml-check.dto.ts deleted file mode 100644 index a894331cce..0000000000 --- a/src/subdomains/core/buy-crypto/process/dto/manual-pass-aml-check.dto.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { IsNotEmpty, IsString } from 'class-validator'; - -export class ManualPassAmlCheckDto { - @IsNotEmpty() - @IsString() - responsible: string; -} diff --git a/src/subdomains/core/buy-crypto/process/services/buy-crypto.service.ts b/src/subdomains/core/buy-crypto/process/services/buy-crypto.service.ts index 008f853752..c06d08efbc 100644 --- a/src/subdomains/core/buy-crypto/process/services/buy-crypto.service.ts +++ b/src/subdomains/core/buy-crypto/process/services/buy-crypto.service.ts @@ -60,6 +60,7 @@ import { Buy } from '../../routes/buy/buy.entity'; import { BuyRepository } from '../../routes/buy/buy.repository'; import { BuyService } from '../../routes/buy/buy.service'; import { BuyHistoryDto } from '../../routes/buy/dto/buy-history.dto'; +import { ManualAmlCheckDto } from '../../../aml/dto/manual-aml-check.dto'; import { UpdateBuyCryptoDto } from '../dto/update-buy-crypto.dto'; import { BuyCrypto, BuyCryptoEditableAmlCheck, BuyCryptoStatus } from '../entities/buy-crypto.entity'; import { BuyCryptoRepository } from '../repositories/buy-crypto.repository'; @@ -683,14 +684,14 @@ export class BuyCryptoService { if (fiatOutputId) await this.fiatOutputService.delete(fiatOutputId); } - async manualPassAmlCheck(id: number, responsible: string): Promise { + async manualPassAmlCheck(id: number, dto: ManualAmlCheckDto): Promise { const entity = await this.buyCryptoRepo.findOneBy({ id }); if (!entity) throw new NotFoundException('BuyCrypto not found'); if (entity.amlCheck !== CheckStatus.PENDING) throw new BadRequestException('BuyCrypto amlCheck must be Pending'); - if (!canManualPass(entity.comment)) + if (dto.amlCheck === CheckStatus.PASS && !canManualPass(entity.comment)) throw new BadRequestException('Manual pass only allowed when all errors are phone-related'); - return this.update(id, { amlCheck: CheckStatus.PASS, amlResponsible: responsible } as UpdateBuyCryptoDto); + return this.update(id, { amlCheck: dto.amlCheck, amlResponsible: dto.responsible } as UpdateBuyCryptoDto); } async getUserVolume( diff --git a/src/subdomains/core/sell-crypto/process/buy-fiat.controller.ts b/src/subdomains/core/sell-crypto/process/buy-fiat.controller.ts index 1be9d4c0de..c3ba8195cb 100644 --- a/src/subdomains/core/sell-crypto/process/buy-fiat.controller.ts +++ b/src/subdomains/core/sell-crypto/process/buy-fiat.controller.ts @@ -6,7 +6,7 @@ import { UserActiveGuard } from 'src/shared/auth/user-active.guard'; import { UserRole } from 'src/shared/auth/user-role.enum'; import { RefundInternalDto } from '../../history/dto/refund-internal.dto'; import { BuyFiat } from './buy-fiat.entity'; -import { ManualPassAmlCheckDto } from './dto/manual-pass-aml-check.dto'; +import { ManualAmlCheckDto } from '../../aml/dto/manual-aml-check.dto'; import { UpdateBuyFiatDto } from './dto/update-buy-fiat.dto'; import { BuyFiatService } from './services/buy-fiat.service'; @@ -63,11 +63,11 @@ export class BuyFiatController { return this.buyFiatService.resetAmlCheck(+id); } - @Put(':id/amlCheck/pass') + @Put(':id/amlCheck') @ApiBearerAuth() @ApiExcludeEndpoint() @UseGuards(AuthGuard(), RoleGuard(UserRole.COMPLIANCE), UserActiveGuard()) - async manualPassAmlCheck(@Param('id') id: string, @Body() dto: ManualPassAmlCheckDto): Promise { - return this.buyFiatService.manualPassAmlCheck(+id, dto.responsible); + async manualPassAmlCheck(@Param('id') id: string, @Body() dto: ManualAmlCheckDto): Promise { + return this.buyFiatService.manualPassAmlCheck(+id, dto); } } diff --git a/src/subdomains/core/sell-crypto/process/dto/manual-pass-aml-check.dto.ts b/src/subdomains/core/sell-crypto/process/dto/manual-pass-aml-check.dto.ts deleted file mode 100644 index a894331cce..0000000000 --- a/src/subdomains/core/sell-crypto/process/dto/manual-pass-aml-check.dto.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { IsNotEmpty, IsString } from 'class-validator'; - -export class ManualPassAmlCheckDto { - @IsNotEmpty() - @IsString() - responsible: string; -} diff --git a/src/subdomains/core/sell-crypto/process/services/buy-fiat.service.ts b/src/subdomains/core/sell-crypto/process/services/buy-fiat.service.ts index 030037d0d9..cd30265fa3 100644 --- a/src/subdomains/core/sell-crypto/process/services/buy-fiat.service.ts +++ b/src/subdomains/core/sell-crypto/process/services/buy-fiat.service.ts @@ -40,6 +40,7 @@ import { SellRepository } from '../../route/sell.repository'; import { SellService } from '../../route/sell.service'; import { BuyFiat, BuyFiatEditableAmlCheck } from '../buy-fiat.entity'; import { BuyFiatRepository } from '../buy-fiat.repository'; +import { ManualAmlCheckDto } from '../../../aml/dto/manual-aml-check.dto'; import { UpdateBuyFiatDto } from '../dto/update-buy-fiat.dto'; import { BuyFiatNotificationService } from './buy-fiat-notification.service'; @@ -416,14 +417,14 @@ export class BuyFiatService { } } - async manualPassAmlCheck(id: number, responsible: string): Promise { + async manualPassAmlCheck(id: number, dto: ManualAmlCheckDto): Promise { const entity = await this.buyFiatRepo.findOneBy({ id }); if (!entity) throw new NotFoundException('BuyFiat not found'); if (entity.amlCheck !== CheckStatus.PENDING) throw new BadRequestException('BuyFiat amlCheck must be Pending'); - if (!canManualPass(entity.comment)) + if (dto.amlCheck === CheckStatus.PASS && !canManualPass(entity.comment)) throw new BadRequestException('Manual pass only allowed when all errors are phone-related'); - return this.update(id, { amlCheck: CheckStatus.PASS, amlResponsible: responsible } as UpdateBuyFiatDto); + return this.update(id, { amlCheck: dto.amlCheck, amlResponsible: dto.responsible } as UpdateBuyFiatDto); } async updateVolumes(start = 1, end = 100000): Promise { From eedefb22aac4bdf8320106c582b0cef906864c01 Mon Sep 17 00:00:00 2001 From: Bernd Date: Mon, 18 May 2026 09:39:14 +0200 Subject: [PATCH 3/4] refactor(compliance): tighten manual AML check preconditions - reject when entity is complete or chargeback initiated by user - forbid only finalized amlCheck (PASS/FAIL) instead of allowing only PENDING --- .../core/buy-crypto/process/services/buy-crypto.service.ts | 5 ++++- .../core/sell-crypto/process/services/buy-fiat.service.ts | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/subdomains/core/buy-crypto/process/services/buy-crypto.service.ts b/src/subdomains/core/buy-crypto/process/services/buy-crypto.service.ts index c06d08efbc..46ee0958e8 100644 --- a/src/subdomains/core/buy-crypto/process/services/buy-crypto.service.ts +++ b/src/subdomains/core/buy-crypto/process/services/buy-crypto.service.ts @@ -687,7 +687,10 @@ export class BuyCryptoService { async manualPassAmlCheck(id: number, dto: ManualAmlCheckDto): Promise { const entity = await this.buyCryptoRepo.findOneBy({ id }); if (!entity) throw new NotFoundException('BuyCrypto not found'); - if (entity.amlCheck !== CheckStatus.PENDING) throw new BadRequestException('BuyCrypto amlCheck must be Pending'); + if (entity.isComplete || entity.chargebackAllowedDateUser) + throw new BadRequestException('BuyCrypto is already complete or chargeback initiated'); + if ([CheckStatus.PASS, CheckStatus.FAIL].includes(entity.amlCheck)) + throw new BadRequestException('BuyCrypto amlCheck is already finalized'); if (dto.amlCheck === CheckStatus.PASS && !canManualPass(entity.comment)) throw new BadRequestException('Manual pass only allowed when all errors are phone-related'); diff --git a/src/subdomains/core/sell-crypto/process/services/buy-fiat.service.ts b/src/subdomains/core/sell-crypto/process/services/buy-fiat.service.ts index cd30265fa3..7d2f0bb449 100644 --- a/src/subdomains/core/sell-crypto/process/services/buy-fiat.service.ts +++ b/src/subdomains/core/sell-crypto/process/services/buy-fiat.service.ts @@ -420,7 +420,10 @@ export class BuyFiatService { async manualPassAmlCheck(id: number, dto: ManualAmlCheckDto): Promise { const entity = await this.buyFiatRepo.findOneBy({ id }); if (!entity) throw new NotFoundException('BuyFiat not found'); - if (entity.amlCheck !== CheckStatus.PENDING) throw new BadRequestException('BuyFiat amlCheck must be Pending'); + if (entity.isComplete || entity.chargebackAllowedDateUser) + throw new BadRequestException('BuyFiat is already complete or chargeback initiated'); + if ([CheckStatus.PASS, CheckStatus.FAIL].includes(entity.amlCheck)) + throw new BadRequestException('BuyFiat amlCheck is already finalized'); if (dto.amlCheck === CheckStatus.PASS && !canManualPass(entity.comment)) throw new BadRequestException('Manual pass only allowed when all errors are phone-related'); From a5242f21a095baa94ffb9f96688c72c82280669e Mon Sep 17 00:00:00 2001 From: Bernd Date: Mon, 18 May 2026 10:43:49 +0200 Subject: [PATCH 4/4] refactor(compliance): set amlReason and priceDefinitionAllowedDate on manual PASS - add optional amlReason field to ManualAmlCheckDto - on PASS: force amlReason to NA and stamp priceDefinitionAllowedDate - on non-PASS: forward optional amlReason from DTO --- src/subdomains/core/aml/dto/manual-aml-check.dto.ts | 7 ++++++- .../core/buy-crypto/process/services/buy-crypto.service.ts | 7 ++++++- .../core/sell-crypto/process/services/buy-fiat.service.ts | 7 ++++++- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/subdomains/core/aml/dto/manual-aml-check.dto.ts b/src/subdomains/core/aml/dto/manual-aml-check.dto.ts index 05156befb9..9829bcc211 100644 --- a/src/subdomains/core/aml/dto/manual-aml-check.dto.ts +++ b/src/subdomains/core/aml/dto/manual-aml-check.dto.ts @@ -1,4 +1,5 @@ -import { IsEnum, IsNotEmpty, IsString } from 'class-validator'; +import { IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator'; +import { AmlReason } from '../enums/aml-reason.enum'; import { CheckStatus } from '../enums/check-status.enum'; export class ManualAmlCheckDto { @@ -6,6 +7,10 @@ export class ManualAmlCheckDto { @IsEnum(CheckStatus) amlCheck: CheckStatus; + @IsOptional() + @IsEnum(AmlReason) + amlReason?: AmlReason; + @IsNotEmpty() @IsString() responsible: string; diff --git a/src/subdomains/core/buy-crypto/process/services/buy-crypto.service.ts b/src/subdomains/core/buy-crypto/process/services/buy-crypto.service.ts index 46ee0958e8..50664c7b23 100644 --- a/src/subdomains/core/buy-crypto/process/services/buy-crypto.service.ts +++ b/src/subdomains/core/buy-crypto/process/services/buy-crypto.service.ts @@ -694,7 +694,12 @@ export class BuyCryptoService { if (dto.amlCheck === CheckStatus.PASS && !canManualPass(entity.comment)) throw new BadRequestException('Manual pass only allowed when all errors are phone-related'); - return this.update(id, { amlCheck: dto.amlCheck, amlResponsible: dto.responsible } as UpdateBuyCryptoDto); + return this.update(id, { + amlCheck: dto.amlCheck, + amlResponsible: dto.responsible, + amlReason: dto.amlCheck === CheckStatus.PASS ? AmlReason.NA : dto.amlReason, + priceDefinitionAllowedDate: dto.amlCheck === CheckStatus.PASS ? new Date() : undefined, + } as UpdateBuyCryptoDto); } async getUserVolume( diff --git a/src/subdomains/core/sell-crypto/process/services/buy-fiat.service.ts b/src/subdomains/core/sell-crypto/process/services/buy-fiat.service.ts index 7d2f0bb449..a8cdcb6434 100644 --- a/src/subdomains/core/sell-crypto/process/services/buy-fiat.service.ts +++ b/src/subdomains/core/sell-crypto/process/services/buy-fiat.service.ts @@ -427,7 +427,12 @@ export class BuyFiatService { if (dto.amlCheck === CheckStatus.PASS && !canManualPass(entity.comment)) throw new BadRequestException('Manual pass only allowed when all errors are phone-related'); - return this.update(id, { amlCheck: dto.amlCheck, amlResponsible: dto.responsible } as UpdateBuyFiatDto); + return this.update(id, { + amlCheck: dto.amlCheck, + amlResponsible: dto.responsible, + amlReason: dto.amlCheck === CheckStatus.PASS ? AmlReason.NA : dto.amlReason, + priceDefinitionAllowedDate: dto.amlCheck === CheckStatus.PASS ? new Date() : undefined, + } as UpdateBuyFiatDto); } async updateVolumes(start = 1, end = 100000): Promise {