Skip to content
Draft
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
28 changes: 28 additions & 0 deletions migration/1781006197061-AddAmlSource.js
Original file line number Diff line number Diff line change
@@ -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"`);
}
}
1 change: 1 addition & 0 deletions src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
5 changes: 5 additions & 0 deletions src/subdomains/core/aml/enums/aml-source.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export enum AmlSource {
AUTOMATIC = 'Automatic',
GSHEET = 'GSheet',
COMPLIANCE = 'Compliance',
}
8 changes: 7 additions & 1 deletion src/subdomains/core/aml/services/aml-helper.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -603,6 +604,7 @@ export class AmlHelperService {
amlReason?: AmlReason;
comment?: string;
amlResponsible?: string;
amlSource?: AmlSource;
priceDefinitionAllowedDate?: Date;
} {
const amlErrors = this.getAmlErrors(
Expand Down Expand Up @@ -634,14 +636,16 @@ export class AmlHelperService {
amlCheck: CheckStatus.PASS,
amlReason: AmlReason.NA,
amlResponsible: 'API',
amlSource: AmlSource.AUTOMATIC,
priceDefinitionAllowedDate: new Date(),
};

const amlResults = amlErrors.map((amlError) => ({ amlError, ...AmlErrorResult[amlError] }));

// 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 ||
Expand All @@ -668,6 +672,7 @@ export class AmlHelperService {
amlReason: crucialErrorResult.amlReason,
comment,
amlResponsible: 'API',
amlSource: AmlSource.AUTOMATIC,
}
: { bankData, comment };
}
Expand All @@ -690,6 +695,7 @@ export class AmlHelperService {
amlReason: amlResults[0].amlReason,
comment,
amlResponsible: 'API',
amlSource: AmlSource.AUTOMATIC,
};

// GSheet
Expand Down
18 changes: 14 additions & 4 deletions src/subdomains/core/buy-crypto/process/buy-crypto.controller.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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<BuyCrypto> {
return this.buyCryptoService.update(+id, dto);
async update(
@Param('id') id: string,
@Body() dto: UpdateBuyCryptoDto,
@GetJwt() jwt: JwtPayload,
): Promise<BuyCrypto> {
return this.buyCryptoService.update(+id, dto, jwt);
}

@Delete(':id/amlCheck')
Expand All @@ -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<BuyCrypto> {
return this.buyCryptoService.manualPassAmlCheck(+id, dto);
async manualPassAmlCheck(
@Param('id') id: string,
@Body() dto: ManualAmlCheckDto,
@GetJwt() jwt: JwtPayload,
): Promise<BuyCrypto> {
return this.buyCryptoService.manualPassAmlCheck(+id, dto, jwt);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -250,7 +252,7 @@ export class BuyCryptoService implements OnModuleInit {
);
}

async update(id: number, dto: UpdateBuyCryptoDto): Promise<BuyCrypto> {
async update(id: number, dto: UpdateBuyCryptoDto, jwt?: JwtPayload): Promise<BuyCrypto> {
let entity = await this.buyCryptoRepo.findOne({
where: { id },
relations: {
Expand Down Expand Up @@ -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<BuyCrypto> = {
...((BuyCryptoEditableAmlCheck.includes(entity.amlCheck) ||
(entity.amlCheck === CheckStatus.FAIL && dto.amlCheck === CheckStatus.GSHEET)) &&
Expand All @@ -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,
Expand Down Expand Up @@ -722,7 +733,7 @@ export class BuyCryptoService implements OnModuleInit {
if (fiatOutputId) await this.fiatOutputService.delete(fiatOutputId);
}

async manualPassAmlCheck(id: number, dto: ManualAmlCheckDto): Promise<BuyCrypto> {
async manualPassAmlCheck(id: number, dto: ManualAmlCheckDto, jwt?: JwtPayload): Promise<BuyCrypto> {
const entity = await this.buyCryptoRepo.findOneBy({ id });
if (!entity) throw new NotFoundException('BuyCrypto not found');
if (entity.isComplete || entity.chargebackAllowedDateUser)
Expand All @@ -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(
Expand Down
14 changes: 10 additions & 4 deletions src/subdomains/core/sell-crypto/process/buy-fiat.controller.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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<BuyFiat> {
return this.buyFiatService.update(+id, dto);
async update(@Param('id') id: string, @Body() dto: UpdateBuyFiatDto, @GetJwt() jwt: JwtPayload): Promise<BuyFiat> {
return this.buyFiatService.update(+id, dto, jwt);
}

@Delete(':id/amlCheck')
Expand All @@ -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<BuyFiat> {
return this.buyFiatService.manualPassAmlCheck(+id, dto);
async manualPassAmlCheck(
@Param('id') id: string,
@Body() dto: ManualAmlCheckDto,
@GetJwt() jwt: JwtPayload,
): Promise<BuyFiat> {
return this.buyFiatService.manualPassAmlCheck(+id, dto, jwt);
}
}
4 changes: 4 additions & 0 deletions src/subdomains/core/sell-crypto/process/buy-fiat.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -152,7 +155,7 @@ export class BuyFiatService implements OnModuleInit {
return entity;
}

async update(id: number, dto: UpdateBuyFiatDto): Promise<BuyFiat> {
async update(id: number, dto: UpdateBuyFiatDto, jwt?: JwtPayload): Promise<BuyFiat> {
let entity = await this.buyFiatRepo.findOne({
where: { id },
relations: {
Expand Down Expand Up @@ -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<BuyFiat> = {
...((BuyFiatEditableAmlCheck.includes(entity.amlCheck) ||
(entity.amlCheck === CheckStatus.FAIL && dto.amlCheck === CheckStatus.GSHEET)) &&
Expand All @@ -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,
Expand Down Expand Up @@ -453,7 +465,7 @@ export class BuyFiatService implements OnModuleInit {
}
}

async manualPassAmlCheck(id: number, dto: ManualAmlCheckDto): Promise<BuyFiat> {
async manualPassAmlCheck(id: number, dto: ManualAmlCheckDto, jwt?: JwtPayload): Promise<BuyFiat> {
const entity = await this.buyFiatRepo.findOneBy({ id });
if (!entity) throw new NotFoundException('BuyFiat not found');
if (entity.isComplete || entity.chargebackAllowedDateUser)
Expand All @@ -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<void> {
Expand Down
8 changes: 8 additions & 0 deletions src/subdomains/generic/gs/gs.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import { GsService } from './gs.service';

@Controller('gs')
export class GsController {
private static readonly loggedDbCallers = new Set<string>();

private readonly logger = new DfxLogger(GsController);

constructor(private readonly gsService: GsService) {}
Expand All @@ -24,6 +26,12 @@ export class GsController {
@ApiExcludeEndpoint()
@UseGuards(AuthGuard(), RoleGuard(UserRole.ADMIN), UserActiveGuard())
async getDbData(@GetJwt() jwt: JwtPayload, @Body() query: DbQueryDto): Promise<DbReturnData> {
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) {
Expand Down