Skip to content
Merged
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
131 changes: 131 additions & 0 deletions migration/1769100000000-FixKycFileIds2025.js
Original file line number Diff line number Diff line change
@@ -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.');
}
};
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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,
Expand All @@ -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;

Expand Down
127 changes: 127 additions & 0 deletions src/subdomains/supporting/support-issue/dto/support-issue.dto.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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;
} = {
Expand Down
Loading
Loading