From 676af44a5c2443cb3dd670edb74f3c4dd7d3d3d1 Mon Sep 17 00:00:00 2001 From: Yannick <52333989+Yannick1712@users.noreply.github.com> Date: Mon, 22 Jun 2026 14:19:34 +0200 Subject: [PATCH 1/2] [NOTASK] blacklisted aml errors (#3949) --- src/subdomains/core/aml/enums/aml-error.enum.ts | 7 +++++++ .../process/services/buy-crypto.service.ts | 13 ++++++++++--- .../process/services/buy-fiat.service.ts | 13 ++++++++++--- 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/subdomains/core/aml/enums/aml-error.enum.ts b/src/subdomains/core/aml/enums/aml-error.enum.ts index 43ecce0e8d..dc51a0e583 100644 --- a/src/subdomains/core/aml/enums/aml-error.enum.ts +++ b/src/subdomains/core/aml/enums/aml-error.enum.ts @@ -96,6 +96,13 @@ export const ManualPassWhitelistErrors: AmlError[] = [ AmlError.REFERRAL_NO_TRADE_HISTORY, ]; +export const ManualPassBlacklistErrors: AmlError[] = [ + AmlError.BANK_DATA_NOT_ACTIVE, + AmlError.BANK_DATA_MANUAL_REVIEW, + AmlError.BANK_DATA_MISSING, + AmlError.BANK_DATA_USER_MISMATCH, +]; + export function canManualPass(comment: string | null | undefined): boolean { const errors = (comment ?? '') .split(';') 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..126534742e 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 @@ -57,7 +57,7 @@ import { TransactionService } from 'src/subdomains/supporting/payment/services/t import { PriceValidity } from 'src/subdomains/supporting/pricing/services/pricing.service'; import { Between, FindOptionsRelations, In, IsNull, MoreThan, Not } from 'typeorm'; import { ManualAmlCheckDto } from '../../../aml/dto/manual-aml-check.dto'; -import { canManualPass } from '../../../aml/enums/aml-error.enum'; +import { canManualPass, ManualPassBlacklistErrors } from '../../../aml/enums/aml-error.enum'; import { AmlReason, PhoneAmlReasons } from '../../../aml/enums/aml-reason.enum'; import { CheckStatus } from '../../../aml/enums/check-status.enum'; import { Buy } from '../../routes/buy/buy.entity'; @@ -299,6 +299,9 @@ export class BuyCryptoService implements OnModuleInit { manualApproved: dto.bankDataManualApproved, }); + if (dto.amlCheck === CheckStatus.PASS && ManualPassBlacklistErrors.some((b) => entity.comment?.includes(b))) + throw new BadRequestException('Blacklisted aml error cannot set Pass'); + if (dto.chargebackAllowedDate) { if (entity.bankTx && !entity.chargebackOutput) { if (!dto.chargebackCreditorName && !entity.creditorData) @@ -729,8 +732,12 @@ export class BuyCryptoService implements OnModuleInit { 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'); + if (dto.amlCheck === CheckStatus.PASS) { + if (ManualPassBlacklistErrors.some((b) => entity.comment?.includes(b))) + throw new BadRequestException('Blacklisted aml error cannot set Pass'); + if (!canManualPass(entity.comment)) + throw new BadRequestException('Manual pass only allowed when all errors are phone-related'); + } return this.update(id, { amlCheck: dto.amlCheck, 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..6c43cc9971 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 @@ -30,7 +30,7 @@ import { SupportLogService } from 'src/subdomains/supporting/support-issue/servi import { Between, FindOptionsRelations, In, IsNull, MoreThan } from 'typeorm'; import { FiatOutputService } from '../../../../supporting/fiat-output/fiat-output.service'; import { ManualAmlCheckDto } from '../../../aml/dto/manual-aml-check.dto'; -import { canManualPass } from '../../../aml/enums/aml-error.enum'; +import { canManualPass, ManualPassBlacklistErrors } from '../../../aml/enums/aml-error.enum'; import { AmlReason, PhoneAmlReasons } from '../../../aml/enums/aml-reason.enum'; import { CheckStatus } from '../../../aml/enums/check-status.enum'; import { BuyCryptoService } from '../../../buy-crypto/process/services/buy-crypto.service'; @@ -203,6 +203,9 @@ export class BuyFiatService implements OnModuleInit { approved: dto.bankDataActive, }); + if (dto.amlCheck === CheckStatus.PASS && ManualPassBlacklistErrors.some((b) => entity.comment?.includes(b))) + throw new BadRequestException('Blacklisted aml error cannot set Pass'); + const forceUpdate: Partial = { ...((BuyFiatEditableAmlCheck.includes(entity.amlCheck) || (entity.amlCheck === CheckStatus.FAIL && dto.amlCheck === CheckStatus.GSHEET)) && @@ -460,8 +463,12 @@ export class BuyFiatService implements OnModuleInit { 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'); + if (dto.amlCheck === CheckStatus.PASS) { + if (ManualPassBlacklistErrors.some((b) => entity.comment?.includes(b))) + throw new BadRequestException('Blacklisted aml error cannot set Pass'); + if (!canManualPass(entity.comment)) + throw new BadRequestException('Manual pass only allowed when all errors are phone-related'); + } return this.update(id, { amlCheck: dto.amlCheck, From 6ffeacf4079f27f5c68390328032a1b8faabc132 Mon Sep 17 00:00:00 2001 From: Daniel Padrino Date: Sat, 20 Jun 2026 16:44:37 -0300 Subject: [PATCH 2/2] fix(api): quote camelCase identifiers + ParseIntPipe on route :id (Postgres) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes a class of HTTP 500s introduced by the MySQL->Postgres migration. Postgres folds unquoted identifiers to lowercase and is stricter on function signatures, so raw-SQL fragments that worked on MySQL now error. - buy/sell/swap controllers: add ParseIntPipe to the route `:id` handlers (get/update/history). A non-numeric `:id` made `+id` -> NaN, bound as an integer param -> `invalid input syntax for type integer: "NaN"` 500. Now rejects with 400 instead. - support-issue, deposit-route, bank-data, payment-link-payment: quote the camelCase aliases/columns in raw query fragments (`"userData"`, `"issueId"`, `"userDataId"`, `"linkId"`, `"maxId"`, `"organizationName"`, `"paymentLinksConfig"`) so they resolve instead of folding to lowercase (`missing FROM-clause entry`, `column ... does not exist`). - ref-reward: cast `ROUND(SUM(...)::numeric, 0)` — Postgres has no `round(double precision, integer)` overload. --- .../buy-crypto/routes/buy/buy.controller.ts | 17 ++++++++++------ .../buy-crypto/routes/swap/swap.controller.ts | 16 +++++++++------ .../services/payment-link-payment.service.ts | 6 +++--- .../reward/services/ref-reward.service.ts | 2 +- .../core/sell-crypto/route/sell.controller.ts | 20 +++++++++++++------ .../models/bank-data/bank-data.service.ts | 2 +- .../route/deposit-route.service.ts | 2 +- .../services/support-issue.service.ts | 12 +++++------ 8 files changed, 47 insertions(+), 30 deletions(-) diff --git a/src/subdomains/core/buy-crypto/routes/buy/buy.controller.ts b/src/subdomains/core/buy-crypto/routes/buy/buy.controller.ts index 9224b7ce36..e22f12b605 100644 --- a/src/subdomains/core/buy-crypto/routes/buy/buy.controller.ts +++ b/src/subdomains/core/buy-crypto/routes/buy/buy.controller.ts @@ -5,6 +5,7 @@ import { Controller, Get, Param, + ParseIntPipe, Post, Put, UseGuards, @@ -249,24 +250,28 @@ export class BuyController { @ApiBearerAuth() @UseGuards(AuthGuard(), RoleGuard(UserRole.USER), BuyActiveGuard()) @ApiExcludeEndpoint() - async updateBuyRoute(@GetJwt() jwt: JwtPayload, @Param('id') id: string, @Body() dto: UpdateBuyDto): Promise { - return this.buyService.updateBuy(jwt.user, +id, dto).then((b) => this.toDto(jwt.user, b)); + async updateBuyRoute( + @GetJwt() jwt: JwtPayload, + @Param('id', ParseIntPipe) id: number, + @Body() dto: UpdateBuyDto, + ): Promise { + return this.buyService.updateBuy(jwt.user, id, dto).then((b) => this.toDto(jwt.user, b)); } @Get(':id') @ApiBearerAuth() @UseGuards(AuthGuard(), RoleGuard(UserRole.USER), BuyActiveGuard()) @ApiOkResponse({ type: BuyDto }) - async getBuy(@GetJwt() jwt: JwtPayload, @Param('id') id: string): Promise { - return this.buyService.get(jwt.account, +id).then((l) => this.toDto(jwt.user, l)); + async getBuy(@GetJwt() jwt: JwtPayload, @Param('id', ParseIntPipe) id: number): Promise { + return this.buyService.get(jwt.account, id).then((l) => this.toDto(jwt.user, l)); } @Get(':id/history') @ApiBearerAuth() @UseGuards(AuthGuard(), RoleGuard(UserRole.USER)) @ApiExcludeEndpoint() - async getBuyRouteHistory(@GetJwt() jwt: JwtPayload, @Param('id') id: string): Promise { - return this.buyCryptoService.getBuyHistory(jwt.user, +id); + async getBuyRouteHistory(@GetJwt() jwt: JwtPayload, @Param('id', ParseIntPipe) id: number): Promise { + return this.buyCryptoService.getBuyHistory(jwt.user, id); } // --- DTO --- // diff --git a/src/subdomains/core/buy-crypto/routes/swap/swap.controller.ts b/src/subdomains/core/buy-crypto/routes/swap/swap.controller.ts index dd546cea18..26765f0c34 100644 --- a/src/subdomains/core/buy-crypto/routes/swap/swap.controller.ts +++ b/src/subdomains/core/buy-crypto/routes/swap/swap.controller.ts @@ -6,6 +6,7 @@ import { Get, NotFoundException, Param, + ParseIntPipe, Post, Put, Query, @@ -75,8 +76,8 @@ export class SwapController { @ApiBearerAuth() @UseGuards(AuthGuard(), RoleGuard(UserRole.USER), SwapActiveGuard()) @ApiOkResponse({ type: SwapDto }) - async getSwap(@GetJwt() jwt: JwtPayload, @Param('id') id: string): Promise { - return this.swapService.get(jwt.user, +id).then((l) => this.toDto(jwt.user, l)); + async getSwap(@GetJwt() jwt: JwtPayload, @Param('id', ParseIntPipe) id: number): Promise { + return this.swapService.get(jwt.user, id).then((l) => this.toDto(jwt.user, l)); } @Post() @@ -206,18 +207,21 @@ export class SwapController { @ApiExcludeEndpoint() async updateSwapRoute( @GetJwt() jwt: JwtPayload, - @Param('id') id: string, + @Param('id', ParseIntPipe) id: number, @Body() updateCryptoDto: UpdateSwapDto, ): Promise { - return this.swapService.updateSwap(jwt.user, +id, updateCryptoDto).then((b) => this.toDto(jwt.user, b)); + return this.swapService.updateSwap(jwt.user, id, updateCryptoDto).then((b) => this.toDto(jwt.user, b)); } @Get(':id/history') @ApiBearerAuth() @UseGuards(AuthGuard(), RoleGuard(UserRole.USER)) @ApiExcludeEndpoint() - async getSwapRouteHistory(@GetJwt() jwt: JwtPayload, @Param('id') id: string): Promise { - return this.buyCryptoService.getCryptoHistory(jwt.user, +id); + async getSwapRouteHistory( + @GetJwt() jwt: JwtPayload, + @Param('id', ParseIntPipe) id: number, + ): Promise { + return this.buyCryptoService.getCryptoHistory(jwt.user, id); } // --- DTO --- // diff --git a/src/subdomains/core/payment-link/services/payment-link-payment.service.ts b/src/subdomains/core/payment-link/services/payment-link-payment.service.ts index 884baa29d6..7161152200 100644 --- a/src/subdomains/core/payment-link/services/payment-link-payment.service.ts +++ b/src/subdomains/core/payment-link/services/payment-link-payment.service.ts @@ -153,12 +153,12 @@ export class PaymentLinkPaymentService { .innerJoin( (qb) => qb - .select('plp2.linkId', 'linkId') + .select('plp2."linkId"', 'linkId') .addSelect('MAX(plp2.id)', 'maxId') .from(PaymentLinkPayment, 'plp2') - .groupBy('plp2.linkId'), + .groupBy('plp2."linkId"'), 'latest', - 'latest.linkId = plp.linkId AND latest.maxId = plp.id', + 'latest."linkId" = plp."linkId" AND latest."maxId" = plp.id', ) .innerJoinAndSelect('plp.currency', 'currency') .innerJoinAndSelect('plp.link', 'link') diff --git a/src/subdomains/core/referral/reward/services/ref-reward.service.ts b/src/subdomains/core/referral/reward/services/ref-reward.service.ts index d85c277a39..7503d0d12f 100644 --- a/src/subdomains/core/referral/reward/services/ref-reward.service.ts +++ b/src/subdomains/core/referral/reward/services/ref-reward.service.ts @@ -261,7 +261,7 @@ export class RefRewardService { .innerJoin('r.user', 'u') .select('u.userDataId', 'userDataId') .addSelect('COUNT(*)', 'count') - .addSelect('ROUND(SUM(r.amountInChf), 0)', 'totalChf') + .addSelect('ROUND(SUM(r.amountInChf)::numeric, 0)', 'totalChf') .where('r.status != :excluded', { excluded: RewardStatus.USER_SWITCH }) .groupBy('u.userDataId') .orderBy('totalChf', 'DESC'); diff --git a/src/subdomains/core/sell-crypto/route/sell.controller.ts b/src/subdomains/core/sell-crypto/route/sell.controller.ts index 750890c2cd..b8f1d32864 100644 --- a/src/subdomains/core/sell-crypto/route/sell.controller.ts +++ b/src/subdomains/core/sell-crypto/route/sell.controller.ts @@ -6,6 +6,7 @@ import { Get, NotFoundException, Param, + ParseIntPipe, Post, Put, Query, @@ -75,8 +76,8 @@ export class SellController { @ApiBearerAuth() @UseGuards(AuthGuard(), RoleGuard(UserRole.USER), SellActiveGuard()) @ApiOkResponse({ type: SellDto }) - async getSell(@GetJwt() jwt: JwtPayload, @Param('id') id: string): Promise { - return this.sellService.get(jwt.user, +id).then((l) => this.toDto(l)); + async getSell(@GetJwt() jwt: JwtPayload, @Param('id', ParseIntPipe) id: number): Promise { + return this.sellService.get(jwt.user, id).then((l) => this.toDto(l)); } @Post() @@ -215,16 +216,23 @@ export class SellController { @ApiBearerAuth() @UseGuards(AuthGuard(), RoleGuard(UserRole.USER), SellActiveGuard()) @ApiExcludeEndpoint() - async updateSell(@GetJwt() jwt: JwtPayload, @Param('id') id: string, @Body() dto: UpdateSellDto): Promise { - return this.sellService.updateSell(jwt.user, +id, dto).then((s) => this.toDto(s)); + async updateSell( + @GetJwt() jwt: JwtPayload, + @Param('id', ParseIntPipe) id: number, + @Body() dto: UpdateSellDto, + ): Promise { + return this.sellService.updateSell(jwt.user, id, dto).then((s) => this.toDto(s)); } @Get(':id/history') @ApiBearerAuth() @UseGuards(AuthGuard(), RoleGuard(UserRole.USER)) @ApiExcludeEndpoint() - async getSellRouteHistory(@GetJwt() jwt: JwtPayload, @Param('id') id: string): Promise { - return this.buyFiatService.getSellHistory(jwt.user, +id); + async getSellRouteHistory( + @GetJwt() jwt: JwtPayload, + @Param('id', ParseIntPipe) id: number, + ): Promise { + return this.buyFiatService.getSellHistory(jwt.user, id); } // --- DTO --- // diff --git a/src/subdomains/generic/user/models/bank-data/bank-data.service.ts b/src/subdomains/generic/user/models/bank-data/bank-data.service.ts index 5869117445..cad164ebce 100644 --- a/src/subdomains/generic/user/models/bank-data/bank-data.service.ts +++ b/src/subdomains/generic/user/models/bank-data/bank-data.service.ts @@ -375,7 +375,7 @@ export class BankDataService { .createQueryBuilder() .update('bank_data') .set({ active: false, default: false }) - .where('bank_data.userDataId = :userDataId', { userDataId }) + .where('bank_data."userDataId" = :userDataId', { userDataId }) .andWhere('bank_data.id != :id', { id: entity.id }) .andWhere('bank_data.iban = :iban', { iban: entity.iban }) .execute(); diff --git a/src/subdomains/supporting/address-pool/route/deposit-route.service.ts b/src/subdomains/supporting/address-pool/route/deposit-route.service.ts index 022cc525bc..df1c4a4c96 100644 --- a/src/subdomains/supporting/address-pool/route/deposit-route.service.ts +++ b/src/subdomains/supporting/address-pool/route/deposit-route.service.ts @@ -89,7 +89,7 @@ export class DepositRouteService { .innerJoinAndSelect('depositRoute.user', 'user') .innerJoinAndSelect('user.userData', 'userData') .where( - `EXISTS (SELECT 1 FROM jsonb_array_elements_text((userData."paymentLinksConfig")::jsonb -> 'accessKeys') AS k WHERE k = :key)`, + `EXISTS (SELECT 1 FROM jsonb_array_elements_text(("userData"."paymentLinksConfig")::jsonb -> 'accessKeys') AS k WHERE k = :key)`, { key }, ) .andWhere('depositRoute.active = :active', { active: true }) diff --git a/src/subdomains/supporting/support-issue/services/support-issue.service.ts b/src/subdomains/supporting/support-issue/services/support-issue.service.ts index 457c727e21..05300b4a29 100644 --- a/src/subdomains/supporting/support-issue/services/support-issue.service.ts +++ b/src/subdomains/supporting/support-issue/services/support-issue.service.ts @@ -294,7 +294,7 @@ export class SupportIssueService { for (let i = 0; i < termCount; i++) { const param = `term${i}`; qb.andWhere( - `(issue.name LIKE :${param} OR issue.uid LIKE :${param} OR issue.clerk LIKE :${param} OR userData.firstname LIKE :${param} OR userData.surname LIKE :${param} OR userData.organizationName LIKE :${param} OR EXISTS (SELECT 1 FROM support_message m WHERE m.issueId = issue.id AND m.message LIKE :${param}))`, + `(issue.name LIKE :${param} OR issue.uid LIKE :${param} OR issue.clerk LIKE :${param} OR "userData".firstname LIKE :${param} OR "userData".surname LIKE :${param} OR "userData"."organizationName" LIKE :${param} OR EXISTS (SELECT 1 FROM support_message m WHERE m."issueId" = issue.id AND m.message LIKE :${param}))`, { [param]: `%${terms[i]}%` }, ); } @@ -327,14 +327,14 @@ export class SupportIssueService { (chunk): Promise<{ issueId: string; count: string; lastDate: Date | null; lastAuthor: string | null }[]> => this.messageRepo .createQueryBuilder('m') - .select('m.issueId', 'issueId') + .select('m."issueId"', 'issueId') .addSelect('COUNT(*)', 'count') .addSelect( (sub) => sub .select('m2.created') .from(SupportMessage, 'm2') - .where('m2.issueId = m.issueId') + .where('m2."issueId" = m."issueId"') .orderBy('m2.id', 'DESC') .limit(1), 'lastDate', @@ -344,13 +344,13 @@ export class SupportIssueService { sub .select('m2.author') .from(SupportMessage, 'm2') - .where('m2.issueId = m.issueId') + .where('m2."issueId" = m."issueId"') .orderBy('m2.id', 'DESC') .limit(1), 'lastAuthor', ) - .where('m.issueId IN (:...ids)', { ids: chunk }) - .groupBy('m.issueId') + .where('m."issueId" IN (:...ids)', { ids: chunk }) + .groupBy('m."issueId"') .getRawMany(), 1000, );