From 346031b0b19e4207c550a4dc1453010ec3c2d8a0 Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Thu, 5 Feb 2026 12:30:32 +0100 Subject: [PATCH 1/2] Add totalCustodyBalanceChfAuditPeriod column to UserData (#3136) Add custody balance field for audit period tracking, analogous to existing totalVolumeChfAuditPeriod. Includes entity column, migration, DTO, query select, and support service mapping. --- ...77-AddTotalCustodyBalanceChfAuditPeriod.js | 26 +++++++++++++++++++ .../support/dto/user-data-support.dto.ts | 1 + .../generic/support/support.service.ts | 1 + .../user-data/dto/update-user-data.dto.ts | 4 +++ .../user/models/user-data/user-data.entity.ts | 3 +++ .../models/user-data/user-data.service.ts | 1 + 6 files changed, 36 insertions(+) create mode 100644 migration/1770287692177-AddTotalCustodyBalanceChfAuditPeriod.js diff --git a/migration/1770287692177-AddTotalCustodyBalanceChfAuditPeriod.js b/migration/1770287692177-AddTotalCustodyBalanceChfAuditPeriod.js new file mode 100644 index 0000000000..1a042c275e --- /dev/null +++ b/migration/1770287692177-AddTotalCustodyBalanceChfAuditPeriod.js @@ -0,0 +1,26 @@ +/** + * @typedef {import('typeorm').MigrationInterface} MigrationInterface + * @typedef {import('typeorm').QueryRunner} QueryRunner + */ + +/** + * @class + * @implements {MigrationInterface} + */ +module.exports = class AddTotalCustodyBalanceChfAuditPeriod1770287692177 { + name = 'AddTotalCustodyBalanceChfAuditPeriod1770287692177' + + /** + * @param {QueryRunner} queryRunner + */ + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_data" ADD "totalCustodyBalanceChfAuditPeriod" float`); + } + + /** + * @param {QueryRunner} queryRunner + */ + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_data" DROP COLUMN "totalCustodyBalanceChfAuditPeriod"`); + } +} diff --git a/src/subdomains/generic/support/dto/user-data-support.dto.ts b/src/subdomains/generic/support/dto/user-data-support.dto.ts index 68ce06fa13..3569ca3268 100644 --- a/src/subdomains/generic/support/dto/user-data-support.dto.ts +++ b/src/subdomains/generic/support/dto/user-data-support.dto.ts @@ -94,6 +94,7 @@ export class KycFileListEntry { pep?: boolean; complexOrgStructure?: boolean; totalVolumeChfAuditPeriod?: number; + totalCustodyBalanceChfAuditPeriod?: number; } export class KycFileYearlyStats { diff --git a/src/subdomains/generic/support/support.service.ts b/src/subdomains/generic/support/support.service.ts index 495ecaab22..376f803080 100644 --- a/src/subdomains/generic/support/support.service.ts +++ b/src/subdomains/generic/support/support.service.ts @@ -161,6 +161,7 @@ export class SupportService { pep: userData.pep, complexOrgStructure: userData.complexOrgStructure, totalVolumeChfAuditPeriod: userData.totalVolumeChfAuditPeriod, + totalCustodyBalanceChfAuditPeriod: userData.totalCustodyBalanceChfAuditPeriod, }; } diff --git a/src/subdomains/generic/user/models/user-data/dto/update-user-data.dto.ts b/src/subdomains/generic/user/models/user-data/dto/update-user-data.dto.ts index a54fe56231..5e605151c5 100644 --- a/src/subdomains/generic/user/models/user-data/dto/update-user-data.dto.ts +++ b/src/subdomains/generic/user/models/user-data/dto/update-user-data.dto.ts @@ -249,6 +249,10 @@ export class UpdateUserDataDto { @IsNumber() totalVolumeChfAuditPeriod?: number; + @IsOptional() + @IsNumber() + totalCustodyBalanceChfAuditPeriod?: number; + @IsOptional() @IsBoolean() olkypayAllowed?: boolean; diff --git a/src/subdomains/generic/user/models/user-data/user-data.entity.ts b/src/subdomains/generic/user/models/user-data/user-data.entity.ts index 70c22c1cb2..d5430deb17 100644 --- a/src/subdomains/generic/user/models/user-data/user-data.entity.ts +++ b/src/subdomains/generic/user/models/user-data/user-data.entity.ts @@ -138,6 +138,9 @@ export class UserData extends IEntity { @Column({ type: 'float', nullable: true }) totalVolumeChfAuditPeriod?: number; + @Column({ type: 'float', nullable: true }) + totalCustodyBalanceChfAuditPeriod?: number; + // TODO remove @Column({ length: 256, nullable: true }) allBeneficialOwnersName?: string; diff --git a/src/subdomains/generic/user/models/user-data/user-data.service.ts b/src/subdomains/generic/user/models/user-data/user-data.service.ts index e435cdedec..6992b42f16 100644 --- a/src/subdomains/generic/user/models/user-data/user-data.service.ts +++ b/src/subdomains/generic/user/models/user-data/user-data.service.ts @@ -235,6 +235,7 @@ export class UserDataService { 'userData.pep', 'userData.complexOrgStructure', 'userData.totalVolumeChfAuditPeriod', + 'userData.totalCustodyBalanceChfAuditPeriod', 'country.name', ]) .where('userData.kycFileId > 0') From b60cbc77dd8ec71c0a911ed090649ac36b7590a9 Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Thu, 5 Feb 2026 13:30:38 +0100 Subject: [PATCH 2/2] Add outputDate column to transaction table (#3138) * Add outputDate column to transaction table Persist the transaction completion date directly on the transaction entity instead of only deriving it from related entities via the completionDate getter. Existing data is backfilled from buy_crypto, buy_fiat, and ref_reward in the migration. * Load transaction relation explicitly for ref-reward completion --- ...770300000000-AddOutputDateToTransaction.js | 53 +++++++++++++++++++ .../services/buy-crypto-out.service.ts | 4 ++ .../reward/services/ref-reward-out.service.ts | 6 ++- .../services/buy-fiat-preparation.service.ts | 5 ++ .../payment/entities/transaction.entity.ts | 5 +- .../payment/services/transaction.service.ts | 4 ++ 6 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 migration/1770300000000-AddOutputDateToTransaction.js diff --git a/migration/1770300000000-AddOutputDateToTransaction.js b/migration/1770300000000-AddOutputDateToTransaction.js new file mode 100644 index 0000000000..9e60fe3a53 --- /dev/null +++ b/migration/1770300000000-AddOutputDateToTransaction.js @@ -0,0 +1,53 @@ +/** + * @typedef {import('typeorm').MigrationInterface} MigrationInterface + * @typedef {import('typeorm').QueryRunner} QueryRunner + */ + +/** + * @class + * @implements {MigrationInterface} + */ +module.exports = class AddOutputDateToTransaction1770300000000 { + name = 'AddOutputDateToTransaction1770300000000'; + + /** + * @param {QueryRunner} queryRunner + */ + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "dbo"."transaction" ADD "outputDate" datetime2`); + + // Populate from buy_crypto + await queryRunner.query(` + UPDATE t + SET t."outputDate" = bc."outputDate" + FROM "dbo"."transaction" t + INNER JOIN "dbo"."buy_crypto" bc ON bc."transactionId" = t."id" + WHERE bc."outputDate" IS NOT NULL + `); + + // Populate from buy_fiat + await queryRunner.query(` + UPDATE t + SET t."outputDate" = bf."outputDate" + FROM "dbo"."transaction" t + INNER JOIN "dbo"."buy_fiat" bf ON bf."transactionId" = t."id" + WHERE bf."outputDate" IS NOT NULL + `); + + // Populate from ref_reward + await queryRunner.query(` + UPDATE t + SET t."outputDate" = rr."outputDate" + FROM "dbo"."transaction" t + INNER JOIN "dbo"."ref_reward" rr ON rr."transactionId" = t."id" + WHERE rr."outputDate" IS NOT NULL + `); + } + + /** + * @param {QueryRunner} queryRunner + */ + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "dbo"."transaction" DROP COLUMN "outputDate"`); + } +}; diff --git a/src/subdomains/core/buy-crypto/process/services/buy-crypto-out.service.ts b/src/subdomains/core/buy-crypto/process/services/buy-crypto-out.service.ts index d950ec84dc..aaa8322024 100644 --- a/src/subdomains/core/buy-crypto/process/services/buy-crypto-out.service.ts +++ b/src/subdomains/core/buy-crypto/process/services/buy-crypto-out.service.ts @@ -10,6 +10,7 @@ import { CustodyOrderService } from 'src/subdomains/core/custody/services/custod import { LiquidityOrderContext } from 'src/subdomains/supporting/dex/entities/liquidity-order.entity'; import { DexService } from 'src/subdomains/supporting/dex/services/dex.service'; import { FeeService } from 'src/subdomains/supporting/payment/services/fee.service'; +import { TransactionService } from 'src/subdomains/supporting/payment/services/transaction.service'; import { PayoutOrderContext } from 'src/subdomains/supporting/payout/entities/payout-order.entity'; import { PayoutRequest } from 'src/subdomains/supporting/payout/interfaces'; import { PayoutService } from 'src/subdomains/supporting/payout/services/payout.service'; @@ -39,6 +40,7 @@ export class BuyCryptoOutService { private readonly fiatService: FiatService, private readonly custodyOrderService: CustodyOrderService, private readonly feeService: FeeService, + private readonly transactionService: TransactionService, ) {} async payoutTransactions(): Promise { @@ -201,6 +203,8 @@ export class BuyCryptoOutService { await this.buyCryptoRepo.save(tx); + if (tx.transaction) await this.transactionService.completeTransaction(tx.transaction.id, tx.outputDate); + const custodyOrder = await this.custodyOrderService.getCustodyOrderByTx(tx); if (custodyOrder) { diff --git a/src/subdomains/core/referral/reward/services/ref-reward-out.service.ts b/src/subdomains/core/referral/reward/services/ref-reward-out.service.ts index 57cfd652c9..58c80dbc93 100644 --- a/src/subdomains/core/referral/reward/services/ref-reward-out.service.ts +++ b/src/subdomains/core/referral/reward/services/ref-reward-out.service.ts @@ -5,6 +5,7 @@ import { LiquidityOrderContext } from 'src/subdomains/supporting/dex/entities/li import { DexService } from 'src/subdomains/supporting/dex/services/dex.service'; import { PayoutOrderContext } from 'src/subdomains/supporting/payout/entities/payout-order.entity'; import { PayoutRequest } from 'src/subdomains/supporting/payout/interfaces'; +import { TransactionService } from 'src/subdomains/supporting/payment/services/transaction.service'; import { PayoutService } from 'src/subdomains/supporting/payout/services/payout.service'; import { RefReward, RewardStatus } from '../ref-reward.entity'; import { RefRewardRepository } from '../ref-reward.repository'; @@ -19,13 +20,14 @@ export class RefRewardOutService { private readonly payoutService: PayoutService, private readonly dexService: DexService, private readonly refRewardService: RefRewardService, + private readonly transactionService: TransactionService, ) {} async checkPaidTransaction(): Promise { try { const transactionsPaidOut = await this.refRewardRepo.find({ where: { status: RewardStatus.PAYING_OUT }, - relations: { user: true }, + relations: { user: true, transaction: true }, }); await this.checkCompletion(transactionsPaidOut); @@ -94,6 +96,8 @@ export class RefRewardOutService { if (isComplete) { await this.refRewardRepo.update(...tx.complete(payoutTxId)); + if (tx.transaction) await this.transactionService.completeTransaction(tx.transaction.id, tx.outputDate); + await this.dexService.completeOrders(LiquidityOrderContext.REF_PAYOUT, tx.id.toString()); await this.refRewardService.updatePaidRefCredit([tx.user?.id]); diff --git a/src/subdomains/core/sell-crypto/process/services/buy-fiat-preparation.service.ts b/src/subdomains/core/sell-crypto/process/services/buy-fiat-preparation.service.ts index db48b42aa7..b32bb0dbe0 100644 --- a/src/subdomains/core/sell-crypto/process/services/buy-fiat-preparation.service.ts +++ b/src/subdomains/core/sell-crypto/process/services/buy-fiat-preparation.service.ts @@ -17,6 +17,7 @@ import { PayInStatus } from 'src/subdomains/supporting/payin/entities/crypto-inp import { CryptoPaymentMethod, FiatPaymentMethod } from 'src/subdomains/supporting/payment/dto/payment-method.enum'; import { FeeService } from 'src/subdomains/supporting/payment/services/fee.service'; import { TransactionHelper } from 'src/subdomains/supporting/payment/services/transaction-helper'; +import { TransactionService } from 'src/subdomains/supporting/payment/services/transaction.service'; import { Price, PriceStep } from 'src/subdomains/supporting/pricing/domain/entities/price'; import { PriceCurrency, @@ -44,6 +45,7 @@ export class BuyFiatPreparationService { private readonly countryService: CountryService, private readonly buyFiatNotificationService: BuyFiatNotificationService, private readonly fiatOutputService: FiatOutputService, + private readonly transactionService: TransactionService, ) {} async doAmlCheck(): Promise { @@ -365,6 +367,9 @@ export class BuyFiatPreparationService { ...entity.complete(entity.fiatOutput.remittanceInfo, entity.fiatOutput.outputDate, entity.fiatOutput.bankTx), ); + if (entity.transaction) + await this.transactionService.completeTransaction(entity.transaction.id, entity.outputDate); + // send webhook await this.buyFiatService.triggerWebhook(entity); } catch (e) { diff --git a/src/subdomains/supporting/payment/entities/transaction.entity.ts b/src/subdomains/supporting/payment/entities/transaction.entity.ts index 7bee9def4a..43c37dcb0e 100644 --- a/src/subdomains/supporting/payment/entities/transaction.entity.ts +++ b/src/subdomains/supporting/payment/entities/transaction.entity.ts @@ -91,6 +91,9 @@ export class Transaction extends IEntity { @Column({ type: 'datetime2', nullable: true }) mailSendDate?: Date; + @Column({ type: 'datetime2', nullable: true }) + outputDate?: Date; + // References @OneToOne(() => BuyCrypto, (buyCrypto) => buyCrypto.transaction, { nullable: true }) buyCrypto?: BuyCrypto; @@ -183,6 +186,6 @@ export class Transaction extends IEntity { } get completionDate(): Date | undefined { - return this.buyCrypto?.outputDate ?? this.buyFiat?.outputDate ?? this.refReward?.outputDate; + return this.outputDate ?? this.buyCrypto?.outputDate ?? this.buyFiat?.outputDate ?? this.refReward?.outputDate; } } diff --git a/src/subdomains/supporting/payment/services/transaction.service.ts b/src/subdomains/supporting/payment/services/transaction.service.ts index f4ff249ffe..eaa916a4a7 100644 --- a/src/subdomains/supporting/payment/services/transaction.service.ts +++ b/src/subdomains/supporting/payment/services/transaction.service.ts @@ -188,6 +188,10 @@ export class TransactionService { return this.repo.find({ where: { userData: { id: userDataId } }, relations }); } + async completeTransaction(transactionId: number, outputDate: Date): Promise { + await this.repo.update(transactionId, { outputDate }); + } + async getTransactionByKey(key: string, value: any): Promise { return this.repo .createQueryBuilder('transaction')