From 37562dbe28487d4a9d1aa8d533e353a61c936b89 Mon Sep 17 00:00:00 2001 From: Yannick <52333989+Yannick1712@users.noreply.github.com> Date: Sat, 31 Jan 2026 17:50:31 +0100 Subject: [PATCH 1/2] [DEV-4537] SupportData endpoint (#3001) * [DEV-4537] SupportData endpoint * [DEV-4537] Refactoring * [DEV-4537] adapt relations * [DEV-4537] Refactoring 2 * [DEV-4537] fix missing optional operator * [DEV-4537] Refactoring 3 --- .../dto/support-issue-dto.mapper.ts | 66 +++++++++ .../support-issue/dto/support-issue.dto.ts | 127 ++++++++++++++++++ .../services/support-issue.service.ts | 20 ++- .../support-issue/support-issue.controller.ts | 10 +- 4 files changed, 221 insertions(+), 2 deletions(-) diff --git a/src/subdomains/supporting/support-issue/dto/support-issue-dto.mapper.ts b/src/subdomains/supporting/support-issue/dto/support-issue-dto.mapper.ts index 92dbc84e26..43c3934a8b 100644 --- a/src/subdomains/supporting/support-issue/dto/support-issue-dto.mapper.ts +++ b/src/subdomains/supporting/support-issue/dto/support-issue-dto.mapper.ts @@ -1,9 +1,14 @@ +import { CountryDtoMapper } from 'src/shared/models/country/dto/country-dto.mapper'; +import { UserData } from 'src/subdomains/generic/user/models/user-data/user-data.entity'; import { Transaction } from '../../payment/entities/transaction.entity'; import { LimitRequest } from '../entities/limit-request.entity'; import { SupportIssue } from '../entities/support-issue.entity'; import { SupportMessage } from '../entities/support-message.entity'; import { SupportIssueDto, + SupportIssueInternalAccountDataDto, + SupportIssueInternalDataDto, + SupportIssueInternalTransactionDataDto, SupportIssueLimitRequestDto, SupportIssueStateMapper, SupportIssueTransactionDto, @@ -27,6 +32,23 @@ export class SupportIssueDtoMapper { return Object.assign(new SupportIssueDto(), dto); } + static mapSupportIssueData(supportIssue: SupportIssue): SupportIssueInternalDataDto { + const dto: SupportIssueInternalDataDto = { + id: supportIssue.id, + created: supportIssue.created, + uid: supportIssue.uid, + type: supportIssue.type, + department: supportIssue.department, + reason: supportIssue.reason, + state: supportIssue.state, + name: supportIssue.name, + account: SupportIssueDtoMapper.mapUserData(supportIssue.userData), + transaction: SupportIssueDtoMapper.mapTransactionData(supportIssue.transaction), + }; + + return Object.assign(new SupportIssueInternalDataDto(), dto); + } + static mapSupportMessage(supportMessage: SupportMessage): SupportMessageDto { const dto: SupportMessageDto = { id: supportMessage.id, @@ -39,6 +61,50 @@ export class SupportIssueDtoMapper { return Object.assign(new SupportMessageDto(), dto); } + static mapUserData(userData: UserData): SupportIssueInternalAccountDataDto { + return { + id: userData.id, + status: userData.status, + verifiedName: userData.verifiedName, + completeName: userData.completeName, + accountType: userData.accountType, + kycLevel: userData.kycLevel, + depositLimit: userData.depositLimit, + annualVolume: userData.annualBuyVolume + userData.annualSellVolume + userData.annualCryptoVolume, + kycHash: userData.kycHash, + country: userData.country ? CountryDtoMapper.entityToDto(userData.country) : undefined, + }; + } + + static mapTransactionData(transaction: Transaction): SupportIssueInternalTransactionDataDto { + if (!transaction?.id) return undefined; + + const targetEntity = transaction.buyCrypto ?? transaction.buyFiat; + + return { + id: transaction.id, + sourceType: transaction.sourceType, + type: transaction.type, + amlCheck: transaction.amlCheck, + amlReason: targetEntity?.amlReason, + comment: targetEntity?.comment, + inputAmount: targetEntity?.inputAmount, + inputAsset: targetEntity?.inputAsset, + inputBlockchain: targetEntity?.cryptoInput?.asset?.blockchain, + outputAmount: targetEntity?.outputAmount, + outputAsset: targetEntity?.outputAsset.name, + outputBlockchain: transaction?.buyCrypto?.outputAsset.blockchain, + wallet: transaction.user?.wallet + ? { + name: transaction.user.wallet.displayName ?? transaction.user.wallet.name, + amlRules: transaction.user.wallet.amlRules, + isKycClient: transaction.user.wallet.isKycClient, + } + : undefined, + isComplete: targetEntity?.isComplete, + }; + } + static mapTransaction(transaction: Transaction): SupportIssueTransactionDto { if (!transaction?.id) return null; diff --git a/src/subdomains/supporting/support-issue/dto/support-issue.dto.ts b/src/subdomains/supporting/support-issue/dto/support-issue.dto.ts index 1624586429..4de910dd01 100644 --- a/src/subdomains/supporting/support-issue/dto/support-issue.dto.ts +++ b/src/subdomains/supporting/support-issue/dto/support-issue.dto.ts @@ -1,4 +1,12 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { Blockchain } from 'src/integration/blockchain/shared/enums/blockchain.enum'; +import { CountryDto } from 'src/shared/models/country/dto/country.dto'; +import { AmlReason } from 'src/subdomains/core/aml/enums/aml-reason.enum'; +import { CheckStatus } from 'src/subdomains/core/aml/enums/check-status.enum'; +import { AccountType } from 'src/subdomains/generic/user/models/user-data/account-type.enum'; +import { KycLevel, UserDataStatus } from 'src/subdomains/generic/user/models/user-data/user-data.enum'; +import { TransactionSourceType, TransactionTypeInternal } from '../../payment/entities/transaction.entity'; +import { Department } from '../enums/department.enum'; import { SupportIssueInternalState, SupportIssueReason, @@ -73,6 +81,125 @@ export class SupportIssueDto { limitRequest?: SupportIssueLimitRequestDto; } +export class SupportIssueInternalAccountDataDto { + @ApiProperty() + id: number; + + @ApiProperty({ enum: UserDataStatus }) + status: UserDataStatus; + + @ApiProperty() + verifiedName: string; + + @ApiProperty() + completeName: string; + + @ApiProperty({ enum: AccountType }) + accountType: AccountType; + + @ApiProperty({ enum: KycLevel }) + kycLevel: KycLevel; + + @ApiProperty() + depositLimit: number; + + @ApiProperty() + annualVolume: number; + + @ApiProperty() + kycHash: string; + + @ApiProperty({ type: CountryDto }) + country: CountryDto; +} + +export class SupportIssueInternalWalletDto { + @ApiProperty() + name: string; + + @ApiProperty() + amlRules: string; + + @ApiProperty() + isKycClient: boolean; +} + +export class SupportIssueInternalTransactionDataDto { + @ApiProperty() + id: number; + + @ApiProperty({ enum: TransactionSourceType }) + sourceType: TransactionSourceType; + + @ApiProperty({ enum: TransactionTypeInternal }) + type: TransactionTypeInternal; + + @ApiProperty({ enum: CheckStatus }) + amlCheck: CheckStatus; + + @ApiProperty({ enum: AmlReason }) + amlReason: AmlReason; + + @ApiProperty() + comment: string; + + @ApiProperty() + inputAmount: number; + + @ApiProperty() + inputAsset: string; + + @ApiPropertyOptional({ enum: Blockchain }) + inputBlockchain?: Blockchain; + + @ApiProperty() + outputAmount: number; + + @ApiProperty() + outputAsset: string; + + @ApiPropertyOptional({ enum: Blockchain }) + outputBlockchain?: Blockchain; + + @ApiProperty({ type: SupportIssueInternalWalletDto }) + wallet: SupportIssueInternalWalletDto; + + @ApiProperty() + isComplete: boolean; +} + +export class SupportIssueInternalDataDto { + @ApiProperty() + id: number; + + @ApiProperty({ type: Date }) + created: Date; + + @ApiProperty() + uid: string; + + @ApiProperty({ enum: SupportIssueType }) + type: SupportIssueType; + + @ApiProperty({ enum: Department }) + department?: Department; + + @ApiProperty({ enum: SupportIssueReason }) + reason: SupportIssueReason; + + @ApiProperty({ enum: SupportIssueInternalState }) + state: SupportIssueInternalState; + + @ApiProperty() + name: string; + + @ApiProperty({ type: SupportIssueInternalAccountDataDto }) + account: SupportIssueInternalAccountDataDto; + + @ApiProperty({ type: SupportIssueInternalTransactionDataDto }) + transaction: SupportIssueInternalTransactionDataDto; +} + export const SupportIssueStateMapper: { [key in SupportIssueInternalState]: SupportIssueState; } = { 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 7cc029f9e9..390e4db426 100644 --- a/src/subdomains/supporting/support-issue/services/support-issue.service.ts +++ b/src/subdomains/supporting/support-issue/services/support-issue.service.ts @@ -7,6 +7,7 @@ import { } from '@nestjs/common'; import { Config } from 'src/config/config'; import { BlobContent } from 'src/integration/infrastructure/azure-storage.service'; +import { FiatService } from 'src/shared/models/fiat/fiat.service'; import { Util } from 'src/shared/utils/util'; import { ContentType } from 'src/subdomains/generic/kyc/enums/content-type.enum'; import { BankDataService } from 'src/subdomains/generic/user/models/bank-data/bank-data.service'; @@ -21,7 +22,7 @@ import { CreateSupportIssueBaseDto, CreateSupportIssueDto } from '../dto/create- import { CreateSupportMessageDto } from '../dto/create-support-message.dto'; import { GetSupportIssueFilter } from '../dto/get-support-issue.dto'; import { SupportIssueDtoMapper } from '../dto/support-issue-dto.mapper'; -import { SupportIssueDto, SupportMessageDto } from '../dto/support-issue.dto'; +import { SupportIssueDto, SupportIssueInternalDataDto, SupportMessageDto } from '../dto/support-issue.dto'; import { UpdateSupportIssueDto } from '../dto/update-support-issue.dto'; import { SupportIssue } from '../entities/support-issue.entity'; import { AutoResponder, CustomerAuthor, SupportMessage } from '../entities/support-message.entity'; @@ -48,6 +49,7 @@ export class SupportIssueService { private readonly transactionRequestService: TransactionRequestService, private readonly supportLogService: SupportLogService, private readonly bankDataService: BankDataService, + private readonly fiatService: FiatService, ) {} async createTransactionRequestIssue(dto: CreateSupportIssueBaseDto): Promise { @@ -220,6 +222,22 @@ export class SupportIssueService { return SupportIssueDtoMapper.mapSupportIssue(issue); } + async getIssueData(id: number): Promise { + const issue = await this.supportIssueRepo.findOne({ + where: { id }, + relations: { + transaction: { + user: { wallet: true }, + buyCrypto: { transaction: true, cryptoInput: true }, + buyFiat: { transaction: true, cryptoInput: true }, + }, + }, + }); + if (!issue) throw new NotFoundException('Support issue not found'); + + return SupportIssueDtoMapper.mapSupportIssueData(issue); + } + async getIssueFile(id: string, messageId: number, userDataId?: number): Promise { const message = await this.messageRepo.findOneBy({ id: messageId, issue: this.getIssueSearch(id, userDataId) }); if (!message) throw new NotFoundException('Message not found'); diff --git a/src/subdomains/supporting/support-issue/support-issue.controller.ts b/src/subdomains/supporting/support-issue/support-issue.controller.ts index 3280a14232..27865086fd 100644 --- a/src/subdomains/supporting/support-issue/support-issue.controller.ts +++ b/src/subdomains/supporting/support-issue/support-issue.controller.ts @@ -11,7 +11,7 @@ import { UserRole } from 'src/shared/auth/user-role.enum'; import { CreateSupportIssueDto, CreateSupportIssueSupportDto } from './dto/create-support-issue.dto'; import { CreateSupportMessageDto } from './dto/create-support-message.dto'; import { GetSupportIssueFilter } from './dto/get-support-issue.dto'; -import { SupportIssueDto, SupportMessageDto } from './dto/support-issue.dto'; +import { SupportIssueDto, SupportIssueInternalDataDto, SupportMessageDto } from './dto/support-issue.dto'; import { UpdateSupportIssueDto } from './dto/update-support-issue.dto'; import { SupportIssue } from './entities/support-issue.entity'; import { CustomerAuthor } from './entities/support-message.entity'; @@ -65,6 +65,14 @@ export class SupportIssueController { return this.supportIssueService.getIssue(id, query, jwt?.account); } + @Get(':id/data') + @ApiBearerAuth() + @ApiExcludeEndpoint() + @UseGuards(AuthGuard(), RoleGuard(UserRole.SUPPORT), UserActiveGuard()) + async getIssueData(@Param('id') id: string): Promise { + return this.supportIssueService.getIssueData(+id); + } + @Post(':id/message') @ApiBearerAuth() @UseGuards(OptionalJwtAuthGuard) From 40571da5b978c197f0ebe6c719135a4ed66910e8 Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Sat, 31 Jan 2026 19:17:02 +0100 Subject: [PATCH 2/2] Add migration to fix missing kycFileIds for 2025 entries (#3094) 16 user_data entries have amlListAddedDate but no kycFileId assigned. This migration renumbers all kycFileIds from 2025-01-01 onwards to maintain chronological order based on amlListAddedDate. Affected: ~2194 entries (16 fixed, rest renumbered) --- migration/1769100000000-FixKycFileIds2025.js | 131 +++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 migration/1769100000000-FixKycFileIds2025.js diff --git a/migration/1769100000000-FixKycFileIds2025.js b/migration/1769100000000-FixKycFileIds2025.js new file mode 100644 index 0000000000..bd0ec533a6 --- /dev/null +++ b/migration/1769100000000-FixKycFileIds2025.js @@ -0,0 +1,131 @@ +/** + * @typedef {import('typeorm').MigrationInterface} MigrationInterface + * @typedef {import('typeorm').QueryRunner} QueryRunner + */ + +/** + * Fix kycFileId for user_data entries from 2025. + * + * Some entries have amlListAddedDate but no kycFileId assigned. + * This migration renumbers all kycFileIds from 2025-01-01 onwards + * to maintain chronological order based on amlListAddedDate. + * + * The starting kycFileId is dynamically determined as: + * MAX(kycFileId) from entries BEFORE 2025-01-01 + * + * @class + * @implements {MigrationInterface} + */ +module.exports = class FixKycFileIds20251769100000000 { + name = 'FixKycFileIds20251769100000000'; + + /** + * @param {QueryRunner} queryRunner + */ + async up(queryRunner) { + console.log('=== Fix kycFileId for 2025 entries ===\n'); + + // Get the last kycFileId that is NOT from 2025 (this will be our starting point) + // Includes entries with amlListAddedDate IS NULL for safety + const lastBefore2025 = await queryRunner.query(` + SELECT MAX(kycFileId) as maxId + FROM dbo.user_data + WHERE kycFileId > 0 + AND (amlListAddedDate IS NULL OR amlListAddedDate < '2025-01-01') + `); + const startId = lastBefore2025[0]?.maxId ?? 0; + console.log(`Last kycFileId before 2025: ${startId}`); + + if (startId === 0) { + console.log('ERROR: Could not determine starting kycFileId. Aborting.'); + return; + } + + // Count entries to process + const entriesToProcess = await queryRunner.query(` + SELECT COUNT(*) as count + FROM dbo.user_data + WHERE amlListAddedDate >= '2025-01-01' + `); + console.log(`Entries from 2025 to renumber: ${entriesToProcess[0].count}`); + + // Check how many are missing kycFileId before fix + const missingBefore = await queryRunner.query(` + SELECT id, amlListAddedDate + FROM dbo.user_data + WHERE amlListAddedDate >= '2025-01-01' + AND (kycFileId IS NULL OR kycFileId = 0) + ORDER BY amlListAddedDate + `); + console.log(`\nMissing kycFileId before fix: ${missingBefore.length}`); + + if (missingBefore.length === 0) { + console.log('No missing entries found. Skipping.\n'); + return; + } + + // Show the missing entries + console.log('Missing entries to fix:'); + for (const entry of missingBefore) { + console.log(` - ID ${entry.id}: ${entry.amlListAddedDate}`); + } + + // Renumber all 2025 entries, starting after the last pre-2025 kycFileId + const result = await queryRunner.query(` + WITH NewIds AS ( + SELECT id, ${startId} + ROW_NUMBER() OVER (ORDER BY amlListAddedDate ASC, id ASC) as new_kycFileId + FROM dbo.user_data + WHERE amlListAddedDate >= '2025-01-01' + ) + UPDATE ud + SET ud.kycFileId = n.new_kycFileId, ud.updated = GETDATE() + FROM dbo.user_data ud + INNER JOIN NewIds n ON ud.id = n.id + `); + + const rowsUpdated = result?.rowsAffected ?? entriesToProcess[0].count; + console.log(`\nUpdated ${rowsUpdated} rows`); + + // Verify fix + const missingAfter = await queryRunner.query(` + SELECT COUNT(*) as count + FROM dbo.user_data + WHERE amlListAddedDate >= '2025-01-01' + AND (kycFileId IS NULL OR kycFileId = 0) + `); + + if (missingAfter[0].count > 0) { + console.log(`\nWARNING: Still ${missingAfter[0].count} entries missing kycFileId!`); + } else { + console.log('\nVerification: All entries now have kycFileId'); + } + + // Show the fixed entries with their new kycFileIds + const fixedIds = missingBefore.map((e) => e.id); + const fixedEntries = await queryRunner.query(` + SELECT id, amlListAddedDate, kycFileId + FROM dbo.user_data + WHERE id IN (${fixedIds.join(',')}) + ORDER BY kycFileId + `); + console.log('\nFixed entries:'); + for (const entry of fixedEntries) { + console.log(` - ID ${entry.id}: kycFileId=${entry.kycFileId}`); + } + + console.log('\n=== SUMMARY ==='); + console.log(` Start kycFileId: ${startId + 1}`); + console.log(` End kycFileId: ${startId + entriesToProcess[0].count}`); + console.log(` Fixed missing: ${missingBefore.length} entries`); + console.log(` Total renumbered: ${rowsUpdated} entries`); + } + + /** + * @param {QueryRunner} queryRunner + */ + async down(queryRunner) { + // Rolling back would require storing the original kycFileId values. + // If needed, identify affected entries from migration logs and handle manually. + console.log('Down migration is not supported. Manual intervention required if rollback needed.'); + } +};