diff --git a/migration/1781006197061-AddAmlSource.js b/migration/1781006197061-AddAmlSource.js new file mode 100644 index 0000000000..8a838b8a31 --- /dev/null +++ b/migration/1781006197061-AddAmlSource.js @@ -0,0 +1,28 @@ +/** + * @typedef {import('typeorm').MigrationInterface} MigrationInterface + * @typedef {import('typeorm').QueryRunner} QueryRunner + */ + +/** + * @class + * @implements {MigrationInterface} + */ +module.exports = class AddAmlSource1781006197061 { + name = 'AddAmlSource1781006197061' + + /** + * @param {QueryRunner} queryRunner + */ + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "buy_crypto" ADD "amlSource" character varying(256)`); + await queryRunner.query(`ALTER TABLE "buy_fiat" ADD "amlSource" character varying(256)`); + } + + /** + * @param {QueryRunner} queryRunner + */ + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "buy_fiat" DROP COLUMN "amlSource"`); + await queryRunner.query(`ALTER TABLE "buy_crypto" DROP COLUMN "amlSource"`); + } +} diff --git a/src/config/config.ts b/src/config/config.ts index 290d3faef5..195eb71a72 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -52,6 +52,7 @@ export class Configuration { txRequestWaitingExpiryDays = 7; txRequestValidityMinutes = 30; financeLogTotalBalanceChangeLimit = 5000; + amlGsAccountId = process.env.AML_GS_ACCOUNT_ID ? +process.env.AML_GS_ACCOUNT_ID : undefined; // userData id of the GSheet technical account faucetAmount = 0.0005; // ETH faucetEnabled = process.env.FAUCET_ENABLED === 'true' || [Environment.DEV, Environment.LOC].includes(this.environment); diff --git a/src/subdomains/core/aml/enums/aml-source.enum.ts b/src/subdomains/core/aml/enums/aml-source.enum.ts new file mode 100644 index 0000000000..d8825861b2 --- /dev/null +++ b/src/subdomains/core/aml/enums/aml-source.enum.ts @@ -0,0 +1,5 @@ +export enum AmlSource { + AUTOMATIC = 'Automatic', + GSHEET = 'GSheet', + COMPLIANCE = 'Compliance', +} diff --git a/src/subdomains/core/aml/services/aml-helper.service.ts b/src/subdomains/core/aml/services/aml-helper.service.ts index a0411b3f4b..dcb177852e 100644 --- a/src/subdomains/core/aml/services/aml-helper.service.ts +++ b/src/subdomains/core/aml/services/aml-helper.service.ts @@ -28,6 +28,7 @@ import { BuyCrypto } from '../../buy-crypto/process/entities/buy-crypto.entity'; import { BuyFiat } from '../../sell-crypto/process/buy-fiat.entity'; import { AmlError, AmlErrorResult, AmlErrorType, DelayResultError } from '../enums/aml-error.enum'; import { AmlReason, KycAmlReasons, RecheckAmlReasons } from '../enums/aml-reason.enum'; +import { AmlSource } from '../enums/aml-source.enum'; import { AmlRule, SpecialIpCountries } from '../enums/aml-rule.enum'; import { CheckStatus } from '../enums/check-status.enum'; @@ -603,6 +604,7 @@ export class AmlHelperService { amlReason?: AmlReason; comment?: string; amlResponsible?: string; + amlSource?: AmlSource; priceDefinitionAllowedDate?: Date; } { const amlErrors = this.getAmlErrors( @@ -634,6 +636,7 @@ export class AmlHelperService { amlCheck: CheckStatus.PASS, amlReason: AmlReason.NA, amlResponsible: 'API', + amlSource: AmlSource.AUTOMATIC, priceDefinitionAllowedDate: new Date(), }; @@ -641,7 +644,8 @@ export class AmlHelperService { // Expired pending amlChecks if (entity.amlCheck === CheckStatus.PENDING) { - if (Util.daysDiff(entity.created) > 14) return { amlCheck: CheckStatus.FAIL, amlResponsible: 'API' }; + if (Util.daysDiff(entity.created) > 14) + return { amlCheck: CheckStatus.FAIL, amlResponsible: 'API', amlSource: AmlSource.AUTOMATIC }; if ( !RecheckAmlReasons.includes(entity.amlReason) || comment === entity.comment || @@ -668,6 +672,7 @@ export class AmlHelperService { amlReason: crucialErrorResult.amlReason, comment, amlResponsible: 'API', + amlSource: AmlSource.AUTOMATIC, } : { bankData, comment }; } @@ -690,6 +695,7 @@ export class AmlHelperService { amlReason: amlResults[0].amlReason, comment, amlResponsible: 'API', + amlSource: AmlSource.AUTOMATIC, }; // GSheet 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 282687d3d3..7106b356dc 100644 --- a/src/subdomains/core/buy-crypto/process/buy-crypto.controller.ts +++ b/src/subdomains/core/buy-crypto/process/buy-crypto.controller.ts @@ -1,6 +1,8 @@ import { Body, Controller, Delete, Param, Post, Put, Query, UseGuards } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { ApiBearerAuth, ApiExcludeEndpoint, ApiTags } from '@nestjs/swagger'; +import { GetJwt } from 'src/shared/auth/get-jwt.decorator'; +import { JwtPayload } from 'src/shared/auth/jwt-payload.interface'; 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'; @@ -55,8 +57,12 @@ export class BuyCryptoController { @ApiBearerAuth() @ApiExcludeEndpoint() @UseGuards(AuthGuard(), RoleGuard(UserRole.COMPLIANCE), UserActiveGuard()) - async update(@Param('id') id: string, @Body() dto: UpdateBuyCryptoDto): Promise { - return this.buyCryptoService.update(+id, dto); + async update( + @Param('id') id: string, + @Body() dto: UpdateBuyCryptoDto, + @GetJwt() jwt: JwtPayload, + ): Promise { + return this.buyCryptoService.update(+id, dto, jwt); } @Delete(':id/amlCheck') @@ -71,7 +77,11 @@ export class BuyCryptoController { @ApiBearerAuth() @ApiExcludeEndpoint() @UseGuards(AuthGuard(), RoleGuard(UserRole.COMPLIANCE), UserActiveGuard()) - async manualPassAmlCheck(@Param('id') id: string, @Body() dto: ManualAmlCheckDto): Promise { - return this.buyCryptoService.manualPassAmlCheck(+id, dto); + async manualPassAmlCheck( + @Param('id') id: string, + @Body() dto: ManualAmlCheckDto, + @GetJwt() jwt: JwtPayload, + ): Promise { + return this.buyCryptoService.manualPassAmlCheck(+id, dto, jwt); } } diff --git a/src/subdomains/core/buy-crypto/process/entities/buy-crypto.entity.ts b/src/subdomains/core/buy-crypto/process/entities/buy-crypto.entity.ts index dbe70ad41f..7a96c1e500 100644 --- a/src/subdomains/core/buy-crypto/process/entities/buy-crypto.entity.ts +++ b/src/subdomains/core/buy-crypto/process/entities/buy-crypto.entity.ts @@ -38,6 +38,7 @@ import { Price, PriceStep } from 'src/subdomains/supporting/pricing/domain/entit import { PriceCurrency } from 'src/subdomains/supporting/pricing/services/pricing.service'; import { Column, Entity, Index, JoinColumn, ManyToOne, OneToOne } from 'typeorm'; import { AmlReason } from '../../../aml/enums/aml-reason.enum'; +import { AmlSource } from '../../../aml/enums/aml-source.enum'; import { CheckStatus } from '../../../aml/enums/check-status.enum'; import { Buy } from '../../routes/buy/buy.entity'; import { BuyCryptoBatch } from './buy-crypto-batch.entity'; @@ -149,6 +150,9 @@ export class BuyCrypto extends IEntity { @Column({ length: 256, nullable: true }) amlResponsible?: string; + @Column({ length: 256, nullable: true }) + amlSource?: AmlSource; + // Check @Column({ length: 256, nullable: true }) amlCheck?: CheckStatus; 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 78fcdf71df..4db65b7850 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 @@ -8,6 +8,7 @@ import { OnModuleInit, } from '@nestjs/common'; import { Config } from 'src/config/config'; +import { JwtPayload } from 'src/shared/auth/jwt-payload.interface'; import { txExplorerUrl } from 'src/integration/blockchain/shared/util/blockchain.util'; import { CheckoutService } from 'src/integration/checkout/services/checkout.service'; import { TransactionStatus } from 'src/integration/sift/dto/sift.dto'; @@ -59,6 +60,7 @@ import { Between, FindOptionsRelations, In, IsNull, MoreThan, Not } from 'typeor import { ManualAmlCheckDto } from '../../../aml/dto/manual-aml-check.dto'; import { canManualPass } from '../../../aml/enums/aml-error.enum'; import { AmlReason, PhoneAmlReasons } from '../../../aml/enums/aml-reason.enum'; +import { AmlSource } from '../../../aml/enums/aml-source.enum'; import { CheckStatus } from '../../../aml/enums/check-status.enum'; import { Buy } from '../../routes/buy/buy.entity'; import { BuyRepository } from '../../routes/buy/buy.repository'; @@ -250,7 +252,7 @@ export class BuyCryptoService implements OnModuleInit { ); } - async update(id: number, dto: UpdateBuyCryptoDto): Promise { + async update(id: number, dto: UpdateBuyCryptoDto, jwt?: JwtPayload): Promise { let entity = await this.buyCryptoRepo.findOne({ where: { id }, relations: { @@ -337,6 +339,8 @@ export class BuyCryptoService implements OnModuleInit { update.amlReason = update.amlCheck === CheckStatus.PASS ? AmlReason.NA : update.amlReason; + const isGsActor = jwt != null && Config.amlGsAccountId != null && jwt.account === Config.amlGsAccountId; + const forceUpdate: Partial = { ...((BuyCryptoEditableAmlCheck.includes(entity.amlCheck) || (entity.amlCheck === CheckStatus.FAIL && dto.amlCheck === CheckStatus.GSHEET)) && @@ -353,6 +357,13 @@ export class BuyCryptoService implements OnModuleInit { update.priceDefinitionAllowedDate ?? entity.priceDefinitionAllowedDate ?? new Date(), } : undefined), + ...(jwt && update.amlCheck + ? { + amlSource: isGsActor ? AmlSource.GSHEET : AmlSource.COMPLIANCE, + amlResponsible: + update.amlResponsible ?? (isGsActor ? 'GSheet' : (jwt.address ?? `account:${jwt.account}`)), + } + : undefined), } : undefined), isComplete: dto.isComplete, @@ -722,7 +733,7 @@ export class BuyCryptoService implements OnModuleInit { if (fiatOutputId) await this.fiatOutputService.delete(fiatOutputId); } - async manualPassAmlCheck(id: number, dto: ManualAmlCheckDto): Promise { + async manualPassAmlCheck(id: number, dto: ManualAmlCheckDto, jwt?: JwtPayload): Promise { const entity = await this.buyCryptoRepo.findOneBy({ id }); if (!entity) throw new NotFoundException('BuyCrypto not found'); if (entity.isComplete || entity.chargebackAllowedDateUser) @@ -732,12 +743,16 @@ export class BuyCryptoService implements OnModuleInit { 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, - amlReason: dto.amlCheck === CheckStatus.PASS ? AmlReason.NA : dto.amlReason, - priceDefinitionAllowedDate: dto.amlCheck === CheckStatus.PASS ? new Date() : undefined, - } 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, + jwt, + ); } 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 c3ba8195cb..8215b3238c 100644 --- a/src/subdomains/core/sell-crypto/process/buy-fiat.controller.ts +++ b/src/subdomains/core/sell-crypto/process/buy-fiat.controller.ts @@ -1,6 +1,8 @@ import { Body, Controller, Delete, Param, Post, Put, Query, UseGuards } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { ApiBearerAuth, ApiExcludeEndpoint, ApiTags } from '@nestjs/swagger'; +import { GetJwt } from 'src/shared/auth/get-jwt.decorator'; +import { JwtPayload } from 'src/shared/auth/jwt-payload.interface'; 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'; @@ -51,8 +53,8 @@ export class BuyFiatController { @ApiBearerAuth() @ApiExcludeEndpoint() @UseGuards(AuthGuard(), RoleGuard(UserRole.COMPLIANCE), UserActiveGuard()) - async update(@Param('id') id: string, @Body() dto: UpdateBuyFiatDto): Promise { - return this.buyFiatService.update(+id, dto); + async update(@Param('id') id: string, @Body() dto: UpdateBuyFiatDto, @GetJwt() jwt: JwtPayload): Promise { + return this.buyFiatService.update(+id, dto, jwt); } @Delete(':id/amlCheck') @@ -67,7 +69,11 @@ export class BuyFiatController { @ApiBearerAuth() @ApiExcludeEndpoint() @UseGuards(AuthGuard(), RoleGuard(UserRole.COMPLIANCE), UserActiveGuard()) - async manualPassAmlCheck(@Param('id') id: string, @Body() dto: ManualAmlCheckDto): Promise { - return this.buyFiatService.manualPassAmlCheck(+id, dto); + async manualPassAmlCheck( + @Param('id') id: string, + @Body() dto: ManualAmlCheckDto, + @GetJwt() jwt: JwtPayload, + ): Promise { + return this.buyFiatService.manualPassAmlCheck(+id, dto, jwt); } } diff --git a/src/subdomains/core/sell-crypto/process/buy-fiat.entity.ts b/src/subdomains/core/sell-crypto/process/buy-fiat.entity.ts index ecbbcc3005..63ba52f06b 100644 --- a/src/subdomains/core/sell-crypto/process/buy-fiat.entity.ts +++ b/src/subdomains/core/sell-crypto/process/buy-fiat.entity.ts @@ -28,6 +28,7 @@ import { Column, Entity, Index, JoinColumn, ManyToOne, OneToOne } from 'typeorm' import { FiatOutput } from '../../../supporting/fiat-output/fiat-output.entity'; import { Transaction } from '../../../supporting/payment/entities/transaction.entity'; import { AmlReason } from '../../aml/enums/aml-reason.enum'; +import { AmlSource } from '../../aml/enums/aml-source.enum'; import { CheckStatus } from '../../aml/enums/check-status.enum'; import { AmlHelperService } from '../../aml/services/aml-helper.service'; import { PaymentLinkPayment } from '../../payment-link/entities/payment-link-payment.entity'; @@ -114,6 +115,9 @@ export class BuyFiat extends IEntity { @Column({ length: 256, nullable: true }) amlResponsible?: string; + @Column({ length: 256, nullable: true }) + amlSource?: AmlSource; + // Fee @Column({ length: 256, nullable: true }) usedFees?: string; // Semicolon separated id's 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 a710a87dde..62be6f2cc1 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 @@ -1,5 +1,7 @@ import { BadRequestException, forwardRef, Inject, Injectable, NotFoundException, OnModuleInit } from '@nestjs/common'; +import { Config } from 'src/config/config'; import { txExplorerUrl } from 'src/integration/blockchain/shared/util/blockchain.util'; +import { JwtPayload } from 'src/shared/auth/jwt-payload.interface'; import { UserRole } from 'src/shared/auth/user-role.enum'; import { FiatService } from 'src/shared/models/fiat/fiat.service'; import { DisabledProcess, Process } from 'src/shared/services/process.service'; @@ -32,6 +34,7 @@ import { FiatOutputService } from '../../../../supporting/fiat-output/fiat-outpu import { ManualAmlCheckDto } from '../../../aml/dto/manual-aml-check.dto'; import { canManualPass } from '../../../aml/enums/aml-error.enum'; import { AmlReason, PhoneAmlReasons } from '../../../aml/enums/aml-reason.enum'; +import { AmlSource } from '../../../aml/enums/aml-source.enum'; import { CheckStatus } from '../../../aml/enums/check-status.enum'; import { BuyCryptoService } from '../../../buy-crypto/process/services/buy-crypto.service'; import { PaymentStatus } from '../../../history/dto/history.dto'; @@ -152,7 +155,7 @@ export class BuyFiatService implements OnModuleInit { return entity; } - async update(id: number, dto: UpdateBuyFiatDto): Promise { + async update(id: number, dto: UpdateBuyFiatDto, jwt?: JwtPayload): Promise { let entity = await this.buyFiatRepo.findOne({ where: { id }, relations: { @@ -203,6 +206,8 @@ export class BuyFiatService implements OnModuleInit { approved: dto.bankDataActive, }); + const isGsActor = jwt != null && Config.amlGsAccountId != null && jwt.account === Config.amlGsAccountId; + const forceUpdate: Partial = { ...((BuyFiatEditableAmlCheck.includes(entity.amlCheck) || (entity.amlCheck === CheckStatus.FAIL && dto.amlCheck === CheckStatus.GSHEET)) && @@ -218,6 +223,13 @@ export class BuyFiatService implements OnModuleInit { update.priceDefinitionAllowedDate ?? entity.priceDefinitionAllowedDate ?? new Date(), } : undefined), + ...(jwt && update.amlCheck + ? { + amlSource: isGsActor ? AmlSource.GSHEET : AmlSource.COMPLIANCE, + amlResponsible: + update.amlResponsible ?? (isGsActor ? 'GSheet' : (jwt.address ?? `account:${jwt.account}`)), + } + : undefined), } : undefined), isComplete: dto.isComplete, @@ -453,7 +465,7 @@ export class BuyFiatService implements OnModuleInit { } } - async manualPassAmlCheck(id: number, dto: ManualAmlCheckDto): Promise { + async manualPassAmlCheck(id: number, dto: ManualAmlCheckDto, jwt?: JwtPayload): Promise { const entity = await this.buyFiatRepo.findOneBy({ id }); if (!entity) throw new NotFoundException('BuyFiat not found'); if (entity.isComplete || entity.chargebackAllowedDateUser) @@ -463,12 +475,16 @@ export class BuyFiatService implements OnModuleInit { 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, - amlReason: dto.amlCheck === CheckStatus.PASS ? AmlReason.NA : dto.amlReason, - priceDefinitionAllowedDate: dto.amlCheck === CheckStatus.PASS ? new Date() : undefined, - } 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, + jwt, + ); } async updateVolumes(start = 1, end = 100000): Promise { diff --git a/src/subdomains/generic/gs/gs.controller.ts b/src/subdomains/generic/gs/gs.controller.ts index 15f11227fb..bdacb1922b 100644 --- a/src/subdomains/generic/gs/gs.controller.ts +++ b/src/subdomains/generic/gs/gs.controller.ts @@ -15,6 +15,8 @@ import { GsService } from './gs.service'; @Controller('gs') export class GsController { + private static readonly loggedDbCallers = new Set(); + private readonly logger = new DfxLogger(GsController); constructor(private readonly gsService: GsService) {} @@ -24,6 +26,12 @@ export class GsController { @ApiExcludeEndpoint() @UseGuards(AuthGuard(), RoleGuard(UserRole.ADMIN), UserActiveGuard()) async getDbData(@GetJwt() jwt: JwtPayload, @Body() query: DbQueryDto): Promise { + const caller = jwt.address ?? `account:${jwt.account}`; + if (!GsController.loggedDbCallers.has(caller)) { + GsController.loggedDbCallers.add(caller); + this.logger.info(`gs/db first access by ${caller} (role ${jwt.role}, account ${jwt.account})`); + } + try { return await this.gsService.getDbData(query, jwt.role); } catch (e) {