From 7569953d248213c7c9ccd807f6136b39bf24e10d Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Thu, 5 Feb 2026 14:35:17 +0100 Subject: [PATCH 1/2] Add migration to adjust KYC stats for compliance report (#3132) --- .../1770246261000-AdjustKycStatsTestData.js | 176 ++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 migration/1770246261000-AdjustKycStatsTestData.js diff --git a/migration/1770246261000-AdjustKycStatsTestData.js b/migration/1770246261000-AdjustKycStatsTestData.js new file mode 100644 index 0000000000..c0e6256288 --- /dev/null +++ b/migration/1770246261000-AdjustKycStatsTestData.js @@ -0,0 +1,176 @@ +/** + * @typedef {import('typeorm').MigrationInterface} MigrationInterface + * @typedef {import('typeorm').QueryRunner} QueryRunner + */ + +/** + * Adjust KYC stats test data to match target values from PRD compliance report. + * + * Use this migration after a fresh PRD import to correct the statistics. + * + * Target values (from PRD compliance report 2026-02-04): + * 2021: newFiles=250, closed=0, endCount=250, highestFileNr=250 + * 2022: newFiles=1947, closed=5, endCount=2192, highestFileNr=2197 + * 2023: newFiles=509, closed=2127, endCount=574, highestFileNr=2706 + * 2024: newFiles=611, reopened=87, closed=253, endCount=1019, highestFileNr=3317 + * 2025: newFiles=2022, reopened=8, closed=2, endCount=3047, highestFileNr=5339 + * + * PRD state vs target (as of 2026-02-04): + * - 2021: 229 newFiles -> 250 (need +21, kycFileId 230-250 belong to 2021) + * - 2022: 1968 newFiles -> 1947 (need -21), 0 closed -> 5 (need +5) + * - 2023: 2049 closed -> 2127 (need +78, plus 5 moved to 2022 = +83, via 83 new closures) + * + * Data source verification: + * - Jan 2024 CSV: DFX Kundenliste_SRO inkl. Kypto Stichtag 15. Januar 2024 + * - 83 records identified as "geschlossen" in CSV but amlListExpiredDate=NULL in PRD + * - All 83 have amlListReactivatedDate (~2024-02-02) - reactivation cleared expiredDate + * - Migration restores the historical closure date while keeping reactivatedDate intact + * - 5 business accounts (kycFileId 54,71,374,473,649) need 2022 closing date + * - 21 records (kycFileId 230-250) need amlListAddedDate moved to 2021 + * + * Expected result: 2022=5 closed, 2023=2049-5+83=2127 closed (target 2127). + * + * @class + * @implements {MigrationInterface} + */ +module.exports = class AdjustKycStatsTestData1770246261000 { + name = 'AdjustKycStatsTestData1770246261000'; + + // Records with kycFileId 230-250 to move from 2022 to 2021 + // These were processed in early Jan 2022 but belong to 2021 per compliance report (highestFileNr 2021 = 250) + // Verified: 21 UserDataIds with kycFileId between 230-250 + movedTo2021Ids = [4161, 1202, 4180, 3949, 1137, 2077, 883, 1886, 4022, 3457, 4144, 3725, 1190, 2781, 4081, 3792, 4262, 3249, 4205, 4243, 1220]; + + // Business accounts to close in 2022 (change amlListExpiredDate from 2023-12-31 to 2022-12-31) + // These 5 Organization accounts have no formal Schliessdatum in CSV but were closed + // kycFileId: 54 -> 1358, 71 -> 1945, 374 -> 998, 473 -> 5365, 649 -> 6617 + closed2022Ids = [1358, 1945, 998, 5365, 6617]; + + // Records to close in 2023 (set amlListExpiredDate = 2023-12-31) + // Verified: These 83 records are "geschlossen" in Jan 2024 CSV AND have amlListExpiredDate=NULL in PRD + // Cross-referenced PRD NULL list (411 records) with CSV "geschlossen" status + // All 83 were reactivated (~2024-02-02) which cleared their expiredDate in PRD + // Migration restores the historical 2023 closure while keeping reactivatedDate intact + // Note: id 8968 holds kycFileId 1129 (transferred from id 7698 which CSV references) + closed2023Ids = [ + 803, 914, 915, 979, 983, 984, 1068, 1144, 1161, 1174, + 1200, 1289, 1412, 1437, 1449, 1502, 1508, 1548, 1559, 1599, + 1778, 1797, 1853, 2006, 2053, 2289, 2578, 2591, 2736, 2746, + 2939, 3008, 3057, 3258, 3297, 3365, 3403, 4015, 4485, 4550, + 4801, 4910, 5076, 5128, 5163, 5170, 5213, 5219, 5373, 5427, + 5479, 5615, 5649, 5662, 5733, 5798, 5951, 6230, 6533, 6568, + 6874, 7183, 7274, 7466, 7549, 8419, 8461, 8570, 8879, 8938, + 8968, 9146, 9327, 9388, 11104, 11123, 11195, 11531, 11827, 13566, + 29059, 31928, 185384 + ]; + + /** + * @param {QueryRunner} queryRunner + */ + async up(queryRunner) { + console.log('=== Adjusting KYC Stats Test Data ===\n'); + + // Get current stats before changes + const beforeStats = await this.getStats(queryRunner); + console.log('Before migration:'); + this.printStats(beforeStats); + + // 1. Move 21 records from 2022 to 2021 (kycFileId 230-250) + console.log('\nStep 1: Moving 21 records with kycFileId 230-250 from 2022 to 2021...'); + const moved = await queryRunner.query(` + UPDATE dbo.user_data + SET amlListAddedDate = DATEADD(year, -1, amlListAddedDate), + updated = GETDATE() + WHERE id IN (${this.movedTo2021Ids.join(',')}) + AND YEAR(amlListAddedDate) = 2022 + `); + console.log(` Affected: ${moved?.rowsAffected?.[0] ?? this.movedTo2021Ids.length} records`); + + // 2. Change 5 business accounts' closing date from 2023 to 2022 + console.log('\nStep 2: Changing 5 business accounts to closed in 2022...'); + const closed2022 = await queryRunner.query(` + UPDATE dbo.user_data + SET amlListExpiredDate = '2022-12-31', + updated = GETDATE() + WHERE id IN (${this.closed2022Ids.join(',')}) + AND YEAR(amlListExpiredDate) = 2023 + `); + console.log(` Affected: ${closed2022?.rowsAffected?.[0] ?? this.closed2022Ids.length} records`); + + // 3. Close 83 records in 2023 (all have amlListReactivatedDate set from ~2024-02-02) + console.log('\nStep 3: Closing 83 records in 2023...'); + const closed2023 = await queryRunner.query(` + UPDATE dbo.user_data + SET amlListExpiredDate = '2023-12-31', + updated = GETDATE() + WHERE id IN (${this.closed2023Ids.join(',')}) + AND amlListExpiredDate IS NULL + `); + console.log(` Affected: ${closed2023?.rowsAffected?.[0] ?? this.closed2023Ids.length} records`); + + // Get stats after changes + const afterStats = await this.getStats(queryRunner); + console.log('\nAfter migration:'); + this.printStats(afterStats); + + console.log('\n=== Migration Complete ==='); + } + + /** + * @param {QueryRunner} queryRunner + */ + async down(queryRunner) { + console.log('=== Reverting KYC Stats Test Data ===\n'); + + // 1. Reset amlListExpiredDate for 2023 records back to NULL + console.log('Step 1: Resetting 83 records back to NULL...'); + await queryRunner.query(` + UPDATE dbo.user_data + SET amlListExpiredDate = NULL, + updated = GETDATE() + WHERE id IN (${this.closed2023Ids.join(',')}) + AND amlListExpiredDate = '2023-12-31' + `); + + // 2. Change 5 business accounts back to 2023 + console.log('\nStep 2: Changing 5 business accounts back to 2023...'); + await queryRunner.query(` + UPDATE dbo.user_data + SET amlListExpiredDate = '2023-12-31', + updated = GETDATE() + WHERE id IN (${this.closed2022Ids.join(',')}) + AND amlListExpiredDate = '2022-12-31' + `); + + // 3. Move records back from 2021 to 2022 + console.log('\nStep 3: Moving 21 records back from 2021 to 2022...'); + await queryRunner.query(` + UPDATE dbo.user_data + SET amlListAddedDate = DATEADD(year, 1, amlListAddedDate), + updated = GETDATE() + WHERE id IN (${this.movedTo2021Ids.join(',')}) + AND YEAR(amlListAddedDate) = 2021 + `); + + console.log('\n=== Revert Complete ==='); + } + + async getStats(queryRunner) { + return queryRunner.query(` + SELECT YEAR(amlListExpiredDate) as year, COUNT(*) as closed + FROM dbo.user_data + WHERE amlListExpiredDate IS NOT NULL + GROUP BY YEAR(amlListExpiredDate) + ORDER BY YEAR(amlListExpiredDate) + `); + } + + printStats(stats) { + const targets = { 2022: 5, 2023: 2127, 2024: 253, 2025: 2 }; + for (const row of stats) { + const target = targets[row.year]; + const marker = target ? (row.closed === target ? '✓' : '✗') : ''; + console.log(` ${row.year}: ${row.closed} closed ${marker}`); + } + } +}; From 65408874963246a2f51223ee87b7c92938c3dba5 Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Thu, 5 Feb 2026 15:58:09 +0100 Subject: [PATCH 2/2] Add admin endpoint to calculate audit period numbers (#3140) --- .../1770400000000-AddAuditPeriodSetting.js | 27 ++++++++ .../core/custody/services/custody.service.ts | 69 +++++++++++++++++++ .../support/dto/user-data-support.dto.ts | 1 + .../generic/support/support.service.ts | 16 ++++- .../models/user-data/user-data.controller.ts | 8 +++ .../models/user-data/user-data.service.ts | 47 ++++++++++++- src/subdomains/generic/user/user.module.ts | 2 + .../payment/services/transaction.service.ts | 27 +++++++- .../pricing/services/asset-prices.service.ts | 20 ++++-- 9 files changed, 205 insertions(+), 12 deletions(-) create mode 100644 migration/1770400000000-AddAuditPeriodSetting.js diff --git a/migration/1770400000000-AddAuditPeriodSetting.js b/migration/1770400000000-AddAuditPeriodSetting.js new file mode 100644 index 0000000000..ce9052e31e --- /dev/null +++ b/migration/1770400000000-AddAuditPeriodSetting.js @@ -0,0 +1,27 @@ +/** + * @typedef {import('typeorm').MigrationInterface} MigrationInterface + * @typedef {import('typeorm').QueryRunner} QueryRunner + */ + +/** + * @class + * @implements {MigrationInterface} + */ +module.exports = class AddAuditPeriodSetting1770400000000 { + name = 'AddAuditPeriodSetting1770400000000'; + + /** + * @param {QueryRunner} queryRunner + */ + async up(queryRunner) { + const auditPeriod = JSON.stringify({ start: '2025-01-30T00:00:00Z', end: '2026-02-02T00:00:00Z' }); + await queryRunner.query(`INSERT INTO "dbo"."setting" ("key", "value") VALUES ('AuditPeriod', '${auditPeriod}')`); + } + + /** + * @param {QueryRunner} queryRunner + */ + async down(queryRunner) { + await queryRunner.query(`DELETE FROM "dbo"."setting" WHERE "key" = 'AuditPeriod'`); + } +}; diff --git a/src/subdomains/core/custody/services/custody.service.ts b/src/subdomains/core/custody/services/custody.service.ts index 7f785cd0c8..c47e89117f 100644 --- a/src/subdomains/core/custody/services/custody.service.ts +++ b/src/subdomains/core/custody/services/custody.service.ts @@ -3,6 +3,7 @@ import { Config } from 'src/config/config'; import { EvmUtil } from 'src/integration/blockchain/shared/evm/evm.util'; import { UserRole } from 'src/shared/auth/user-role.enum'; import { Asset } from 'src/shared/models/asset/asset.entity'; +import { AssetService } from 'src/shared/models/asset/asset.service'; import { AmountType, Util } from 'src/shared/utils/util'; import { AuthService } from 'src/subdomains/generic/user/models/auth/auth.service'; import { UserDataService } from 'src/subdomains/generic/user/models/user-data/user-data.service'; @@ -38,6 +39,7 @@ export class CustodyService { private readonly custodyOrderRepo: CustodyOrderRepository, private readonly custodyBalanceRepo: CustodyBalanceRepository, private readonly assetPricesService: AssetPricesService, + private readonly assetService: AssetService, ) {} // --- ACCOUNT --- // @@ -220,4 +222,71 @@ export class CustodyService { return { totalValue }; } + + async getUserTotalBalancesChf(date: Date): Promise> { + const balances = await this.getHistoricalBalances(date); + if (!balances.length) return new Map(); + + // Get historical prices + const assetIds = [...new Set(balances.map((b) => b.assetId))]; + const priceMap = await this.assetPricesService.getAssetPricesForDate(assetIds, date); + + // Sum CHF values per userDataId + const result = new Map(); + + for (const { userDataId, assetId, balance } of balances) { + const priceChf = + priceMap.get(assetId) ?? (await this.assetService.getAssetById(assetId).then((a) => a?.approxPriceChf)); + if (priceChf && balance > 0) { + result.set(userDataId, (result.get(userDataId) ?? 0) + balance * priceChf); + } + } + + return result; + } + + private async getHistoricalBalances(date: Date): Promise<{ userDataId: number; assetId: number; balance: number }[]> { + const deposits = await this.custodyOrderRepo + .createQueryBuilder('o') + .select('u.userDataId', 'userDataId') + .addSelect('o.inputAssetId', 'assetId') + .addSelect('SUM(o.inputAmount)', 'amount') + .innerJoin('o.user', 'u') + .where('o.status = :status', { status: CustodyOrderStatus.COMPLETED }) + .andWhere('o.updated <= :date', { date }) + .groupBy('u.userDataId') + .addGroupBy('o.inputAssetId') + .getRawMany<{ userDataId: number; assetId: number; amount: number }>(); + + const withdrawals = await this.custodyOrderRepo + .createQueryBuilder('o') + .select('u.userDataId', 'userDataId') + .addSelect('o.outputAssetId', 'assetId') + .addSelect('SUM(o.outputAmount)', 'amount') + .innerJoin('o.user', 'u') + .where('o.status != :status', { status: CustodyOrderStatus.CREATED }) + .andWhere('o.outputAssetId IS NOT NULL') + .andWhere('o.updated <= :date', { date }) + .groupBy('u.userDataId') + .addGroupBy('o.outputAssetId') + .getRawMany<{ userDataId: number; assetId: number; amount: number }>(); + + // Calculate net balances + const balanceMap = new Map(); + + for (const d of deposits) { + const key = `${d.userDataId}-${d.assetId}`; + balanceMap.set(key, { userDataId: d.userDataId, assetId: d.assetId, balance: d.amount }); + } + + for (const w of withdrawals) { + const key = `${w.userDataId}-${w.assetId}`; + const existing = balanceMap.get(key); + if (existing) { + existing.balance -= w.amount; + } + } + + return [...balanceMap.values()]; + } } 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 3569ca3268..d464ba0b78 100644 --- a/src/subdomains/generic/support/dto/user-data-support.dto.ts +++ b/src/subdomains/generic/support/dto/user-data-support.dto.ts @@ -90,6 +90,7 @@ export class KycFileListEntry { amlListAddedDate?: Date; amlListExpiredDate?: Date; amlListReactivatedDate?: Date; + newOpeningInAuditPeriod?: boolean; highRisk?: boolean; pep?: boolean; complexOrgStructure?: boolean; diff --git a/src/subdomains/generic/support/support.service.ts b/src/subdomains/generic/support/support.service.ts index 376f803080..cef410deea 100644 --- a/src/subdomains/generic/support/support.service.ts +++ b/src/subdomains/generic/support/support.service.ts @@ -2,6 +2,7 @@ import { BadRequestException, Inject, Injectable, NotFoundException, forwardRef import { isIP } from 'class-validator'; import * as IbanTools from 'ibantools'; import { Config } from 'src/config/config'; +import { SettingService } from 'src/shared/models/setting/setting.service'; import { Util } from 'src/shared/utils/util'; import { BuyCryptoService } from 'src/subdomains/core/buy-crypto/process/services/buy-crypto.service'; import { Buy } from 'src/subdomains/core/buy-crypto/routes/buy/buy.entity'; @@ -79,6 +80,7 @@ export class SupportService { private readonly bankService: BankService, @Inject(forwardRef(() => TransactionHelper)) private readonly transactionHelper: TransactionHelper, + private readonly settingService: SettingService, ) {} async getUserDataDetails(id: number): Promise { @@ -108,8 +110,14 @@ export class SupportService { }; } - getKycFileList(): Promise { - return this.userDataService.getUserDatasWithKycFile().then((u) => u.map((d) => this.toKycFileListEntry(d))); + async getKycFileList(): Promise { + const [userData, auditPeriod] = await Promise.all([ + this.userDataService.getUserDatasWithKycFile(), + this.settingService.getObj<{ start: string; end: string }>('AuditPeriod'), + ]); + const auditStartDate = auditPeriod?.start ? new Date(auditPeriod.start) : undefined; + + return userData.map((d) => this.toKycFileListEntry(d, auditStartDate)); } async getKycFileStats(startYear = 2021, endYear = new Date().getFullYear()): Promise { @@ -146,7 +154,7 @@ export class SupportService { // --- MAPPING METHODS --- // - private toKycFileListEntry(userData: UserData): KycFileListEntry { + private toKycFileListEntry(userData: UserData, auditStartDate?: Date): KycFileListEntry { return { kycFileId: userData.kycFileId, id: userData.id, @@ -157,6 +165,8 @@ export class SupportService { amlListAddedDate: userData.amlListAddedDate, amlListExpiredDate: userData.amlListExpiredDate, amlListReactivatedDate: userData.amlListReactivatedDate, + newOpeningInAuditPeriod: + auditStartDate && userData.amlListAddedDate ? new Date(userData.amlListAddedDate) > auditStartDate : false, highRisk: userData.highRisk, pep: userData.pep, complexOrgStructure: userData.complexOrgStructure, diff --git a/src/subdomains/generic/user/models/user-data/user-data.controller.ts b/src/subdomains/generic/user/models/user-data/user-data.controller.ts index 0a98cfe738..df438e7b7e 100644 --- a/src/subdomains/generic/user/models/user-data/user-data.controller.ts +++ b/src/subdomains/generic/user/models/user-data/user-data.controller.ts @@ -54,6 +54,14 @@ export class UserDataController { return this.userDataRepo.find(); } + @Put('auditPeriodNumbers') + @ApiBearerAuth() + @ApiExcludeEndpoint() + @UseGuards(AuthGuard(), RoleGuard(UserRole.ADMIN), UserActiveGuard()) + async calculateAuditPeriodNumbers(): Promise<{ updatedVolumes: number; updatedCustody: number }> { + return this.userDataService.calculateAuditPeriodNumbers(); + } + @Put(':id') @ApiBearerAuth() @ApiExcludeEndpoint() 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 6992b42f16..dbc31b3b81 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 @@ -23,8 +23,9 @@ import { RepositoryFactory } from 'src/shared/repositories/repository.factory'; import { ApiKeyService } from 'src/shared/services/api-key.service'; import { DfxLogger } from 'src/shared/services/dfx-logger'; import { DfxCron } from 'src/shared/utils/cron'; -import { Util } from 'src/shared/utils/util'; +import { AmountType, Util } from 'src/shared/utils/util'; import { CheckStatus } from 'src/subdomains/core/aml/enums/check-status.enum'; +import { CustodyService } from 'src/subdomains/core/custody/services/custody.service'; import { HistoryFilter, HistoryFilterKey } from 'src/subdomains/core/history/dto/history-filter.dto'; import { DefaultPaymentLinkConfig, @@ -46,7 +47,7 @@ import { MailContext } from 'src/subdomains/supporting/notification/enums'; import { SpecialExternalAccountService } from 'src/subdomains/supporting/payment/services/special-external-account.service'; import { TransactionService } from 'src/subdomains/supporting/payment/services/transaction.service'; import { transliterate } from 'transliteration'; -import { Equal, FindOptionsRelations, In, IsNull, Not } from 'typeorm'; +import { Equal, FindOptionsRelations, In, IsNull, MoreThan, Not } from 'typeorm'; import { WebhookService } from '../../services/webhook/webhook.service'; import { MergeReason } from '../account-merge/account-merge.entity'; import { AccountMergeService } from '../account-merge/account-merge.service'; @@ -110,6 +111,8 @@ export class UserDataService { @Inject(forwardRef(() => KycService)) private readonly kycService: KycService, private readonly ipLogService: IpLogService, + @Inject(forwardRef(() => CustodyService)) + private readonly custodyService: CustodyService, ) {} // --- GETTERS --- // @@ -961,6 +964,46 @@ export class UserDataService { }); } + // --- AUDIT PERIOD --- // + + async calculateAuditPeriodNumbers(): Promise<{ updatedVolumes: number; updatedCustody: number }> { + const auditPeriod = await this.settingService.getObj<{ start: string; end: string }>('AuditPeriod'); + if (!auditPeriod?.start || !auditPeriod?.end) throw new BadRequestException('Audit period not configured'); + + const startDate = new Date(auditPeriod.start); + const endDate = new Date(auditPeriod.end); + + // Reset all audit values + await this.userDataRepo.update( + [{ totalVolumeChfAuditPeriod: MoreThan(0) }, { totalCustodyBalanceChfAuditPeriod: MoreThan(0) }], + { totalVolumeChfAuditPeriod: 0, totalCustodyBalanceChfAuditPeriod: 0 }, + ); + + // Update volumes + const volumeResults = await this.transactionService.getAuditPeriodVolumes(startDate, endDate); + + for (const { userDataId, totalVolume: volume } of volumeResults) { + await this.userDataRepo.update(userDataId, { + totalVolumeChfAuditPeriod: Util.roundReadable(volume, AmountType.FIAT), + }); + } + + // Update custody balances + const custodyBalances = await this.custodyService.getUserTotalBalancesChf(endDate); + + for (const [userDataId, balanceChf] of custodyBalances.entries()) { + await this.userDataRepo.update(userDataId, { + totalCustodyBalanceChfAuditPeriod: Util.roundReadable(balanceChf, AmountType.FIAT), + }); + } + + this.logger.info( + `Audit period numbers calculated: ${volumeResults.length} volumes, ${custodyBalances.size} custody balances`, + ); + + return { updatedVolumes: volumeResults.length, updatedCustody: custodyBalances.size }; + } + // --- MERGING --- // async mergeUserData(masterId: number, slaveId: number, mail?: string, notifyUser = false): Promise { if (masterId === slaveId) throw new BadRequestException('Merging with oneself is not possible'); diff --git a/src/subdomains/generic/user/user.module.ts b/src/subdomains/generic/user/user.module.ts index 50e46c6038..7f734378d1 100644 --- a/src/subdomains/generic/user/user.module.ts +++ b/src/subdomains/generic/user/user.module.ts @@ -2,6 +2,7 @@ import { Module, forwardRef } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { BlockchainModule } from 'src/integration/blockchain/blockchain.module'; import { SiftModule } from 'src/integration/sift/sift.module'; +import { CustodyModule } from 'src/subdomains/core/custody/custody.module'; import { SharedModule } from 'src/shared/shared.module'; import { ReferralModule } from 'src/subdomains/core/referral/referral.module'; import { UserDataController } from 'src/subdomains/generic/user/models/user-data/user-data.controller'; @@ -81,6 +82,7 @@ import { WebhookService } from './services/webhook/webhook.service'; SiftModule, forwardRef(() => SupportIssueModule), forwardRef(() => TransactionModule), + forwardRef(() => CustodyModule), ], controllers: [ UserV2Controller, diff --git a/src/subdomains/supporting/payment/services/transaction.service.ts b/src/subdomains/supporting/payment/services/transaction.service.ts index eaa916a4a7..9e49dbf80f 100644 --- a/src/subdomains/supporting/payment/services/transaction.service.ts +++ b/src/subdomains/supporting/payment/services/transaction.service.ts @@ -1,10 +1,10 @@ -import { Injectable, NotFoundException, Inject, forwardRef } from '@nestjs/common'; +import { Inject, Injectable, NotFoundException, forwardRef } from '@nestjs/common'; import { Config } from 'src/config/config'; import { Util } from 'src/shared/utils/util'; import { BankDataType } from 'src/subdomains/generic/user/models/bank-data/bank-data.entity'; import { BankDataService } from 'src/subdomains/generic/user/models/bank-data/bank-data.service'; import { UserDataService } from 'src/subdomains/generic/user/models/user-data/user-data.service'; -import { Between, FindOptionsRelations, IsNull, LessThanOrEqual, Not } from 'typeorm'; +import { Between, Brackets, FindOptionsRelations, IsNull, LessThanOrEqual, Not } from 'typeorm'; import { CreateTransactionDto } from '../dto/input/create-transaction.dto'; import { UpdateTransactionInternalDto } from '../dto/input/update-transaction-internal.dto'; import { UpdateTransactionDto } from '../dto/update-transaction.dto'; @@ -192,6 +192,29 @@ export class TransactionService { await this.repo.update(transactionId, { outputDate }); } + async getAuditPeriodVolumes(startDate: Date, endDate: Date): Promise<{ userDataId: number; totalVolume: number }[]> { + return this.repo + .createQueryBuilder('tx') + .select('tx.userDataId', 'userDataId') + .addSelect('SUM(tx.amountInChf)', 'totalVolume') + .leftJoin('tx.buyCrypto', 'buyCrypto') + .leftJoin('tx.buyFiat', 'buyFiat') + .leftJoin('tx.refReward', 'refReward') + .where('tx.userDataId IS NOT NULL') + .andWhere('tx.amountInChf IS NOT NULL') + .andWhere('tx.created BETWEEN :startDate AND :endDate', { startDate, endDate }) + .andWhere( + new Brackets((qb) => + qb + .where('buyCrypto.outputDate BETWEEN :startDate AND :endDate') + .orWhere('buyFiat.outputDate BETWEEN :startDate AND :endDate') + .orWhere('refReward.outputDate BETWEEN :startDate AND :endDate'), + ), + ) + .groupBy('tx.userDataId') + .getRawMany(); + } + async getTransactionByKey(key: string, value: any): Promise { return this.repo .createQueryBuilder('transaction') diff --git a/src/subdomains/supporting/pricing/services/asset-prices.service.ts b/src/subdomains/supporting/pricing/services/asset-prices.service.ts index 1a176db6e7..d76d2e1264 100644 --- a/src/subdomains/supporting/pricing/services/asset-prices.service.ts +++ b/src/subdomains/supporting/pricing/services/asset-prices.service.ts @@ -16,18 +16,28 @@ export class AssetPricesService { }); } + async getAssetPricesForDate(assetIds: number[], date: Date): Promise> { + const prices = await this.getAssetPriceEntitiesForDate(assetIds, date); + return new Map(prices.map((p) => [p.asset.id, p.priceChf])); + } + async getAssetPriceForDate(assetId: number, date: Date): Promise { + const prices = await this.getAssetPriceEntitiesForDate([assetId], date); + return prices[0] ?? null; + } + + private async getAssetPriceEntitiesForDate(assetIds: number[], date: Date): Promise { + if (!assetIds.length) return []; + const startOfDay = new Date(date); startOfDay.setHours(0, 0, 0, 0); const endOfDay = new Date(date); endOfDay.setHours(23, 59, 59, 999); - return this.assetPriceRepo.findOne({ - where: { - asset: { id: assetId }, - created: Between(startOfDay, endOfDay), - }, + return this.assetPriceRepo.find({ + where: { asset: { id: In(assetIds) }, created: Between(startOfDay, endOfDay) }, + relations: { asset: true }, }); } }