diff --git a/CHANGELOG.md b/CHANGELOG.md index d2bf583e8..e940bdb0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,47 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Breaking Change**: The `sslmode=prefer` parameter in `DATABASE_URL` is no longer supported. Please update your environment variables (see `.env`) to use `sslmode=require` if _SSL_ is enabled or remove the `sslmode` parameter entirely if _SSL_ is not used. +## 2.237.0 - 2026-02-08 + +### Changed + +- Removed the deprecated `transactionCount` in the portfolio calculator and service +- Refreshed the cryptocurrencies list +- Upgraded `Nx` from version `22.4.1` to `22.4.5` + +### Fixed + +- Fixed the accounts of the assistant for the impersonation mode +- Fixed the tags of the assistant for the impersonation mode + +## 2.236.0 - 2026-02-05 + +### Changed + +- Removed the deprecated `transactionCount` in the endpoint `GET api/v1/admin` +- Upgraded `stripe` from version `20.1.0` to `20.3.0` + +### Fixed + +- Fixed an exception when fetching the top holdings for ETF and mutual fund assets from _Yahoo Finance_ + +## 2.235.0 - 2026-02-03 + +### Added + +- Added the ability to fetch top holdings for ETF and mutual fund assets from _Yahoo Finance_ +- Added support for the impersonation mode in the endpoint `GET api/v1/account/:id/balances` +- Added an action menu to the user detail dialog in the users section of the admin control panel + +### Changed + +- Optimized the value redaction interceptor for the impersonation mode by introducing `fast-redact` +- Refactored `showTransactions` in favor of `showActivitiesCount` in the accounts table component +- Refactored `transactionCount` in favor of `activitiesCount` in the accounts table component +- Deprecated `transactionCount` in favor of `activitiesCount` in the endpoint `GET api/v1/admin` +- Removed the deprecated `firstBuyDate` in the portfolio calculator +- Upgraded `yahoo-finance2` from version `3.11.2` to `3.13.0` + ## 2.234.0 - 2026-01-30 ### Changed diff --git a/apps/api/src/app/account/account.controller.ts b/apps/api/src/app/account/account.controller.ts index 542b199fd..052720176 100644 --- a/apps/api/src/app/account/account.controller.ts +++ b/apps/api/src/app/account/account.controller.ts @@ -132,12 +132,16 @@ export class AccountController { @UseGuards(AuthGuard('jwt'), HasPermissionGuard) @UseInterceptors(RedactValuesInResponseInterceptor) public async getAccountBalancesById( + @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string, @Param('id') id: string ): Promise { + const impersonationUserId = + await this.impersonationService.validateImpersonationId(impersonationId); + return this.accountBalanceService.getAccountBalances({ filters: [{ id, type: 'ACCOUNT' }], userCurrency: this.request.user.settings.settings.baseCurrency, - userId: this.request.user.id + userId: impersonationUserId || this.request.user.id }); } diff --git a/apps/api/src/app/account/account.service.ts b/apps/api/src/app/account/account.service.ts index 398a89bb9..e1b01a6ed 100644 --- a/apps/api/src/app/account/account.service.ts +++ b/apps/api/src/app/account/account.service.ts @@ -150,15 +150,15 @@ export class AccountService { }); return accounts.map((account) => { - let transactionCount = 0; + let activitiesCount = 0; for (const { isDraft } of account.activities) { if (!isDraft) { - transactionCount += 1; + activitiesCount += 1; } } - const result = { ...account, transactionCount }; + const result = { ...account, activitiesCount }; delete result.activities; diff --git a/apps/api/src/app/admin/admin.service.ts b/apps/api/src/app/admin/admin.service.ts index 705085a48..2cc8bbfb8 100644 --- a/apps/api/src/app/admin/admin.service.ts +++ b/apps/api/src/app/admin/admin.service.ts @@ -138,11 +138,11 @@ export class AdminService { public async get(): Promise { const dataSources = Object.values(DataSource); - const [enabledDataSources, settings, transactionCount, userCount] = + const [activitiesCount, enabledDataSources, settings, userCount] = await Promise.all([ + this.prismaService.order.count(), this.dataProviderService.getDataSources(), this.propertyService.get(), - this.prismaService.order.count(), this.countUsersWithAnalytics() ]); @@ -182,9 +182,9 @@ export class AdminService { ).filter(Boolean); return { + activitiesCount, dataProviders, settings, - transactionCount, userCount, version: environment.version }; diff --git a/apps/api/src/app/endpoints/market-data/market-data.controller.ts b/apps/api/src/app/endpoints/market-data/market-data.controller.ts index 987d34918..0dae82d2c 100644 --- a/apps/api/src/app/endpoints/market-data/market-data.controller.ts +++ b/apps/api/src/app/endpoints/market-data/market-data.controller.ts @@ -2,6 +2,8 @@ import { AdminService } from '@ghostfolio/api/app/admin/admin.service'; import { SymbolService } from '@ghostfolio/api/app/symbol/symbol.service'; import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; +import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor'; +import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor'; import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; import { @@ -28,7 +30,8 @@ import { Param, Post, Query, - UseGuards + UseGuards, + UseInterceptors } from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; import { AuthGuard } from '@nestjs/passport'; @@ -86,6 +89,8 @@ export class MarketDataController { @Get(':dataSource/:symbol') @UseGuards(AuthGuard('jwt')) + @UseInterceptors(TransformDataSourceInRequestInterceptor) + @UseInterceptors(TransformDataSourceInResponseInterceptor) public async getMarketDataBySymbol( @Param('dataSource') dataSource: DataSource, @Param('symbol') symbol: string diff --git a/apps/api/src/app/endpoints/market-data/market-data.module.ts b/apps/api/src/app/endpoints/market-data/market-data.module.ts index a8b355de3..d5d64673d 100644 --- a/apps/api/src/app/endpoints/market-data/market-data.module.ts +++ b/apps/api/src/app/endpoints/market-data/market-data.module.ts @@ -1,5 +1,7 @@ import { AdminModule } from '@ghostfolio/api/app/admin/admin.module'; import { SymbolModule } from '@ghostfolio/api/app/symbol/symbol.module'; +import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module'; +import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module'; import { MarketDataModule as MarketDataServiceModule } from '@ghostfolio/api/services/market-data/market-data.module'; import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module'; @@ -13,7 +15,9 @@ import { MarketDataController } from './market-data.controller'; AdminModule, MarketDataServiceModule, SymbolModule, - SymbolProfileModule + SymbolProfileModule, + TransformDataSourceInRequestModule, + TransformDataSourceInResponseModule ] }) export class MarketDataModule {} diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index b3b1d3410..2e58a4ef5 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts @@ -416,7 +416,6 @@ export abstract class PortfolioCalculator { dividendInBaseCurrency: totalDividendInBaseCurrency, fee: item.fee, feeInBaseCurrency: item.feeInBaseCurrency, - firstBuyDate: item.firstBuyDate, grossPerformance: !hasErrors ? (grossPerformance ?? null) : null, grossPerformancePercentage: !hasErrors ? (grossPerformancePercentage ?? null) @@ -446,7 +445,6 @@ export abstract class PortfolioCalculator { quantity: item.quantity, symbol: item.symbol, tags: item.tags, - transactionCount: item.transactionCount, valueInBaseCurrency: new Big(marketPriceInBaseCurrency).mul( item.quantity ) @@ -1004,11 +1002,9 @@ export abstract class PortfolioCalculator { fee: oldAccumulatedSymbol.fee.plus(fee), feeInBaseCurrency: oldAccumulatedSymbol.feeInBaseCurrency.plus(feeInBaseCurrency), - firstBuyDate: oldAccumulatedSymbol.firstBuyDate, includeInHoldings: oldAccumulatedSymbol.includeInHoldings, quantity: newQuantity, - tags: oldAccumulatedSymbol.tags.concat(tags), - transactionCount: oldAccumulatedSymbol.transactionCount + 1 + tags: oldAccumulatedSymbol.tags.concat(tags) }; } else { currentTransactionPointItem = { @@ -1024,11 +1020,9 @@ export abstract class PortfolioCalculator { averagePrice: unitPrice, dateOfFirstActivity: date, dividend: new Big(0), - firstBuyDate: date, includeInHoldings: INVESTMENT_ACTIVITY_TYPES.includes(type), investment: unitPrice.mul(quantity).mul(factor), - quantity: quantity.mul(factor), - transactionCount: 1 + quantity: quantity.mul(factor) }; } diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-buy.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-buy.spec.ts index a1021a57b..52c8489dd 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-buy.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-buy.spec.ts @@ -153,7 +153,6 @@ describe('PortfolioCalculator', () => { dividendInBaseCurrency: new Big('0'), fee: new Big('3.2'), feeInBaseCurrency: new Big('3.2'), - firstBuyDate: '2021-11-22', grossPerformance: new Big('36.6'), grossPerformancePercentage: new Big('0.07706261539956593567'), grossPerformancePercentageWithCurrencyEffect: new Big( @@ -179,7 +178,6 @@ describe('PortfolioCalculator', () => { timeWeightedInvestmentWithCurrencyEffect: new Big( '474.93846153846153846154' ), - transactionCount: 2, valueInBaseCurrency: new Big('595.6') } ], diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts index 002730e32..3998b081d 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts @@ -169,7 +169,6 @@ describe('PortfolioCalculator', () => { dividendInBaseCurrency: new Big('0'), fee: new Big('3.2'), feeInBaseCurrency: new Big('3.2'), - firstBuyDate: '2021-11-22', grossPerformance: new Big('-12.6'), grossPerformancePercentage: new Big('-0.04408677396780965649'), grossPerformancePercentageWithCurrencyEffect: new Big( @@ -193,7 +192,6 @@ describe('PortfolioCalculator', () => { timeWeightedInvestmentWithCurrencyEffect: new Big( '285.80000000000000396627' ), - transactionCount: 3, valueInBaseCurrency: new Big('0') } ], diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell.spec.ts index e4ba70158..acd0d0b2e 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell.spec.ts @@ -153,7 +153,6 @@ describe('PortfolioCalculator', () => { dividendInBaseCurrency: new Big('0'), fee: new Big('3.2'), feeInBaseCurrency: new Big('3.2'), - firstBuyDate: '2021-11-22', grossPerformance: new Big('-12.6'), grossPerformancePercentage: new Big('-0.0440867739678096571'), grossPerformancePercentageWithCurrencyEffect: new Big( @@ -177,7 +176,6 @@ describe('PortfolioCalculator', () => { tags: [], timeWeightedInvestment: new Big('285.8'), timeWeightedInvestmentWithCurrencyEffect: new Big('285.8'), - transactionCount: 2, valueInBaseCurrency: new Big('0') } ], diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy.spec.ts index e6cae7865..652e72db0 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy.spec.ts @@ -143,7 +143,6 @@ describe('PortfolioCalculator', () => { dividendInBaseCurrency: new Big('0'), fee: new Big('1.55'), feeInBaseCurrency: new Big('1.55'), - firstBuyDate: '2021-11-30', grossPerformance: new Big('24.6'), grossPerformancePercentage: new Big('0.09004392386530014641'), grossPerformancePercentageWithCurrencyEffect: new Big( @@ -173,7 +172,6 @@ describe('PortfolioCalculator', () => { tags: [], timeWeightedInvestment: new Big('273.2'), timeWeightedInvestmentWithCurrencyEffect: new Big('273.2'), - transactionCount: 1, valueInBaseCurrency: new Big('297.8') } ], diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur.spec.ts index 6cc58a70f..055356325 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur.spec.ts @@ -204,7 +204,6 @@ describe('PortfolioCalculator', () => { dividendInBaseCurrency: new Big('0'), fee: new Big('4.46'), feeInBaseCurrency: new Big('4.46'), - firstBuyDate: '2021-12-12', grossPerformance: new Big('-1458.72'), grossPerformancePercentage: new Big('-0.03273724696701543726'), grossPerformancePercentageWithCurrencyEffect: new Big( @@ -228,7 +227,6 @@ describe('PortfolioCalculator', () => { tags: [], timeWeightedInvestment: new Big('44558.42'), timeWeightedInvestmentWithCurrencyEffect: new Big('44558.42'), - transactionCount: 1, valueInBaseCurrency: new Big('43099.7') } ], diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts index 41f1d80a8..a70cc2986 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts @@ -167,7 +167,6 @@ describe('PortfolioCalculator', () => { dividendInBaseCurrency: new Big('0'), fee: new Big('0'), feeInBaseCurrency: new Big('0'), - firstBuyDate: '2015-01-01', grossPerformance: new Big('27172.74').mul(0.97373), grossPerformancePercentage: new Big('0.4241983590271396608571'), grossPerformancePercentageWithCurrencyEffect: new Big( @@ -195,7 +194,6 @@ describe('PortfolioCalculator', () => { timeWeightedInvestmentWithCurrencyEffect: new Big( '636.79389574611155533947' ), - transactionCount: 2, valueInBaseCurrency: new Big('13298.425356') } ], diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd.spec.ts index b8cecb350..64882061f 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd.spec.ts @@ -204,7 +204,6 @@ describe('PortfolioCalculator', () => { dividendInBaseCurrency: new Big('0'), fee: new Big('4.46'), feeInBaseCurrency: new Big('4.46'), - firstBuyDate: '2021-12-12', grossPerformance: new Big('-1458.72'), grossPerformancePercentage: new Big('-0.03273724696701543726'), grossPerformancePercentageWithCurrencyEffect: new Big( @@ -228,7 +227,6 @@ describe('PortfolioCalculator', () => { tags: [], timeWeightedInvestment: new Big('44558.42'), timeWeightedInvestmentWithCurrencyEffect: new Big('44558.42'), - transactionCount: 1, valueInBaseCurrency: new Big('43099.7') } ], diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts index bbcaba294..a53ebcf05 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts @@ -239,7 +239,6 @@ describe('PortfolioCalculator', () => { dividendInBaseCurrency: new Big(0), fee: new Big(0), feeInBaseCurrency: new Big(0), - firstBuyDate: '2023-12-31', grossPerformance: new Big(0), grossPerformancePercentage: new Big(0), grossPerformancePercentageWithCurrencyEffect: new Big( @@ -277,7 +276,6 @@ describe('PortfolioCalculator', () => { timeWeightedInvestmentWithCurrencyEffect: new Big( '852.45231607629427792916' ), - transactionCount: 2, valueInBaseCurrency: new Big(1820) }); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-googl-buy.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-googl-buy.spec.ts index e438d9c6d..9b48a1324 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-googl-buy.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-googl-buy.spec.ts @@ -149,7 +149,6 @@ describe('PortfolioCalculator', () => { dividendInBaseCurrency: new Big('0'), fee: new Big('1'), feeInBaseCurrency: new Big('0.9238'), - firstBuyDate: '2023-01-03', grossPerformance: new Big('27.33').mul(0.8854), grossPerformancePercentage: new Big('0.3066651705565529623'), grossPerformancePercentageWithCurrencyEffect: new Big( @@ -173,7 +172,6 @@ describe('PortfolioCalculator', () => { tags: [], timeWeightedInvestment: new Big('89.12').mul(0.8854), timeWeightedInvestmentWithCurrencyEffect: new Big('82.329056'), - transactionCount: 1, valueInBaseCurrency: new Big('103.10483') } ], diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-with-dividend.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-with-dividend.spec.ts index 88895b8c6..b19adb642 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-with-dividend.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-with-dividend.spec.ts @@ -139,7 +139,6 @@ describe('PortfolioCalculator', () => { dividend: new Big('0.62'), dividendInBaseCurrency: new Big('0.62'), fee: new Big('19'), - firstBuyDate: '2021-09-16', grossPerformance: new Big('33.25'), grossPerformancePercentage: new Big('0.11136043941322258691'), grossPerformancePercentageWithCurrencyEffect: new Big( @@ -163,8 +162,7 @@ describe('PortfolioCalculator', () => { }, quantity: new Big('1'), symbol: 'MSFT', - tags: [], - transactionCount: 2 + tags: [] } ], totalFeesWithCurrencyEffect: new Big('19'), diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell-partially.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell-partially.spec.ts index 8c0b1af6a..fecf17011 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell-partially.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell-partially.spec.ts @@ -149,7 +149,6 @@ describe('PortfolioCalculator', () => { dividendInBaseCurrency: new Big('0'), fee: new Big('4.25'), feeInBaseCurrency: new Big('4.25'), - firstBuyDate: '2022-03-07', grossPerformance: new Big('21.93'), grossPerformancePercentage: new Big('0.15113417083448194384'), grossPerformancePercentageWithCurrencyEffect: new Big( @@ -175,7 +174,6 @@ describe('PortfolioCalculator', () => { timeWeightedInvestmentWithCurrencyEffect: new Big( '145.10285714285714285714' ), - transactionCount: 2, valueInBaseCurrency: new Big('87.8') } ], diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell.spec.ts index c4850db66..adbb5c3ff 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell.spec.ts @@ -202,7 +202,6 @@ describe('PortfolioCalculator', () => { dividendInBaseCurrency: new Big('0'), fee: new Big('0'), feeInBaseCurrency: new Big('0'), - firstBuyDate: '2022-03-07', grossPerformance: new Big('19.86'), grossPerformancePercentage: new Big('0.13100263852242744063'), grossPerformancePercentageWithCurrencyEffect: new Big( @@ -226,7 +225,6 @@ describe('PortfolioCalculator', () => { tags: [], timeWeightedInvestment: new Big('151.6'), timeWeightedInvestmentWithCurrencyEffect: new Big('151.6'), - transactionCount: 2, valueInBaseCurrency: new Big('0') } ], diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-valuable.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-valuable.spec.ts index 5e73841ce..6fc94622f 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-valuable.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-valuable.spec.ts @@ -125,7 +125,6 @@ describe('PortfolioCalculator', () => { dividendInBaseCurrency: new Big('0'), fee: new Big('0'), feeInBaseCurrency: new Big('0'), - firstBuyDate: '2022-01-01', grossPerformance: new Big('0'), grossPerformancePercentage: new Big('0'), grossPerformancePercentageWithCurrencyEffect: new Big('0'), @@ -147,7 +146,6 @@ describe('PortfolioCalculator', () => { tags: [], timeWeightedInvestment: new Big('500000'), timeWeightedInvestmentWithCurrencyEffect: new Big('500000'), - transactionCount: 1, valueInBaseCurrency: new Big('500000') } ], diff --git a/apps/api/src/app/portfolio/interfaces/transaction-point-symbol.interface.ts b/apps/api/src/app/portfolio/interfaces/transaction-point-symbol.interface.ts index ab2351f11..7f3f54ff5 100644 --- a/apps/api/src/app/portfolio/interfaces/transaction-point-symbol.interface.ts +++ b/apps/api/src/app/portfolio/interfaces/transaction-point-symbol.interface.ts @@ -11,17 +11,10 @@ export interface TransactionPointSymbol { dividend: Big; fee: Big; feeInBaseCurrency: Big; - - /** @deprecated use dateOfFirstActivity instead */ - firstBuyDate: string; - includeInHoldings: boolean; investment: Big; quantity: Big; skipErrors: boolean; symbol: string; tags?: Tag[]; - - /** @deprecated use activitiesCount instead */ - transactionCount: number; } diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index a5a1d95ee..b8aefe0ac 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -195,11 +195,13 @@ export class PortfolioController { 'excludedAccountsAndActivities', 'fees', 'filteredValueInBaseCurrency', + 'fireWealth', 'grossPerformance', 'grossPerformanceWithCurrencyEffect', 'interestInBaseCurrency', 'items', 'liabilities', + 'liabilitiesInBaseCurrency', 'netPerformance', 'netPerformanceWithCurrencyEffect', 'totalBuy', diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 7db743a43..7be375473 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -233,7 +233,6 @@ export class PortfolioService { account.currency, userCurrency ), - transactionCount: activitiesCount, value: this.exchangeRateDataService.toCurrency( valueInBaseCurrency, userCurrency, @@ -284,7 +283,6 @@ export class PortfolioService { let totalDividendInBaseCurrency = new Big(0); let totalInterestInBaseCurrency = new Big(0); let totalValueInBaseCurrency = new Big(0); - let transactionCount = 0; for (const account of accounts) { activitiesCount += account.activitiesCount; @@ -301,8 +299,6 @@ export class PortfolioService { totalValueInBaseCurrency = totalValueInBaseCurrency.plus( account.valueInBaseCurrency ); - - transactionCount += account.transactionCount; } for (const account of accounts) { @@ -317,7 +313,6 @@ export class PortfolioService { return { accounts, activitiesCount, - transactionCount, totalBalanceInBaseCurrency: totalBalanceInBaseCurrency.toNumber(), totalDividendInBaseCurrency: totalDividendInBaseCurrency.toNumber(), totalInterestInBaseCurrency: totalInterestInBaseCurrency.toNumber(), @@ -576,8 +571,8 @@ export class PortfolioService { for (const { activitiesCount, currency, + dateOfFirstActivity, dividend, - firstBuyDate, grossPerformance, grossPerformanceWithCurrencyEffect, grossPerformancePercentage, @@ -591,7 +586,6 @@ export class PortfolioService { quantity, symbol, tags, - transactionCount, valueInBaseCurrency } of positions) { if (isFilteredByClosedHoldings === true) { @@ -625,7 +619,6 @@ export class PortfolioService { marketPrice, symbol, tags, - transactionCount, allocationInPercentage: filteredValueInBaseCurrency.eq(0) ? 0 : valueInBaseCurrency.div(filteredValueInBaseCurrency).toNumber(), @@ -633,7 +626,7 @@ export class PortfolioService { assetSubClass: assetProfile.assetSubClass, countries: assetProfile.countries, dataSource: assetProfile.dataSource, - dateOfFirstActivity: parseDate(firstBuyDate), + dateOfFirstActivity: parseDate(dateOfFirstActivity), dividend: dividend?.toNumber() ?? 0, grossPerformance: grossPerformance?.toNumber() ?? 0, grossPerformancePercent: grossPerformancePercentage?.toNumber() ?? 0, @@ -801,9 +794,9 @@ export class PortfolioService { activitiesCount, averagePrice, currency, + dateOfFirstActivity, dividendInBaseCurrency, feeInBaseCurrency, - firstBuyDate, grossPerformance, grossPerformancePercentage, grossPerformancePercentageWithCurrencyEffect, @@ -828,7 +821,10 @@ export class PortfolioService { }); const dividendYieldPercent = getAnnualizedPerformancePercent({ - daysInMarket: differenceInDays(new Date(), parseDate(firstBuyDate)), + daysInMarket: differenceInDays( + new Date(), + parseDate(dateOfFirstActivity) + ), netPerformancePercentage: timeWeightedInvestment.eq(0) ? new Big(0) : dividendInBaseCurrency.div(timeWeightedInvestment) @@ -836,7 +832,10 @@ export class PortfolioService { const dividendYieldPercentWithCurrencyEffect = getAnnualizedPerformancePercent({ - daysInMarket: differenceInDays(new Date(), parseDate(firstBuyDate)), + daysInMarket: differenceInDays( + new Date(), + parseDate(dateOfFirstActivity) + ), netPerformancePercentage: timeWeightedInvestmentWithCurrencyEffect.eq(0) ? new Big(0) : dividendInBaseCurrency.div(timeWeightedInvestmentWithCurrencyEffect) @@ -845,7 +844,7 @@ export class PortfolioService { const historicalData = await this.dataProviderService.getHistorical( [{ dataSource, symbol }], 'day', - parseISO(firstBuyDate), + parseISO(dateOfFirstActivity), new Date() ); @@ -910,7 +909,7 @@ export class PortfolioService { // Add historical entry for buy date, if no historical data available historicalDataArray.push({ averagePrice: activitiesOfHolding[0].unitPriceInAssetProfileCurrency, - date: firstBuyDate, + date: dateOfFirstActivity, marketPrice: activitiesOfHolding[0].unitPriceInAssetProfileCurrency, quantity: activitiesOfHolding[0].quantity }); @@ -924,6 +923,7 @@ export class PortfolioService { return { activitiesCount, + dateOfFirstActivity, marketPrice, marketPriceMax, marketPriceMin, @@ -931,7 +931,6 @@ export class PortfolioService { tags, averagePrice: averagePrice.toNumber(), dataProviderInfo: portfolioCalculator.getDataProviderInfos()?.[0], - dateOfFirstActivity: firstBuyDate, dividendInBaseCurrency: dividendInBaseCurrency.toNumber(), dividendYieldPercent: dividendYieldPercent.toNumber(), dividendYieldPercentWithCurrencyEffect: @@ -1690,7 +1689,6 @@ export class PortfolioService { sectors: [], symbol: currency, tags: [], - transactionCount: 0, valueInBaseCurrency: balance }; } diff --git a/apps/api/src/app/subscription/subscription.service.ts b/apps/api/src/app/subscription/subscription.service.ts index b38b07bb4..689ee3e6a 100644 --- a/apps/api/src/app/subscription/subscription.service.ts +++ b/apps/api/src/app/subscription/subscription.service.ts @@ -35,7 +35,7 @@ export class SubscriptionService { this.stripe = new Stripe( this.configurationService.get('STRIPE_SECRET_KEY'), { - apiVersion: '2025-12-15.clover' + apiVersion: '2026-01-28.clover' } ); } @@ -100,7 +100,6 @@ export class SubscriptionService { ); return { - sessionId: session.id, sessionUrl: session.url }; } diff --git a/apps/api/src/app/user/user.controller.ts b/apps/api/src/app/user/user.controller.ts index 397ae016b..6346ce43a 100644 --- a/apps/api/src/app/user/user.controller.ts +++ b/apps/api/src/app/user/user.controller.ts @@ -1,8 +1,11 @@ import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; +import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response/redact-values-in-response.interceptor'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; +import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service'; +import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config'; import { DeleteOwnUserDto, UpdateOwnAccessTokenDto, @@ -28,7 +31,8 @@ import { Param, Post, Put, - UseGuards + UseGuards, + UseInterceptors } from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; import { JwtService } from '@nestjs/jwt'; @@ -43,6 +47,7 @@ import { UserService } from './user.service'; export class UserController { public constructor( private readonly configurationService: ConfigurationService, + private readonly impersonationService: ImpersonationService, private readonly jwtService: JwtService, private readonly prismaService: PrismaService, private readonly propertyService: PropertyService, @@ -107,13 +112,19 @@ export class UserController { @Get() @UseGuards(AuthGuard('jwt'), HasPermissionGuard) + @UseInterceptors(RedactValuesInResponseInterceptor) public async getUser( - @Headers('accept-language') acceptLanguage: string + @Headers('accept-language') acceptLanguage: string, + @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string ): Promise { - return this.userService.getUser( - this.request.user, - acceptLanguage?.split(',')?.[0] - ); + const impersonationUserId = + await this.impersonationService.validateImpersonationId(impersonationId); + + return this.userService.getUser({ + impersonationUserId, + locale: acceptLanguage?.split(',')?.[0], + user: this.request.user + }); } @Post() diff --git a/apps/api/src/app/user/user.module.ts b/apps/api/src/app/user/user.module.ts index 8a21b0a55..7ca68d275 100644 --- a/apps/api/src/app/user/user.module.ts +++ b/apps/api/src/app/user/user.module.ts @@ -1,7 +1,9 @@ import { OrderModule } from '@ghostfolio/api/app/order/order.module'; import { SubscriptionModule } from '@ghostfolio/api/app/subscription/subscription.module'; +import { RedactValuesInResponseModule } from '@ghostfolio/api/interceptors/redact-values-in-response/redact-values-in-response.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; import { I18nModule } from '@ghostfolio/api/services/i18n/i18n.module'; +import { ImpersonationModule } from '@ghostfolio/api/services/impersonation/impersonation.module'; import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; import { PropertyModule } from '@ghostfolio/api/services/property/property.module'; import { TagModule } from '@ghostfolio/api/services/tag/tag.module'; @@ -18,6 +20,7 @@ import { UserService } from './user.service'; imports: [ ConfigurationModule, I18nModule, + ImpersonationModule, JwtModule.register({ secret: process.env.JWT_SECRET_KEY, signOptions: { expiresIn: '30 days' } @@ -25,6 +28,7 @@ import { UserService } from './user.service'; OrderModule, PrismaModule, PropertyModule, + RedactValuesInResponseModule, SubscriptionModule, TagModule ], diff --git a/apps/api/src/app/user/user.service.ts b/apps/api/src/app/user/user.service.ts index 3280fbfac..def0b94d9 100644 --- a/apps/api/src/app/user/user.service.ts +++ b/apps/api/src/app/user/user.service.ts @@ -30,7 +30,7 @@ import { PROPERTY_IS_READ_ONLY_MODE, PROPERTY_SYSTEM_MESSAGE, TAG_ID_EXCLUDE_FROM_ANALYSIS, - locale + locale as defaultLocale } from '@ghostfolio/common/config'; import { User as IUser, @@ -49,7 +49,7 @@ import { Injectable } from '@nestjs/common'; import { EventEmitter2 } from '@nestjs/event-emitter'; import { Prisma, Role, User } from '@prisma/client'; import { differenceInDays, subDays } from 'date-fns'; -import { sortBy, without } from 'lodash'; +import { without } from 'lodash'; import { createHmac } from 'node:crypto'; @Injectable() @@ -96,10 +96,17 @@ export class UserService { return { accessToken, hashedAccessToken }; } - public async getUser( - { accounts, id, permissions, settings, subscription }: UserWithSettings, - aLocale = locale - ): Promise { + public async getUser({ + impersonationUserId, + locale = defaultLocale, + user + }: { + impersonationUserId: string; + locale?: string; + user: UserWithSettings; + }): Promise { + const { id, permissions, settings, subscription } = user; + const userData = await Promise.all([ this.prismaService.access.findMany({ include: { @@ -108,22 +115,31 @@ export class UserService { orderBy: { alias: 'asc' }, where: { granteeUserId: id } }), + this.prismaService.account.findMany({ + orderBy: { + name: 'asc' + }, + where: { + userId: impersonationUserId || user.id + } + }), this.prismaService.order.count({ - where: { userId: id } + where: { userId: impersonationUserId || user.id } }), this.prismaService.order.findFirst({ orderBy: { date: 'asc' }, - where: { userId: id } + where: { userId: impersonationUserId || user.id } }), - this.tagService.getTagsForUser(id) + this.tagService.getTagsForUser(impersonationUserId || user.id) ]); const access = userData[0]; - const activitiesCount = userData[1]; - const firstActivity = userData[2]; - let tags = userData[3].filter((tag) => { + const accounts = userData[1]; + const activitiesCount = userData[2]; + const firstActivity = userData[3]; + let tags = userData[4].filter((tag) => { return tag.id !== TAG_ID_EXCLUDE_FROM_ANALYSIS; }); @@ -146,7 +162,6 @@ export class UserService { } return { - accounts, activitiesCount, id, permissions, @@ -160,10 +175,13 @@ export class UserService { permissions: accessItem.permissions }; }), + accounts: accounts.sort((a, b) => { + return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); + }), dateOfFirstActivity: firstActivity?.date ?? new Date(), settings: { ...(settings.settings as UserSettings), - locale: (settings.settings as UserSettings)?.locale ?? aLocale + locale: (settings.settings as UserSettings)?.locale ?? locale } }; } @@ -516,9 +534,10 @@ export class UserService { currentPermissions.push(permissions.impersonateAllUsers); } - user.accounts = sortBy(user.accounts, ({ name }) => { - return name.toLowerCase(); + user.accounts = user.accounts.sort((a, b) => { + return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); }); + user.permissions = currentPermissions.sort(); return user; diff --git a/apps/api/src/assets/cryptocurrencies/cryptocurrencies.json b/apps/api/src/assets/cryptocurrencies/cryptocurrencies.json index 6806bb8ff..e456f6f1d 100644 --- a/apps/api/src/assets/cryptocurrencies/cryptocurrencies.json +++ b/apps/api/src/assets/cryptocurrencies/cryptocurrencies.json @@ -4,6 +4,7 @@ "4": "4", "7": "Lucky7", "8": "8", + "21": "2131KOBUSHIDE", "32": "Project 32", "42": "Semantic Layer", "47": "President Trump", @@ -20,6 +21,7 @@ "1337": "EliteCoin", "1717": "1717 Masonic Commemorative Token", "2015": "2015 coin", + "2016": "2016 coin", "2024": "2024", "2025": "2025 TOKEN", "2026": "2026", @@ -116,6 +118,7 @@ "3DES": "3DES", "3DVANCE": "3D Vance", "3FT": "ThreeFold Token", + "3KDS": "3KDS", "3KM": "3 Kingdoms Multiverse", "3P": "Web3Camp", "3RDEYE": "3rd Eye", @@ -149,6 +152,7 @@ "7E": "7ELEVEN", "88MPH": "88mph", "8BIT": "8BIT Coin", + "8BITCOIN": "8-Bit COIN", "8BT": "8 Circuit Studios", "8LNDS": "8Lends", "8PAY": "8Pay", @@ -179,6 +183,7 @@ "AAC": "Double-A Chain", "AAG": "AAG Ventures", "AAI": "AutoAir AI", + "AALON": "American Airlines Group (Ondo Tokenized)", "AAPLON": "Apple (Ondo Tokenized)", "AAPLX": "Apple xStock", "AAPX": "AMPnet", @@ -713,6 +718,7 @@ "AMBRX": "Amber xStock", "AMBT": "AMBT Token", "AMC": "AI Meta Coin", + "AMCON": "AMC Entertainment (Ondo Tokenized)", "AMDC": "Allmedi Coin", "AMDG": "AMDG", "AMDX": "AMD xStock", @@ -933,6 +939,7 @@ "ARBI": "Arbipad", "ARBINU": "ArbInu", "ARBIT": "Arbit Coin", + "ARBITROVE": "Arbitrove Governance Token", "ARBP": "ARB Protocol", "ARBS": "Arbswap", "ARBT": "ARBITRAGE", @@ -1101,6 +1108,7 @@ "ASTA": "ASTA", "ASTER": "Aster", "ASTERINU": "Aster INU", + "ASTHERUSUSDF": "Astherus USDF", "ASTO": "Altered State Token", "ASTON": "Aston", "ASTONV": "Aston Villa Fan Token", @@ -1171,6 +1179,7 @@ "ATON": "Further Network", "ATOPLUS": "ATO+", "ATOR": "ATOR Protocol", + "ATOS": "Atoshi", "ATOZ": "Race Kingdom", "ATP": "Atlas Protocol", "ATPAY": "AtPay", @@ -1364,7 +1373,6 @@ "BABI": "Babylons", "BABL": "Babylon Finance", "BABY": "Babylon", - "BABY4": "Baby 4", "BABYANDY": "Baby Andy", "BABYASTER": "Baby Aster", "BABYB": "Baby Bali", @@ -2007,7 +2015,8 @@ "BIPC": "BipCoin", "BIPX": "Bispex", "BIR": "Birake", - "BIRB": "Birb", + "BIRB": "Moonbirds", + "BIRBV1": "Birb", "BIRD": "BIRD", "BIRDCHAIN": "Birdchain", "BIRDD": "BIRD DOG", @@ -2048,6 +2057,7 @@ "BITCOINCONFI": "Bitcoin Confidential", "BITCOINOTE": "BitcoiNote", "BITCOINP": "Bitcoin Private", + "BITCOINSCRYPT": "Bitcoin Scrypt", "BITCOINV": "BitcoinV", "BITCONNECT": "BitConnect Coin", "BITCORE": "BitCore", @@ -2181,6 +2191,7 @@ "BLOCKASSET": "Blockasset", "BLOCKB": "Block Browser", "BLOCKBID": "Blockbid", + "BLOCKCHAINTRADED": "Blockchain Traded Fund", "BLOCKF": "Block Farm Club", "BLOCKG": "BlockGames", "BLOCKIFY": "Blockify.Games", @@ -2808,7 +2819,7 @@ "BTCR": "BitCurrency", "BTCRED": "Bitcoin Red", "BTCRY": "BitCrystal", - "BTCS": "Bitcoin Scrypt", + "BTCS": "BTCs", "BTCSR": "BTC Strategic Reserve", "BTCST": "BTC Standard Hashrate Token", "BTCTOKEN": "Bitcoin Token", @@ -2823,9 +2834,10 @@ "BTELEGRAM": "BetterTelegram Token", "BTEV1": "Betero v1", "BTEX": "BTEX", - "BTF": "Blockchain Traded Fund", + "BTF": "Bitfinity Network", "BTFA": "Banana Task Force Ape", "BTG": "Bitcoin Gold", + "BTGON": "B2Gold (Ondo Tokenized)", "BTH": "Bithereum", "BTK": "Bostoken", "BTL": "Bitlocus", @@ -2973,9 +2985,11 @@ "BUSY": "Busy DAO", "BUT": "Bucket Token", "BUTT": "Buttercat", + "BUTTC": "Buttcoin", "BUTTCOIN": "The Next Bitcoin", "BUTTHOLE": "Butthole Coin", "BUTTPLUG": "fartcoin killer", + "BUTWHY": "ButWhy", "BUX": "BUX", "BUXCOIN": "Buxcoin", "BUY": "Burency", @@ -3007,6 +3021,7 @@ "BXA": "Blockchain Exchange Alliance", "BXBT": "BoxBet", "BXC": "BonusCloud", + "BXE": "Banxchange", "BXF": "BlackFort Token", "BXH": "BXH", "BXK": "Bitbook Gambling", @@ -3230,6 +3245,7 @@ "CATW": "Cat wif Hands", "CATWARRIOR": "Cat warrior", "CATWIF": "CatWifHat", + "CATWIFM": "catwifmask", "CATX": "CAT.trade Protocol", "CATZ": "CatzCoin", "CAU": "Canxium", @@ -3613,6 +3629,8 @@ "CLASH": "GeorgePlaysClashRoyale", "CLASHUB": "Clashub", "CLASS": "Class Coin", + "CLAWD": "clawd.atg.eth", + "CLAWNCH": "CLAWNCH", "CLAY": "Clayton", "CLAYN": "Clay Nation", "CLB": "Cloudbric", @@ -3637,7 +3655,8 @@ "CLIN": "Clinicoin", "CLINK": "cLINK", "CLINT": "Clinton", - "CLIPPY": "CLIPPY", + "CLIPPY": "Clippy", + "CLIPPYETH": "CLIPPY", "CLIPS": "Clips", "CLIQ": "DefiCliq", "CLIST": "Chainlist", @@ -3867,6 +3886,7 @@ "COPIUM": "Copium", "COPPER": "COPPER", "COPS": "Cops Finance", + "COPXON": "Global X Copper Miners ETF (Ondo Tokenized)", "COPYCAT": "Copycat Finance", "COQ": "Coq Inu", "COR": "Coreto", @@ -3944,6 +3964,7 @@ "CPLO": "Cpollo", "CPM": "Crypto Pump Meme", "CPN": "CompuCoin", + "CPNGON": "Coupang (Ondo Tokenized)", "CPO": "Cryptopolis", "CPOO": "Cockapoo", "CPOOL": "Clearpool", @@ -3976,6 +3997,7 @@ "CRAPPY": "CrappyBird", "CRASH": "Solana Crash", "CRASHBOYS": "CRASHBOYS", + "CRAT": "CratD2C", "CRAVE": "CraveCoin", "CRAYRABBIT": "CrazyRabbit", "CRAZ": "CRAZY FLOKI", @@ -4411,6 +4433,7 @@ "DANJ": "Danjuan Cat", "DANK": "DarkKush", "DANKDOGE": "Dank Doge", + "DANKDOGEAI": "DankDogeAI", "DANNY": "Degen Danny", "DAO": "DAO Maker", "DAO1": "DAO1", @@ -4897,6 +4920,7 @@ "DLXV": "Delta-X", "DLY": "Daily Finance", "DLYCOP": "Daily COP", + "DM": "Dumb Money", "DMA": "Dragoma", "DMAGA": "Dark MAGA", "DMAIL": "DMAIL Network", @@ -5096,7 +5120,8 @@ "DONKEY": "donkey", "DONNIEFIN": "Donnie Finance", "DONS": "The Dons", - "DONT": "Donald Trump (dont.cash)", + "DONT": "DisclaimerCoin", + "DONTCASH": "DONT", "DONU": "Donu", "DONUT": "Donut", "DONUTS": "The Simpsons", @@ -5451,6 +5476,7 @@ "ECET": "Evercraft Ecotechnologies", "ECG": "EcoSmart", "ECH": "EthereCash", + "ECHELON": "Echelon Token", "ECHO": "Echo", "ECHOBOT": "ECHO BOT", "ECHOD": "EchoDEX", @@ -5569,6 +5595,7 @@ "EHASH": "EHash", "EHIVE": "eHive", "EHRT": "Eight Hours Token", + "EICOIN": "EICOIN", "EIFI": "EIFI FINANCE", "EIGEN": "EigenLayer", "EIGENP": "Eigenpie", @@ -5899,6 +5926,7 @@ "ETHEREM": "Etherempires", "ETHEREUMMEME": "Solana Ethereum Meme", "ETHEREUMP": "ETHEREUMPLUS", + "ETHEREUMSCRYPT": "EthereumScrypt", "ETHERINC": "EtherInc", "ETHERKING": "Ether Kingdoms Token", "ETHERNITY": "Ethernity Chain", @@ -5921,7 +5949,7 @@ "ETHPR": "Ethereum Premium", "ETHPY": "Etherpay", "ETHR": "Ethereal", - "ETHS": "EthereumScrypt", + "ETHS": "Ethscriptions", "ETHSHIB": "Eth Shiba", "ETHV": "Ethverse", "ETHW": "Ethereum PoW", @@ -6096,6 +6124,7 @@ "F2C": "Ftribe Fighters", "F2K": "Farm2Kitchen", "F3": "Friend3", + "F5": "F5-promoT5", "F7": "Five7", "F9": "Falcon Nine", "FAB": "FABRK Token", @@ -6283,6 +6312,7 @@ "FIC": "Filecash", "FID": "Fidira", "FIDA": "Bonfida", + "FIDD": "Fidelity Digital Dollar", "FIDLE": "Fidlecoin", "FIDO": "FIDO", "FIDU": "Fidu", @@ -6672,6 +6702,7 @@ "FRR": "Frontrow", "FRSP": "Forkspot", "FRST": "FirstCoin", + "FRT": "FORT Token", "FRTC": "FART COIN", "FRTN": "EbisusBay Fortune", "FRTS": "Fruits", @@ -7211,6 +7242,7 @@ "GMDP": "GMD Protocol", "GME": "GameStop", "GMEE": "GAMEE", + "GMEON": "GameStop (Ondo Tokenized)", "GMEPEPE": "GAMESTOP PEPE", "GMETHERFRENS": "GM", "GMETRUMP": "GME TRUMP", @@ -7289,6 +7321,7 @@ "GOFINDXR": "Gofind XR", "GOFX": "GooseFX", "GOG": "Guild of Guardians", + "GOGE": "GOLD DOGE", "GOGLZ": "GOGGLES", "GOGLZV1": "GOGGLES v1", "GOGO": "GOGO Finance", @@ -7399,6 +7432,7 @@ "GPU": "Node AI", "GPUCOIN": "GPU Coin", "GPUINU": "GPU Inu", + "GPUNET": "GPUnet", "GPX": "GPEX", "GQ": "Galactic Quadrant", "GR": "GROM", @@ -7537,6 +7571,7 @@ "GTAN": "Giant Token", "GTAVI": "GTAVI", "GTBOT": "Gaming-T-Bot", + "GTBTC": "Gate Wrapped BTC", "GTC": "Gitcoin", "GTCC": "GTC COIN", "GTCOIN": "Game Tree", @@ -7599,6 +7634,7 @@ "GVT": "Genesis Vision", "GW": "Gyrowin", "GWD": "GreenWorld", + "GWEI": "ETHGas", "GWGW": "GoWrap", "GWT": "Galaxy War", "GX": "GameX", @@ -7915,7 +7951,7 @@ "HLG": "Holograph", "HLINK": "Chainlink (Harmony One Bridge)", "HLM": "Helium", - "HLN": "Holonus", + "HLN": "Ēnosys", "HLO": "Halo", "HLOV1": "Halo v1", "HLP": "Purpose Coin", @@ -7980,6 +8016,7 @@ "HOLDON4": "HoldOn4DearLife", "HOLDS": "Holdstation", "HOLO": "Holoworld", + "HOLON": "Holonus", "HOLY": "Holy Trinity", "HOM": "Homeety", "HOME": "Home", @@ -8199,6 +8236,7 @@ "IAI": "inheritance Art", "IAM": "IAME Identity", "IAOMIN": "Yao Ming", + "IAUON": "iShares Gold Trust (Ondo Tokenized)", "IB": "Iron Bank", "IBANK": "iBankCoin", "IBAT": "Battle Infinity", @@ -8357,7 +8395,7 @@ "IMS": "Independent Money System", "IMST": "Imsmart", "IMT": "Immortal Token", - "IMU": "imusify", + "IMUSIFY": "imusify", "IMVR": "ImmVRse", "IMX": "Immutable X", "IN": "INFINIT", @@ -8522,6 +8560,7 @@ "IRA": "Diligence", "IRC": "IRIS", "IRENA": "Irena Coin Apps", + "IRENON": "IREN (Ondo Tokenized)", "IRIS": "IRIS Network", "IRISTOKEN": "Iris Ecosystem", "IRL": "IrishCoin", @@ -8595,6 +8634,7 @@ "IVZ": "InvisibleCoin", "IW": "iWallet", "IWFT": "İstanbul Wild Cats", + "IWMON": "iShares Russell 2000 ETF (Ondo Tokenized)", "IWT": "IwToken", "IX": "X-Block", "IXC": "IXcoin", @@ -8698,6 +8738,7 @@ "JETCOIN": "Jetcoin", "JETFUEL": "Jetfuel Finance", "JETTON": "JetTon Game", + "JETUSD": "JETUSD", "JEUR": "Jarvis Synthetic Euro", "JEW": "Shekel", "JEWEL": "DeFi Kingdoms", @@ -8847,6 +8888,7 @@ "JWBTC": "Wrapped Bitcoin (TON Bridge)", "JWIF": "Jerrywifhat", "JWL": "Jewels", + "JWT": "JW Token", "JYAI": "Jerry The Turtle By Matt Furie", "JYC": "Joe-Yo Coin", "K": "Sidekick", @@ -8855,7 +8897,13 @@ "KAAI": "KanzzAI", "KAAS": "KAASY.AI", "KAB": "KABOSU", - "KABOSU": "Kabosu Family", + "KABOSU": "X Meme Dog", + "KABOSUCOIN": "Kabosu", + "KABOSUCOM": "Kabosu", + "KABOSUFAMILY": "Kabosu Family", + "KABOSUTOKEN": "Kabosu", + "KABOSUTOKENETH": "KABOSU", + "KABUTO": "Kabuto", "KABY": "Kaby Arena", "KAC": "KACO Finance", "KACY": "markkacy", @@ -9155,6 +9203,7 @@ "KNDC": "KanadeCoin", "KNDM": "Kingdom", "KNDX": "Kondux", + "KNEKTED": "Knekted", "KNFT": "KStarNFT", "KNG": "BetKings", "KNGN": "KingN Coin", @@ -9167,7 +9216,7 @@ "KNOW": "KNOW", "KNOX": "KnoxDAO", "KNS": "Kenshi", - "KNT": "Knekted", + "KNT": "KayakNet", "KNTO": "Kento", "KNTQ": "Kinetiq Governance Token", "KNU": "Keanu", @@ -9670,7 +9719,7 @@ "LITTLEGUY": "just a little guy", "LITTLEMANYU": "Little Manyu", "LIV": "LiviaCoin", - "LIVE": "TRONbetLive", + "LIVE": "SecondLive", "LIVENCOIN": "LivenPay", "LIVESEY": "Dr. Livesey", "LIVESTARS": "Live Stars", @@ -10008,6 +10057,7 @@ "M0": "M by M^0", "M1": "SupplyShock", "M2O": "M2O Token", + "M3H": "MehVerseCoin", "M3M3": "M3M3", "M87": "MESSIER", "MA": "Mind-AI", @@ -10026,6 +10076,8 @@ "MADOG": "MarvelDoge", "MADP": "Mad Penguin", "MADPEPE": "Mad Pepe", + "MADU": "Nicolas Maduro", + "MADURO": "MADURO", "MAECENAS": "Maecenas", "MAEP": "Maester Protocol", "MAF": "MetaMAFIA", @@ -10115,7 +10167,8 @@ "MANUSAI": "Manus AI Agent", "MANYU": "Manyu", "MANYUDOG": "MANYU", - "MAO": "Mao", + "MAO": "MAO", + "MAOMEME": "Mao", "MAOW": "MAOW", "MAP": "MAP Protocol", "MAPC": "MapCoin", @@ -10125,6 +10178,7 @@ "MAPS": "MAPS", "MAPU": "MatchAwards Platform Utility Token", "MAR3": "Mar3 AI", + "MARAON": "MARA Holdings (Ondo Tokenized)", "MARCO": "MELEGA", "MARCUS": "Marcus Cesar Inu", "MARE": "Mare Finance", @@ -10179,7 +10233,7 @@ "MASTERMIX": "Master MIX Token", "MASTERTRADER": "MasterTraderCoin", "MASYA": "MASYA", - "MAT": "My Master Wa", + "MAT": "Matchain", "MATA": "Ninneko", "MATAR": "MATAR AI", "MATCH": "Matching Game", @@ -10397,6 +10451,7 @@ "MEMEAI": "Meme Ai", "MEMEBRC": "MEME", "MEMECOIN": "just memecoin", + "MEMECOINDAOAI": "MemeCoinDAO", "MEMECUP": "Meme Cup", "MEMEETF": "Meme ETF", "MEMEFI": "MemeFi", @@ -10408,7 +10463,7 @@ "MEMEMUSK": "MEME MUSK", "MEMENTO": "MEMENTO•MORI (Runes)", "MEMERUNE": "MEME•ECONOMICS", - "MEMES": "MemeCoinDAO", + "MEMES": "memes will continue", "MEMESAI": "Memes AI", "MEMESQUAD": "Meme Squad", "MEMET": "MEMETOON", @@ -10596,7 +10651,7 @@ "MIININGNFT": "MiningNFT", "MIKE": "Mike", "MIKS": "MIKS COIN", - "MIL": "Milllionaire Coin", + "MIL": "Mil", "MILA": "MILADY MEME TOKEN", "MILC": "Micro Licensing Coin", "MILE": "milestoneBased", @@ -10609,6 +10664,7 @@ "MILLI": "Million", "MILLIM": "Millimeter", "MILLIMV1": "Millimeter v1", + "MILLLIONAIRECOIN": "Milllionaire Coin", "MILLY": "milly", "MILO": "Milo Inu", "MILOCEO": "Milo CEO", @@ -10879,6 +10935,7 @@ "MOLK": "Mobilink Token", "MOLLARS": "MollarsToken", "MOLLY": "Molly", + "MOLT": "Moltbook", "MOM": "Mother of Memes", "MOMA": "Mochi Market", "MOMIJI": "MAGA Momiji", @@ -10894,6 +10951,7 @@ "MONAV": "Monavale", "MONB": "MonbaseCoin", "MONDO": "mondo", + "MONEROAI": "Monero AI", "MONEROCHAN": "Monerochan", "MONET": "Claude Monet Memeory Coin", "MONETA": "Moneta", @@ -11084,6 +11142,7 @@ "MSCT": "MUSE ENT NFT", "MSD": "MSD", "MSFT": "Microsoft 6900", + "MSFTON": "Microsoft (Ondo Tokenized)", "MSFTX": "Microsoft xStock", "MSG": "MsgSender", "MSGO": "MetaSetGO", @@ -11107,6 +11166,7 @@ "MSTRX": "MicroStrategy xStock", "MSU": "MetaSoccer", "MSUSHI": "Sushi (Multichain)", + "MSVP": "MetaSoilVerseProtocol", "MSWAP": "MoneySwap", "MT": "Mint Token", "MTA": "Meta", @@ -11262,10 +11322,12 @@ "MYL": "MyLottoCoin", "MYLINX": "Linx", "MYLO": "MYLOCAT", + "MYMASTERWAR": "My Master Wa", "MYNE": "ITSMYNE", "MYO": "Mycro", "MYOBU": "Myōbu", "MYRA": "Mytheria", + "MYRC": "MYRC", "MYRE": "Myre", "MYRIA": "Myria", "MYRO": "Myro", @@ -11586,6 +11648,7 @@ "NIC": "NewInvestCoin", "NICE": "Nice", "NICEC": "NiceCoin", + "NIETZSCHEAN": "Nietzschean Penguin", "NIF": "Unifty", "NIFT": "Niftify", "NIFTSY": "Envelop", @@ -11617,6 +11680,7 @@ "NINU": "Nvidia Inu", "NIOB": "Niob Finance", "NIOCTIB": "nioctiB", + "NIOON": "NIO (Ondo Tokenized)", "NIOX": "Autonio", "NIOXV1": "Autonio v1", "NIOXV2": "Autonio v2", @@ -11750,6 +11814,7 @@ "NRCH": "EnreachDAO", "NRFB": "NuriFootBall", "NRG": "Energi", + "NRGE": "New Resources Generation Energy", "NRGY": "NRGY Defi", "NRK": "Nordek", "NRM": "Neuromachine", @@ -12308,6 +12373,7 @@ "OWB": "OWB", "OWC": "Oduwa", "OWD": "Owlstand", + "OWL": "Owlto", "OWLTOKEN": "OWL Token", "OWN": "OTHERWORLD", "OWNDATA": "OWNDATA", @@ -12342,6 +12408,7 @@ "P202": "Project 202", "P2P": "Sentinel", "P2PS": "P2P Solutions Foundation", + "P2PV1": "Sentinel", "P33L": "THE P33L", "P3D": "3DPass", "P404": "Potion 404", @@ -12593,6 +12660,7 @@ "PENDY": "Pendy", "PENG": "Peng", "PENGCOIN": "PENG", + "PENGO": "Petro Penguins", "PENGU": "Pudgy Penguins", "PENGUAI": "PENGU AI", "PENGUI": "Penguiana", @@ -12718,6 +12786,7 @@ "PEW": "pepe in a memes world", "PEX": "Pexcoin", "PF": "Purple Frog", + "PFEON": "Pfizer (Ondo Tokenized)", "PFEX": "Pfizer xStock", "PFF": "PumpFunFloki", "PFI": "PrimeFinance", @@ -12815,6 +12884,7 @@ "PIKAM": "Pikamoon", "PIKE": "Pike Token", "PIKO": "Pinnako", + "PIKZ": "PIKZ", "PILLAR": "PillarFi", "PILOT": "Unipilot", "PIM": "PIM", @@ -12952,6 +13022,7 @@ "PLSX": "PulseX", "PLT": "Poollotto.finance", "PLTC": "PlatonCoin", + "PLTRON": "Palantir Technologies (Ondo Tokenized)", "PLTRX": "Palantir xStock", "PLTX": "PlutusX", "PLTXYZ": "Add.xyz", @@ -13199,6 +13270,7 @@ "PRESI": "Turbo Trump", "PRESID": "President Ron DeSantis", "PRESIDEN": "President Elon", + "PRESSX": "PressX", "PRFT": "Proof Suite Token", "PRG": "Paragon", "PRI": "PRIVATEUM INITIATIVE", @@ -13431,6 +13503,7 @@ "PYME": "PymeDAO", "PYN": "Paynetic", "PYP": "PayPro", + "PYPLON": "PayPal (Ondo Tokenized)", "PYQ": "PolyQuity", "PYR": "Vulcan Forged", "PYRAM": "Pyram Token", @@ -13467,6 +13540,7 @@ "QBX": "qiibee foundation", "QBZ": "QUEENBEE", "QC": "Qcash", + "QCAD": "QCAD", "QCH": "QChi", "QCN": "Quazar Coin", "QCO": "Qravity", @@ -13538,6 +13612,7 @@ "QUA": "Quantum Tech", "QUAC": "QUACK", "QUACK": "Rich Quack", + "QUADRANS": "QuadransToken", "QUAI": "Quai Network", "QUAIN": "QUAIN", "QUAM": "Quam Network", @@ -13614,6 +13689,7 @@ "RAIF": "RAI Finance", "RAIIN": "Raiin", "RAIL": "Railgun", + "RAILS": "Rails Token", "RAIN": "Rain", "RAINBOW": "Rainbow Token", "RAINC": "RainCheck", @@ -13744,6 +13820,7 @@ "REALYN": "Real", "REAP": "ReapChain", "REAPER": "Grim Finance", + "REAT": "REAT", "REAU": "Vira-lata Finance", "REBD": "REBORN", "REBL": "REBL", @@ -14557,7 +14634,7 @@ "SENSOR": "Sensor Protocol", "SENSOV1": "SENSO v1", "SENSUS": "Sensus", - "SENT": "Sentinel", + "SENT": "Sentient", "SENTAI": "SentAI", "SENTI": "Sentinel Bot Ai", "SENTIS": "Sentism AI Token", @@ -14626,6 +14703,7 @@ "SGB": "Songbird", "SGDX": "eToro Singapore Dollar", "SGE": "Society of Galactic Exploration", + "SGI": "SmartGolfToken", "SGLY": "Singularity", "SGN": "Signals Network", "SGO": "SafuuGO", @@ -15151,6 +15229,7 @@ "SOETH": "Wrapped Ethereum (Sollet)", "SOFAC": "SofaCat", "SOFI": "RAI Finance", + "SOFION": "SoFi Technologies (Ondo Tokenized)", "SOFTCO": "SOFT COQ INU", "SOFTT": "Wrapped FTT (Sollet)", "SOGNI": "Sogni AI", @@ -15208,6 +15287,7 @@ "SOLIDSEX": "SOLIDsex: Tokenized veSOLID", "SOLINK": "Wrapped Chainlink (Sollet)", "SOLITO": "SOLITO", + "SOLKABOSU": "Kabosu", "SOLKIT": "Solana Kit", "SOLLY": "Solly", "SOLM": "SolMix", @@ -15432,6 +15512,7 @@ "SQG": "Squid Token", "SQGROW": "SquidGrow", "SQL": "Squall Coin", + "SQQQON": "ProShares UltraPro Short QQQ (Ondo Tokenized)", "SQR": "Magic Square", "SQRL": "Squirrel Swap", "SQT": "SubQuery Network", @@ -15519,6 +15600,7 @@ "STAK": "Jigstack", "STAKE": "xDai Chain", "STAKEDETH": "StakeHound Staked Ether", + "STAKERDAOWXTZ": "Wrapped Tezos", "STALIN": "StalinCoin", "STAMP": "SafePost", "STAN": "Stank Memes", @@ -16561,7 +16643,7 @@ "TRGI": "The Real Golden Inu", "TRHUB": "Tradehub", "TRI": "Triangles Coin", - "TRIA": "Triaconta", + "TRIA": "TRIA", "TRIAS": "Trias", "TRIBE": "Tribe", "TRIBETOKEN": "TribeToken", @@ -16596,6 +16678,7 @@ "TROLLMODE": "TROLL MODE", "TROLLRUN": "TROLL", "TROLLS": "trolls in a memes world", + "TRONBETLIVE": "TRONbetLive", "TRONDOG": "TronDog", "TRONI": "Tron Inu", "TRONP": "Donald Tronp", @@ -16603,7 +16686,7 @@ "TROP": "Interop", "TROPPY": "TROPPY", "TROSS": "Trossard", - "TROVE": "Arbitrove Governance Token", + "TROVE": "TROVE", "TROY": "Troy", "TRP": "Tronipay", "TRR": "Terran Coin", @@ -17048,7 +17131,8 @@ "USA": "Based USA", "USACOIN": "American Coin", "USAGIBNB": "U", - "USAT": "USAT", + "USAT": "Tether America USD", + "USATINC": "USAT", "USBT": "Universal Blockchain", "USC": "Ultimate Secure Cash", "USCC": "USC", @@ -17087,7 +17171,8 @@ "USDGLOBI": "Globiance USD Stablecoin", "USDGV1": "USDG v1", "USDGV2": "USDG", - "USDH": "USDH Hubble Stablecoin", + "USDH": "USDH", + "USDHHUBBLE": "USDH Hubble Stablecoin", "USDHL": "Hyper USD", "USDI": "Interest Protocol USDi", "USDJ": "USDJ", @@ -17182,8 +17267,9 @@ "UUU": "U Network", "UVT": "UvToken", "UW3S": "Utility Web3Shot", - "UWU": "UwU Lend", + "UWU": "Unlimited Wealth Utility", "UWUCOIN": "uwu", + "UWULEND": "UwU Lend", "UX": "Umee", "UXLINK": "UXLINK", "UXLINKV1": "UXLINK v1", @@ -17542,6 +17628,7 @@ "VSYS": "V Systems", "VT": "Virtual Tourist", "VTC": "Vertcoin", + "VTCN": "Versatize Coin", "VTG": "Victory Gem", "VTHO": "VeChainThor", "VTIX": "Vanguard xStock", @@ -17587,6 +17674,7 @@ "VYPER": "VYPER.WIN", "VYVO": "Vyvo AI", "VZ": "Vault Zero", + "VZON": "Verizon (Ondo Tokenized)", "VZT": "Vezt", "W": "Wormhole", "W1": "W1", @@ -17644,7 +17732,7 @@ "WANNA": "Wanna Bot", "WANUSDT": "wanUSDT", "WAP": "Wet Ass Pussy", - "WAR": "WeStarter", + "WAR": "WAR", "WARP": "WarpCoin", "WARPED": "Warped Games", "WARPIE": "Warpie", @@ -17713,7 +17801,7 @@ "WCFGV1": "Wrapped Centrifuge", "WCFX": "Wrapped Conflux", "WCG": "World Crypto Gold", - "WCHZ": "Chiliz (Portal Bridge)", + "WCHZ": "Wrapped Chiliz", "WCKB": "Wrapped Nervos Network", "WCOIN": "WCoin", "WCORE": "Wrapped Core", @@ -17786,6 +17874,7 @@ "WERK": "Werk Family", "WESHOWTOKEN": "WeShow Token", "WEST": "Waves Enterprise", + "WESTARTER": "WeStarter", "WET": "HumidiFi Token", "WETH": "WETH", "WETHV1": "WETH v1", @@ -17830,6 +17919,7 @@ "WHATSONPIC": "WhatsOnPic", "WHBAR": "Wrapped HBAR", "WHC": "Whales Club", + "WHCHZ": "Chiliz (Portal Bridge)", "WHEAT": "Wheat Token", "WHEE": "WHEE (Ordinals)", "WHEEL": "Wheelers", @@ -18118,6 +18208,7 @@ "WUF": "WUFFI", "WUK": "WUKONG", "WUKONG": "Sun Wukong", + "WULFON": "Terawulf (Ondo Tokenized)", "WULFY": "Wulfy", "WUM": "Unicorn Meat", "WUSD": "Worldwide USD", @@ -18143,7 +18234,6 @@ "WXPL": "Wrapped XPL", "WXRP": "Wrapped XRP", "WXT": "WXT", - "WXTZ": "Wrapped Tezos", "WYAC": "Woman Yelling At Cat", "WYN": "Wynn", "WYNN": "Anita Max Wynn", @@ -18186,6 +18276,7 @@ "XAS": "Asch", "XAT": "ShareAt", "XAUC": "XauCoin", + "XAUH": "Herculis Gold Coin", "XAUM": "Matrixdock Gold", "XAUR": "Xaurum", "XAUT": "Tether Gold", @@ -18778,7 +18869,8 @@ "ZEBU": "ZEBU", "ZEC": "ZCash", "ZECD": "ZCashDarkCoin", - "ZED": "ZedCoins", + "ZED": "ZED Token", + "ZEDCOIN": "ZedCoin", "ZEDD": "ZedDex", "ZEDTOKEN": "Zed Token", "ZEDX": "ZEDX Сoin", @@ -18932,6 +19024,7 @@ "ZOOM": "ZoomCoin", "ZOOMER": "Zoomer Coin", "ZOON": "CryptoZoon", + "ZOOSTORY": "ZOO", "ZOOT": "Zoo Token", "ZOOTOPIA": "Zootopia", "ZORA": "Zora", @@ -19009,5 +19102,13 @@ "vXDEFI": "vXDEFI", "wsOHM": "Wrapped Staked Olympus", "修仙": "修仙", - "币安人生": "币安人生" + "分红狗头": "分红狗头", + "哭哭马": "哭哭马", + "安": "安", + "币安人生": "币安人生", + "恶俗企鹅": "恶俗企鹅", + "我踏马来了": "我踏马来了", + "老子": "老子", + "雪球": "雪球", + "黑马": "黑马" } diff --git a/apps/api/src/helper/object.helper.spec.ts b/apps/api/src/helper/object.helper.spec.ts index e1ec81b8f..ed821390f 100644 --- a/apps/api/src/helper/object.helper.spec.ts +++ b/apps/api/src/helper/object.helper.spec.ts @@ -1,4 +1,6 @@ -import { query, redactAttributes } from './object.helper'; +import { DEFAULT_REDACTED_PATHS } from '@ghostfolio/common/config'; + +import { query, redactPaths } from './object.helper'; describe('query', () => { it('should get market price from stock API response', () => { @@ -22,46 +24,38 @@ describe('query', () => { describe('redactAttributes', () => { it('should redact provided attributes', () => { - expect(redactAttributes({ object: {}, options: [] })).toStrictEqual({}); + expect(redactPaths({ object: {}, paths: [] })).toStrictEqual({}); - expect( - redactAttributes({ object: { value: 1000 }, options: [] }) - ).toStrictEqual({ value: 1000 }); + expect(redactPaths({ object: { value: 1000 }, paths: [] })).toStrictEqual({ + value: 1000 + }); expect( - redactAttributes({ + redactPaths({ object: { value: 1000 }, - options: [{ attribute: 'value', valueMap: { '*': null } }] + paths: ['value'] }) ).toStrictEqual({ value: null }); expect( - redactAttributes({ + redactPaths({ object: { value: 'abc' }, - options: [{ attribute: 'value', valueMap: { abc: 'xyz' } }] + paths: ['value'], + valueMap: { abc: 'xyz' } }) ).toStrictEqual({ value: 'xyz' }); expect( - redactAttributes({ + redactPaths({ object: { data: [{ value: 'a' }, { value: 'b' }] }, - options: [{ attribute: 'value', valueMap: { a: 1, b: 2 } }] + paths: ['data[*].value'], + valueMap: { a: 1, b: 2 } }) ).toStrictEqual({ data: [{ value: 1 }, { value: 2 }] }); - expect( - redactAttributes({ - object: { value1: 'a', value2: 'b' }, - options: [ - { attribute: 'value1', valueMap: { a: 'x' } }, - { attribute: 'value2', valueMap: { '*': 'y' } } - ] - }) - ).toStrictEqual({ value1: 'x', value2: 'y' }); - console.time('redactAttributes execution time'); expect( - redactAttributes({ + redactPaths({ object: { accounts: { '2e937c05-657c-4de9-8fb3-0813a2245f26': { @@ -117,6 +111,7 @@ describe('redactAttributes', () => { hasError: false, holdings: { 'AAPL.US': { + activitiesCount: 1, currency: 'USD', markets: { UNKNOWN: 0, @@ -136,7 +131,6 @@ describe('redactAttributes', () => { marketPrice: 220.79, symbol: 'AAPL.US', tags: [], - transactionCount: 1, allocationInPercentage: 0.044900865255793135, assetClass: 'EQUITY', assetSubClass: 'STOCK', @@ -169,6 +163,7 @@ describe('redactAttributes', () => { valueInPercentage: 0.0694356974830054 }, 'ALV.DE': { + activitiesCount: 2, currency: 'EUR', markets: { UNKNOWN: 0, @@ -188,7 +183,6 @@ describe('redactAttributes', () => { marketPrice: 296.5, symbol: 'ALV.DE', tags: [], - transactionCount: 2, allocationInPercentage: 0.026912563036519527, assetClass: 'EQUITY', assetSubClass: 'STOCK', @@ -216,6 +210,7 @@ describe('redactAttributes', () => { valueInPercentage: 0.04161818652826481 }, AMZN: { + activitiesCount: 1, currency: 'USD', markets: { UNKNOWN: 0, @@ -235,7 +230,6 @@ describe('redactAttributes', () => { marketPrice: 187.99, symbol: 'AMZN', tags: [], - transactionCount: 1, allocationInPercentage: 0.07646101417126275, assetClass: 'EQUITY', assetSubClass: 'STOCK', @@ -268,6 +262,7 @@ describe('redactAttributes', () => { valueInPercentage: 0.11824101426541227 }, bitcoin: { + activitiesCount: 1, currency: 'USD', markets: { UNKNOWN: 36985.0332704, @@ -293,7 +288,6 @@ describe('redactAttributes', () => { userId: null } ], - transactionCount: 1, allocationInPercentage: 0.15042891393226654, assetClass: 'LIQUIDITY', assetSubClass: 'CRYPTOCURRENCY', @@ -319,6 +313,7 @@ describe('redactAttributes', () => { valueInPercentage: 0.232626620912395 }, BONDORA_GO_AND_GROW: { + activitiesCount: 5, currency: 'EUR', markets: { UNKNOWN: 2231.644722160232, @@ -344,7 +339,6 @@ describe('redactAttributes', () => { userId: null } ], - transactionCount: 5, allocationInPercentage: 0.009076749759365777, assetClass: 'FIXED_INCOME', assetSubClass: 'BOND', @@ -370,6 +364,7 @@ describe('redactAttributes', () => { valueInPercentage: 0.014036487867880205 }, FRANKLY95P: { + activitiesCount: 6, currency: 'CHF', markets: { UNKNOWN: 0, @@ -395,7 +390,6 @@ describe('redactAttributes', () => { userId: null } ], - transactionCount: 6, allocationInPercentage: 0.09095764645669335, assetClass: 'EQUITY', assetSubClass: 'ETF', @@ -494,6 +488,7 @@ describe('redactAttributes', () => { valueInPercentage: 0.14065892911313693 }, MSFT: { + activitiesCount: 1, currency: 'USD', markets: { UNKNOWN: 0, @@ -513,7 +508,6 @@ describe('redactAttributes', () => { marketPrice: 428.02, symbol: 'MSFT', tags: [], - transactionCount: 1, allocationInPercentage: 0.05222646409742627, assetClass: 'EQUITY', assetSubClass: 'STOCK', @@ -546,6 +540,7 @@ describe('redactAttributes', () => { valueInPercentage: 0.08076416659271518 }, TSLA: { + activitiesCount: 1, currency: 'USD', markets: { UNKNOWN: 0, @@ -565,7 +560,6 @@ describe('redactAttributes', () => { marketPrice: 260.46, symbol: 'TSLA', tags: [], - transactionCount: 1, allocationInPercentage: 0.1589050142378352, assetClass: 'EQUITY', assetSubClass: 'STOCK', @@ -598,6 +592,7 @@ describe('redactAttributes', () => { valueInPercentage: 0.2457342510950259 }, VTI: { + activitiesCount: 5, currency: 'USD', markets: { UNKNOWN: 0, @@ -617,7 +612,6 @@ describe('redactAttributes', () => { marketPrice: 282.05, symbol: 'VTI', tags: [], - transactionCount: 5, allocationInPercentage: 0.057358979326040366, assetClass: 'EQUITY', assetSubClass: 'ETF', @@ -770,6 +764,7 @@ describe('redactAttributes', () => { valueInPercentage: 0.08870120238725339 }, 'VWRL.SW': { + activitiesCount: 5, currency: 'CHF', markets: { UNKNOWN: 0, @@ -789,7 +784,6 @@ describe('redactAttributes', () => { marketPrice: 117.62, symbol: 'VWRL.SW', tags: [], - transactionCount: 5, allocationInPercentage: 0.09386983901959013, assetClass: 'EQUITY', assetSubClass: 'ETF', @@ -1178,6 +1172,7 @@ describe('redactAttributes', () => { valueInPercentage: 0.145162408515095 }, 'XDWD.DE': { + activitiesCount: 1, currency: 'EUR', markets: { UNKNOWN: 0, @@ -1197,7 +1192,6 @@ describe('redactAttributes', () => { marketPrice: 105.72, symbol: 'XDWD.DE', tags: [], - transactionCount: 1, allocationInPercentage: 0.03598477442100562, assetClass: 'EQUITY', assetSubClass: 'ETF', @@ -1456,6 +1450,7 @@ describe('redactAttributes', () => { valueInPercentage: 0.055647656152211074 }, USD: { + activitiesCount: 0, currency: 'USD', allocationInPercentage: 0.20291717628620132, assetClass: 'LIQUIDITY', @@ -1478,7 +1473,6 @@ describe('redactAttributes', () => { sectors: [], symbol: 'USD', tags: [], - transactionCount: 0, valueInBaseCurrency: 49890, valueInPercentage: 0.3137956381563603 } @@ -1564,34 +1558,7 @@ describe('redactAttributes', () => { currentNetWorth: null } }, - options: [ - 'balance', - 'balanceInBaseCurrency', - 'comment', - 'convertedBalance', - 'dividendInBaseCurrency', - 'fee', - 'feeInBaseCurrency', - 'grossPerformance', - 'grossPerformanceWithCurrencyEffect', - 'investment', - 'netPerformance', - 'netPerformanceWithCurrencyEffect', - 'quantity', - 'symbolMapping', - 'totalBalanceInBaseCurrency', - 'totalValueInBaseCurrency', - 'unitPrice', - 'value', - 'valueInBaseCurrency' - ].map((attribute) => { - return { - attribute, - valueMap: { - '*': null - } - }; - }) + paths: DEFAULT_REDACTED_PATHS }) ).toStrictEqual({ accounts: { @@ -1648,6 +1615,7 @@ describe('redactAttributes', () => { hasError: false, holdings: { 'AAPL.US': { + activitiesCount: 1, currency: 'USD', markets: { UNKNOWN: 0, @@ -1667,7 +1635,6 @@ describe('redactAttributes', () => { marketPrice: 220.79, symbol: 'AAPL.US', tags: [], - transactionCount: 1, allocationInPercentage: 0.044900865255793135, assetClass: 'EQUITY', assetSubClass: 'STOCK', @@ -1681,7 +1648,7 @@ describe('redactAttributes', () => { ], dataSource: 'EOD_HISTORICAL_DATA', dateOfFirstActivity: '2021-11-30T23:00:00.000Z', - dividend: 0, + dividend: null, grossPerformance: null, grossPerformancePercent: 0.3183066634822068, grossPerformancePercentWithCurrencyEffect: 0.3183066634822068, @@ -1700,6 +1667,7 @@ describe('redactAttributes', () => { valueInPercentage: 0.0694356974830054 }, 'ALV.DE': { + activitiesCount: 2, currency: 'EUR', markets: { UNKNOWN: 0, @@ -1719,7 +1687,6 @@ describe('redactAttributes', () => { marketPrice: 296.5, symbol: 'ALV.DE', tags: [], - transactionCount: 2, allocationInPercentage: 0.026912563036519527, assetClass: 'EQUITY', assetSubClass: 'STOCK', @@ -1728,7 +1695,7 @@ describe('redactAttributes', () => { ], dataSource: 'YAHOO', dateOfFirstActivity: '2021-04-22T22:00:00.000Z', - dividend: 192, + dividend: null, grossPerformance: null, grossPerformancePercent: 0.3719230057375532, grossPerformancePercentWithCurrencyEffect: 0.2650716044872953, @@ -1747,6 +1714,7 @@ describe('redactAttributes', () => { valueInPercentage: 0.04161818652826481 }, AMZN: { + activitiesCount: 1, currency: 'USD', markets: { UNKNOWN: 0, @@ -1766,7 +1734,6 @@ describe('redactAttributes', () => { marketPrice: 187.99, symbol: 'AMZN', tags: [], - transactionCount: 1, allocationInPercentage: 0.07646101417126275, assetClass: 'EQUITY', assetSubClass: 'STOCK', @@ -1780,7 +1747,7 @@ describe('redactAttributes', () => { ], dataSource: 'YAHOO', dateOfFirstActivity: '2018-09-30T22:00:00.000Z', - dividend: 0, + dividend: null, grossPerformance: null, grossPerformancePercent: 0.8594552890963852, grossPerformancePercentWithCurrencyEffect: 0.8594552890963852, @@ -1799,6 +1766,7 @@ describe('redactAttributes', () => { valueInPercentage: 0.11824101426541227 }, bitcoin: { + activitiesCount: 1, currency: 'USD', markets: { UNKNOWN: 36985.0332704, @@ -1824,14 +1792,13 @@ describe('redactAttributes', () => { userId: null } ], - transactionCount: 1, allocationInPercentage: 0.15042891393226654, assetClass: 'LIQUIDITY', assetSubClass: 'CRYPTOCURRENCY', countries: [], dataSource: 'COINGECKO', dateOfFirstActivity: '2017-08-15T22:00:00.000Z', - dividend: 0, + dividend: null, grossPerformance: null, grossPerformancePercent: 17.4925166352, grossPerformancePercentWithCurrencyEffect: 17.4925166352, @@ -1850,6 +1817,7 @@ describe('redactAttributes', () => { valueInPercentage: 0.232626620912395 }, BONDORA_GO_AND_GROW: { + activitiesCount: 5, currency: 'EUR', markets: { UNKNOWN: 2231.644722160232, @@ -1875,14 +1843,13 @@ describe('redactAttributes', () => { userId: null } ], - transactionCount: 5, allocationInPercentage: 0.009076749759365777, assetClass: 'FIXED_INCOME', assetSubClass: 'BOND', countries: [], dataSource: 'MANUAL', dateOfFirstActivity: '2021-01-31T23:00:00.000Z', - dividend: 11.45, + dividend: null, grossPerformance: null, grossPerformancePercent: 0, grossPerformancePercentWithCurrencyEffect: -0.06153834320225245, @@ -1901,6 +1868,7 @@ describe('redactAttributes', () => { valueInPercentage: 0.014036487867880205 }, FRANKLY95P: { + activitiesCount: 6, currency: 'CHF', markets: { UNKNOWN: 0, @@ -1926,7 +1894,6 @@ describe('redactAttributes', () => { userId: null } ], - transactionCount: 6, allocationInPercentage: 0.09095764645669335, assetClass: 'EQUITY', assetSubClass: 'ETF', @@ -1986,7 +1953,7 @@ describe('redactAttributes', () => { ], dataSource: 'MANUAL', dateOfFirstActivity: '2021-03-31T22:00:00.000Z', - dividend: 0, + dividend: null, grossPerformance: null, grossPerformancePercent: 0.27579517683678895, grossPerformancePercentWithCurrencyEffect: 0.458553421589667, @@ -2005,6 +1972,7 @@ describe('redactAttributes', () => { valueInPercentage: 0.14065892911313693 }, MSFT: { + activitiesCount: 1, currency: 'USD', markets: { UNKNOWN: 0, @@ -2024,7 +1992,6 @@ describe('redactAttributes', () => { marketPrice: 428.02, symbol: 'MSFT', tags: [], - transactionCount: 1, allocationInPercentage: 0.05222646409742627, assetClass: 'EQUITY', assetSubClass: 'STOCK', @@ -2038,7 +2005,7 @@ describe('redactAttributes', () => { ], dataSource: 'YAHOO', dateOfFirstActivity: '2023-01-02T23:00:00.000Z', - dividend: 0, + dividend: null, grossPerformance: null, grossPerformancePercent: 0.7865431171216295, grossPerformancePercentWithCurrencyEffect: 0.7865431171216295, @@ -2057,6 +2024,7 @@ describe('redactAttributes', () => { valueInPercentage: 0.08076416659271518 }, TSLA: { + activitiesCount: 1, currency: 'USD', markets: { UNKNOWN: 0, @@ -2076,7 +2044,6 @@ describe('redactAttributes', () => { marketPrice: 260.46, symbol: 'TSLA', tags: [], - transactionCount: 1, allocationInPercentage: 0.1589050142378352, assetClass: 'EQUITY', assetSubClass: 'STOCK', @@ -2090,7 +2057,7 @@ describe('redactAttributes', () => { ], dataSource: 'YAHOO', dateOfFirstActivity: '2017-01-02T23:00:00.000Z', - dividend: 0, + dividend: null, grossPerformance: null, grossPerformancePercent: 17.184314638161936, grossPerformancePercentWithCurrencyEffect: 17.184314638161936, @@ -2109,6 +2076,7 @@ describe('redactAttributes', () => { valueInPercentage: 0.2457342510950259 }, VTI: { + activitiesCount: 5, currency: 'USD', markets: { UNKNOWN: 0, @@ -2128,7 +2096,6 @@ describe('redactAttributes', () => { marketPrice: 282.05, symbol: 'VTI', tags: [], - transactionCount: 5, allocationInPercentage: 0.057358979326040366, assetClass: 'EQUITY', assetSubClass: 'ETF', @@ -2172,7 +2139,7 @@ describe('redactAttributes', () => { ], dataSource: 'YAHOO', dateOfFirstActivity: '2019-02-28T23:00:00.000Z', - dividend: 0, + dividend: null, grossPerformance: null, grossPerformancePercent: 0.8832083851170418, grossPerformancePercentWithCurrencyEffect: 0.8832083851170418, @@ -2281,6 +2248,7 @@ describe('redactAttributes', () => { valueInPercentage: 0.08870120238725339 }, 'VWRL.SW': { + activitiesCount: 5, currency: 'CHF', markets: { UNKNOWN: 0, @@ -2300,7 +2268,6 @@ describe('redactAttributes', () => { marketPrice: 117.62, symbol: 'VWRL.SW', tags: [], - transactionCount: 5, allocationInPercentage: 0.09386983901959013, assetClass: 'EQUITY', assetSubClass: 'ETF', @@ -2567,7 +2534,7 @@ describe('redactAttributes', () => { ], dataSource: 'YAHOO', dateOfFirstActivity: '2018-02-28T23:00:00.000Z', - dividend: 0, + dividend: null, grossPerformance: null, grossPerformancePercent: 0.3683200415015591, grossPerformancePercentWithCurrencyEffect: 0.5806366182968891, @@ -2681,6 +2648,7 @@ describe('redactAttributes', () => { valueInPercentage: 0.145162408515095 }, 'XDWD.DE': { + activitiesCount: 1, currency: 'EUR', markets: { UNKNOWN: 0, @@ -2700,7 +2668,6 @@ describe('redactAttributes', () => { marketPrice: 105.72, symbol: 'XDWD.DE', tags: [], - transactionCount: 1, allocationInPercentage: 0.03598477442100562, assetClass: 'EQUITY', assetSubClass: 'ETF', @@ -2846,7 +2813,7 @@ describe('redactAttributes', () => { ], dataSource: 'YAHOO', dateOfFirstActivity: '2021-08-18T22:00:00.000Z', - dividend: 0, + dividend: null, grossPerformance: null, grossPerformancePercent: 0.3474381850624522, grossPerformancePercentWithCurrencyEffect: 0.28744846894552306, @@ -2959,12 +2926,13 @@ describe('redactAttributes', () => { valueInPercentage: 0.055647656152211074 }, USD: { + activitiesCount: 0, currency: 'USD', allocationInPercentage: 0.20291717628620132, assetClass: 'LIQUIDITY', assetSubClass: 'CASH', countries: [], - dividend: 0, + dividend: null, grossPerformance: null, grossPerformancePercent: 0, grossPerformancePercentWithCurrencyEffect: 0, @@ -2981,7 +2949,6 @@ describe('redactAttributes', () => { sectors: [], symbol: 'USD', tags: [], - transactionCount: 0, valueInBaseCurrency: null, valueInPercentage: 0.3137956381563603 } diff --git a/apps/api/src/helper/object.helper.ts b/apps/api/src/helper/object.helper.ts index 6bb6579d2..350d5fe04 100644 --- a/apps/api/src/helper/object.helper.ts +++ b/apps/api/src/helper/object.helper.ts @@ -1,6 +1,6 @@ -import { Big } from 'big.js'; +import fastRedact from 'fast-redact'; import jsonpath from 'jsonpath'; -import { cloneDeep, isArray, isObject } from 'lodash'; +import { cloneDeep, isObject } from 'lodash'; export function hasNotDefinedValuesInObject(aObject: Object): boolean { for (const key in aObject) { @@ -42,60 +42,29 @@ export function query({ return jsonpath.query(object, pathExpression); } -export function redactAttributes({ - isFirstRun = true, +export function redactPaths({ object, - options + paths, + valueMap }: { - isFirstRun?: boolean; object: any; - options: { attribute: string; valueMap: { [key: string]: any } }[]; + paths: fastRedact.RedactOptions['paths']; + valueMap?: { [key: string]: any }; }): any { - if (!object || !options?.length) { - return object; - } - - // Create deep clone - const redactedObject = isFirstRun - ? JSON.parse(JSON.stringify(object)) - : object; - - for (const option of options) { - if (redactedObject.hasOwnProperty(option.attribute)) { - if (option.valueMap['*'] || option.valueMap['*'] === null) { - redactedObject[option.attribute] = option.valueMap['*']; - } else if (option.valueMap[redactedObject[option.attribute]]) { - redactedObject[option.attribute] = - option.valueMap[redactedObject[option.attribute]]; - } - } else { - // If the attribute is not present on the current object, - // check if it exists on any nested objects - for (const property in redactedObject) { - if (isArray(redactedObject[property])) { - redactedObject[property] = redactedObject[property].map( - (currentObject) => { - return redactAttributes({ - options, - isFirstRun: false, - object: currentObject - }); - } - ); - } else if ( - isObject(redactedObject[property]) && - !(redactedObject[property] instanceof Big) - ) { - // Recursively call the function on the nested object - redactedObject[property] = redactAttributes({ - options, - isFirstRun: false, - object: redactedObject[property] - }); + const redact = fastRedact({ + paths, + censor: (value) => { + if (valueMap) { + if (valueMap[value]) { + return valueMap[value]; + } else { + return value; } + } else { + return null; } } - } + }); - return redactedObject; + return JSON.parse(redact(object)); } diff --git a/apps/api/src/interceptors/redact-values-in-response/redact-values-in-response.interceptor.ts b/apps/api/src/interceptors/redact-values-in-response/redact-values-in-response.interceptor.ts index 5ecf7c48d..60b994cac 100644 --- a/apps/api/src/interceptors/redact-values-in-response/redact-values-in-response.interceptor.ts +++ b/apps/api/src/interceptors/redact-values-in-response/redact-values-in-response.interceptor.ts @@ -1,5 +1,8 @@ -import { redactAttributes } from '@ghostfolio/api/helper/object.helper'; -import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config'; +import { redactPaths } from '@ghostfolio/api/helper/object.helper'; +import { + DEFAULT_REDACTED_PATHS, + HEADER_KEY_IMPERSONATION +} from '@ghostfolio/common/config'; import { hasReadRestrictedAccessPermission, isRestrictedView @@ -39,40 +42,9 @@ export class RedactValuesInResponseInterceptor implements NestInterceptor< }) || isRestrictedView(user) ) { - data = redactAttributes({ + data = redactPaths({ object: data, - options: [ - 'balance', - 'balanceInBaseCurrency', - 'comment', - 'convertedBalance', - 'dividendInBaseCurrency', - 'fee', - 'feeInBaseCurrency', - 'grossPerformance', - 'grossPerformanceWithCurrencyEffect', - 'interestInBaseCurrency', - 'investment', - 'netPerformance', - 'netPerformanceWithCurrencyEffect', - 'quantity', - 'symbolMapping', - 'totalBalanceInBaseCurrency', - 'totalDividendInBaseCurrency', - 'totalInterestInBaseCurrency', - 'totalValueInBaseCurrency', - 'unitPrice', - 'unitPriceInAssetProfileCurrency', - 'value', - 'valueInBaseCurrency' - ].map((attribute) => { - return { - attribute, - valueMap: { - '*': null - } - }; - }) + paths: DEFAULT_REDACTED_PATHS }); } diff --git a/apps/api/src/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor.ts b/apps/api/src/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor.ts index 9af256671..eaa6dd08c 100644 --- a/apps/api/src/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor.ts +++ b/apps/api/src/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor.ts @@ -1,4 +1,4 @@ -import { redactAttributes } from '@ghostfolio/api/helper/object.helper'; +import { redactPaths } from '@ghostfolio/api/helper/object.helper'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { encodeDataSource } from '@ghostfolio/common/helper'; @@ -58,13 +58,18 @@ export class TransformDataSourceInResponseInterceptor< } } - data = redactAttributes({ + data = redactPaths({ + valueMap, object: data, - options: [ - { - valueMap, - attribute: 'dataSource' - } + paths: [ + 'activities[*].SymbolProfile.dataSource', + 'benchmarks[*].dataSource', + 'fearAndGreedIndex.CRYPTOCURRENCIES.dataSource', + 'fearAndGreedIndex.STOCKS.dataSource', + 'holdings[*].dataSource', + 'items[*].dataSource', + 'SymbolProfile.dataSource', + 'watchlist[*].dataSource' ] }); } diff --git a/apps/api/src/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service.ts b/apps/api/src/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service.ts index 65bcd6c06..c83e35503 100644 --- a/apps/api/src/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service.ts +++ b/apps/api/src/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service.ts @@ -135,10 +135,10 @@ export class YahooFinanceDataEnhancerService implements DataEnhancerInterface { shortName, symbol }: { - longName: Price['longName']; - quoteType: Price['quoteType']; - shortName: Price['shortName']; - symbol: Price['symbol']; + longName?: Price['longName']; + quoteType?: Price['quoteType']; + shortName?: Price['shortName']; + symbol?: Price['symbol']; }) { let name = longName; @@ -206,17 +206,26 @@ export class YahooFinanceDataEnhancerService implements DataEnhancerInterface { ); if (['ETF', 'MUTUALFUND'].includes(assetSubClass)) { - response.sectors = []; - - for (const sectorWeighting of assetProfile.topHoldings - ?.sectorWeightings ?? []) { - for (const [sector, weight] of Object.entries(sectorWeighting)) { - response.sectors.push({ + response.holdings = + assetProfile.topHoldings?.holdings?.map( + ({ holdingName, holdingPercent }) => { + return { + name: this.formatName({ longName: holdingName }), + weight: holdingPercent + }; + } + ) ?? []; + + response.sectors = ( + assetProfile.topHoldings?.sectorWeightings ?? [] + ).flatMap((sectorWeighting) => { + return Object.entries(sectorWeighting).map(([sector, weight]) => { + return { name: this.parseSector(sector), weight: weight as number - }); - } - } + }; + }); + }); } else if ( assetSubClass === 'STOCK' && assetProfile.summaryProfile?.country diff --git a/apps/api/src/services/tag/tag.service.ts b/apps/api/src/services/tag/tag.service.ts index eb2d7bfef..f4cbd4cb1 100644 --- a/apps/api/src/services/tag/tag.service.ts +++ b/apps/api/src/services/tag/tag.service.ts @@ -75,12 +75,16 @@ export class TagService { } }); - return tags.map(({ _count, id, name, userId }) => ({ - id, - name, - userId, - isUsed: _count.activities > 0 - })); + return tags + .map(({ _count, id, name, userId }) => ({ + id, + name, + userId, + isUsed: _count.activities > 0 + })) + .sort((a, b) => { + return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); + }); } public async getTagsWithActivityCount() { diff --git a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts index b40043cc8..380fb69cb 100644 --- a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts +++ b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts @@ -80,6 +80,7 @@ import { AccountDetailDialogParams } from './interfaces/interfaces'; export class GfAccountDetailDialogComponent implements OnDestroy, OnInit { public accountBalances: AccountBalancesResponse['balances']; public activities: OrderWithAccount[]; + public activitiesCount: number; public balance: number; public balancePrecision = 2; public currency: string; @@ -100,7 +101,6 @@ export class GfAccountDetailDialogComponent implements OnDestroy, OnInit { public sortColumn = 'date'; public sortDirection: SortDirection = 'desc'; public totalItems: number; - public transactionCount: number; public user: User; public valueInBaseCurrency: number; @@ -215,16 +215,17 @@ export class GfAccountDetailDialogComponent implements OnDestroy, OnInit { .pipe(takeUntil(this.unsubscribeSubject)) .subscribe( ({ + activitiesCount, balance, currency, dividendInBaseCurrency, interestInBaseCurrency, name, platform, - transactionCount, value, valueInBaseCurrency }) => { + this.activitiesCount = activitiesCount; this.balance = balance; if ( @@ -270,7 +271,6 @@ export class GfAccountDetailDialogComponent implements OnDestroy, OnInit { this.name = name; this.platformName = platform?.name ?? '-'; - this.transactionCount = transactionCount; this.valueInBaseCurrency = valueInBaseCurrency; this.changeDetectorRef.markForCheck(); diff --git a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html index 07ea17038..15dd8f13a 100644 --- a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html +++ b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html @@ -82,7 +82,7 @@ >
- Activities
diff --git a/apps/client/src/app/components/admin-overview/admin-overview.component.ts b/apps/client/src/app/components/admin-overview/admin-overview.component.ts index 6284f05fd..c0ccb0f64 100644 --- a/apps/client/src/app/components/admin-overview/admin-overview.component.ts +++ b/apps/client/src/app/components/admin-overview/admin-overview.component.ts @@ -73,6 +73,7 @@ import { takeUntil } from 'rxjs/operators'; templateUrl: './admin-overview.html' }) export class GfAdminOverviewComponent implements OnDestroy, OnInit { + public activitiesCount: number; public couponDuration: StringValue = '14 days'; public coupons: Coupon[]; public hasPermissionForSubscription: boolean; @@ -83,7 +84,6 @@ export class GfAdminOverviewComponent implements OnDestroy, OnInit { public isDataGatheringEnabled: boolean; public permissions = permissions; public systemMessage: SystemMessage; - public transactionCount: number; public userCount: number; public user: User; public version: string; @@ -289,12 +289,12 @@ export class GfAdminOverviewComponent implements OnDestroy, OnInit { this.adminService .fetchAdminData() .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe(({ settings, transactionCount, userCount, version }) => { + .subscribe(({ activitiesCount, settings, userCount, version }) => { + this.activitiesCount = activitiesCount; this.coupons = (settings[PROPERTY_COUPONS] as Coupon[]) ?? []; this.isDataGatheringEnabled = settings[PROPERTY_IS_DATA_GATHERING_ENABLED] === false ? false : true; this.systemMessage = settings[PROPERTY_SYSTEM_MESSAGE] as SystemMessage; - this.transactionCount = transactionCount; this.userCount = userCount; this.version = version; diff --git a/apps/client/src/app/components/admin-overview/admin-overview.html b/apps/client/src/app/components/admin-overview/admin-overview.html index c47387f37..f0a6ea1d5 100644 --- a/apps/client/src/app/components/admin-overview/admin-overview.html +++ b/apps/client/src/app/components/admin-overview/admin-overview.html @@ -20,11 +20,11 @@
- @if (transactionCount && userCount) { + @if (activitiesCount && userCount) {
- {{ transactionCount / userCount | number: '1.2-2' }} + {{ activitiesCount / userCount | number: '1.2-2' }} per User
} diff --git a/apps/client/src/app/components/admin-users/admin-users.component.ts b/apps/client/src/app/components/admin-users/admin-users.component.ts index 2ae3b1a57..d479f2037 100644 --- a/apps/client/src/app/components/admin-users/admin-users.component.ts +++ b/apps/client/src/app/components/admin-users/admin-users.component.ts @@ -57,7 +57,7 @@ import { import { DeviceDetectorService } from 'ngx-device-detector'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { Subject } from 'rxjs'; -import { takeUntil } from 'rxjs/operators'; +import { switchMap, takeUntil, tap } from 'rxjs/operators'; @Component({ imports: [ @@ -139,8 +139,25 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit { ]; } - this.route.paramMap - .pipe(takeUntil(this.unsubscribeSubject)) + this.userService.stateChanged + .pipe( + takeUntil(this.unsubscribeSubject), + tap((state) => { + if (state?.user) { + this.user = state.user; + + this.defaultDateFormat = getDateFormatString( + this.user.settings.locale + ); + + this.hasPermissionToImpersonateAllUsers = hasPermission( + this.user.permissions, + permissions.impersonateAllUsers + ); + } + }), + switchMap(() => this.route.paramMap) + ) .subscribe((params) => { const userId = params.get('userId'); @@ -149,23 +166,6 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit { } }); - this.userService.stateChanged - .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe((state) => { - if (state?.user) { - this.user = state.user; - - this.defaultDateFormat = getDateFormatString( - this.user.settings.locale - ); - - this.hasPermissionToImpersonateAllUsers = hasPermission( - this.user.permissions, - permissions.impersonateAllUsers - ); - } - }); - addIcons({ contractOutline, ellipsisHorizontal, @@ -208,10 +208,13 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit { .deleteUser(aId) .pipe(takeUntil(this.unsubscribeSubject)) .subscribe(() => { - this.fetchUsers(); + this.router.navigate(['..'], { relativeTo: this.route }); }); }, confirmType: ConfirmationDialogType.Warn, + discardFn: () => { + this.router.navigate(['..'], { relativeTo: this.route }); + }, title: $localize`Do you really want to delete this user?` }); } @@ -293,6 +296,7 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit { >(GfUserDetailDialogComponent, { autoFocus: false, data: { + currentUserId: this.user?.id, deviceType: this.deviceType, hasPermissionForSubscription: this.hasPermissionForSubscription, locale: this.user?.settings?.locale, @@ -305,10 +309,14 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit { dialogRef .afterClosed() .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe(() => { - this.router.navigate( - internalRoutes.adminControl.subRoutes.users.routerLink - ); + .subscribe((data) => { + if (data?.action === 'delete' && data?.userId) { + this.onDeleteUser(data.userId); + } else { + this.router.navigate( + internalRoutes.adminControl.subRoutes.users.routerLink + ); + } }); } } diff --git a/apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts b/apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts index 7f03ea57f..2ecefc311 100644 --- a/apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts +++ b/apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts @@ -1,6 +1,5 @@ import { getTooltipOptions, - getTooltipPositionerMapTop, getVerticalHoverLinePlugin } from '@ghostfolio/common/chart-helper'; import { primaryColorRgb, secondaryColorRgb } from '@ghostfolio/common/config'; @@ -15,12 +14,14 @@ import { LineChartItem, User } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { internalRoutes } from '@ghostfolio/common/routes/routes'; import { ColorScheme } from '@ghostfolio/common/types'; +import { registerChartConfiguration } from '@ghostfolio/ui/chart'; import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator'; import { CommonModule } from '@angular/common'; import { ChangeDetectionStrategy, Component, + type ElementRef, EventEmitter, Input, OnChanges, @@ -42,7 +43,7 @@ import { PointElement, TimeScale, Tooltip, - TooltipPosition + type TooltipOptions } from 'chart.js'; import 'chartjs-adapter-date-fns'; import annotationPlugin from 'chartjs-plugin-annotation'; @@ -78,7 +79,7 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy { @Output() benchmarkChanged = new EventEmitter(); - @ViewChild('chartCanvas') chartCanvas; + @ViewChild('chartCanvas') chartCanvas: ElementRef; public chart: Chart<'line'>; public hasPermissionToAccessAdminControl: boolean; @@ -96,8 +97,7 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy { Tooltip ); - Tooltip.positioners['top'] = (_elements, position: TooltipPosition) => - getTooltipPositionerMapTop(this.chart, position); + registerChartConfiguration(); addIcons({ arrowForwardOutline }); } @@ -157,8 +157,10 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy { if (this.chartCanvas) { if (this.chart) { this.chart.data = data; + this.chart.options.plugins ??= {}; this.chart.options.plugins.tooltip = - this.getTooltipPluginConfiguration() as unknown; + this.getTooltipPluginConfiguration(); + this.chart.update(); } else { this.chart = new Chart(this.chartCanvas.nativeElement, { @@ -196,7 +198,7 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy { verticalHoverLine: { color: `rgba(${getTextColor(this.colorScheme)}, 0.1)` } - } as unknown, + }, responsive: true, scales: { x: { @@ -253,7 +255,7 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy { } } - private getTooltipPluginConfiguration() { + private getTooltipPluginConfiguration(): Partial> { return { ...getTooltipOptions({ colorScheme: this.colorScheme, @@ -261,7 +263,7 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy { unit: '%' }), mode: 'index', - position: 'top' as unknown, + position: 'top', xAlign: 'center', yAlign: 'bottom' }; diff --git a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html index f9329dbfb..27df91a17 100644 --- a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html +++ b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html @@ -380,10 +380,10 @@ [deviceType]="data.deviceType" [hasPermissionToOpenDetails]="false" [locale]="user?.settings?.locale" + [showActivitiesCount]="false" [showAllocationInPercentage]="user?.settings?.isExperimentalFeatures" [showBalance]="false" [showFooter]="false" - [showTransactions]="false" [showValue]="false" [showValueInBaseCurrency]="false" /> diff --git a/apps/client/src/app/components/investment-chart/investment-chart.component.ts b/apps/client/src/app/components/investment-chart/investment-chart.component.ts index 5492ddd4c..53d4f5693 100644 --- a/apps/client/src/app/components/investment-chart/investment-chart.component.ts +++ b/apps/client/src/app/components/investment-chart/investment-chart.component.ts @@ -1,6 +1,5 @@ import { getTooltipOptions, - getTooltipPositionerMapTop, getVerticalHoverLinePlugin, transformTickToAbbreviation } from '@ghostfolio/common/chart-helper'; @@ -15,11 +14,13 @@ import { import { LineChartItem } from '@ghostfolio/common/interfaces'; import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface'; import { ColorScheme, GroupBy } from '@ghostfolio/common/types'; +import { registerChartConfiguration } from '@ghostfolio/ui/chart'; import { CommonModule } from '@angular/common'; import { ChangeDetectionStrategy, Component, + type ElementRef, Input, OnChanges, OnDestroy, @@ -34,12 +35,15 @@ import { LineController, LineElement, PointElement, + type ScriptableLineSegmentContext, TimeScale, Tooltip, - TooltipPosition + type TooltipOptions } from 'chart.js'; import 'chartjs-adapter-date-fns'; -import annotationPlugin from 'chartjs-plugin-annotation'; +import annotationPlugin, { + type AnnotationOptions +} from 'chartjs-plugin-annotation'; import { isAfter } from 'date-fns'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; @@ -62,7 +66,7 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy { @Input() locale = getLocale(); @Input() savingsRate = 0; - @ViewChild('chartCanvas') chartCanvas; + @ViewChild('chartCanvas') chartCanvas: ElementRef; public chart: Chart<'bar' | 'line'>; private investments: InvestmentItem[]; @@ -81,8 +85,7 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy { Tooltip ); - Tooltip.positioners['top'] = (_elements, position: TooltipPosition) => - getTooltipPositionerMapTop(this.chart, position); + registerChartConfiguration(); } public ngOnChanges() { @@ -121,12 +124,12 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy { }), label: this.benchmarkDataLabel, segment: { - borderColor: (context: unknown) => + borderColor: (context) => this.isInFuture( context, `rgba(${secondaryColorRgb.r}, ${secondaryColorRgb.g}, ${secondaryColorRgb.b}, 0.67)` ), - borderDash: (context: unknown) => this.isInFuture(context, [2, 2]) + borderDash: (context) => this.isInFuture(context, [2, 2]) }, stepped: true }, @@ -143,12 +146,12 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy { label: $localize`Total Amount`, pointRadius: 0, segment: { - borderColor: (context: unknown) => + borderColor: (context) => this.isInFuture( context, `rgba(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b}, 0.67)` ), - borderDash: (context: unknown) => this.isInFuture(context, [2, 2]) + borderDash: (context) => this.isInFuture(context, [2, 2]) } } ] @@ -157,17 +160,14 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy { if (this.chartCanvas) { if (this.chart) { this.chart.data = chartData; + this.chart.options.plugins ??= {}; this.chart.options.plugins.tooltip = - this.getTooltipPluginConfiguration() as unknown; + this.getTooltipPluginConfiguration(); - if ( - this.savingsRate && - // @ts-ignore - this.chart.options.plugins.annotation.annotations.savingsRate - ) { - // @ts-ignore - this.chart.options.plugins.annotation.annotations.savingsRate.value = - this.savingsRate; + const annotations = this.chart.options.plugins.annotation + .annotations as Record>; + if (this.savingsRate && annotations.savingsRate) { + annotations.savingsRate.value = this.savingsRate; } this.chart.update(); @@ -201,7 +201,7 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy { color: 'white', content: $localize`Savings Rate`, display: true, - font: { size: '10px', weight: 'normal' }, + font: { size: 10, weight: 'normal' }, padding: { x: 4, y: 2 @@ -229,7 +229,7 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy { verticalHoverLine: { color: `rgba(${getTextColor(this.colorScheme)}, 0.1)` } - } as unknown, + }, responsive: true, scales: { x: { @@ -286,7 +286,9 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy { } } - private getTooltipPluginConfiguration() { + private getTooltipPluginConfiguration(): Partial< + TooltipOptions<'bar' | 'line'> + > { return { ...getTooltipOptions({ colorScheme: this.colorScheme, @@ -296,13 +298,13 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy { unit: this.isInPercent ? '%' : undefined }), mode: 'index', - position: 'top' as unknown, + position: 'top', xAlign: 'center', yAlign: 'bottom' }; } - private isInFuture(aContext: any, aValue: T) { + private isInFuture(aContext: ScriptableLineSegmentContext, aValue: T) { return isAfter(new Date(aContext?.p1?.parsed?.x), new Date()) ? aValue : undefined; diff --git a/apps/client/src/app/components/user-detail-dialog/interfaces/interfaces.ts b/apps/client/src/app/components/user-detail-dialog/interfaces/interfaces.ts index b922e7a54..ed46e8a02 100644 --- a/apps/client/src/app/components/user-detail-dialog/interfaces/interfaces.ts +++ b/apps/client/src/app/components/user-detail-dialog/interfaces/interfaces.ts @@ -1,4 +1,5 @@ export interface UserDetailDialogParams { + currentUserId: string; deviceType: string; hasPermissionForSubscription: boolean; locale: string; diff --git a/apps/client/src/app/components/user-detail-dialog/user-detail-dialog.component.ts b/apps/client/src/app/components/user-detail-dialog/user-detail-dialog.component.ts index cdf977058..6f7f4ead6 100644 --- a/apps/client/src/app/components/user-detail-dialog/user-detail-dialog.component.ts +++ b/apps/client/src/app/components/user-detail-dialog/user-detail-dialog.component.ts @@ -1,6 +1,4 @@ import { AdminUserResponse } from '@ghostfolio/common/interfaces'; -import { GfDialogFooterComponent } from '@ghostfolio/ui/dialog-footer'; -import { GfDialogHeaderComponent } from '@ghostfolio/ui/dialog-header'; import { AdminService } from '@ghostfolio/ui/services'; import { GfValueComponent } from '@ghostfolio/ui/value'; @@ -16,6 +14,10 @@ import { import { MatButtonModule } from '@angular/material/button'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MatDialogModule } from '@angular/material/dialog'; +import { MatMenuModule } from '@angular/material/menu'; +import { IonIcon } from '@ionic/angular/standalone'; +import { addIcons } from 'ionicons'; +import { ellipsisVertical } from 'ionicons/icons'; import { EMPTY, Subject } from 'rxjs'; import { catchError, takeUntil } from 'rxjs/operators'; @@ -25,11 +27,11 @@ import { UserDetailDialogParams } from './interfaces/interfaces'; changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'd-flex flex-column h-100' }, imports: [ - GfDialogFooterComponent, - GfDialogHeaderComponent, GfValueComponent, + IonIcon, MatButtonModule, - MatDialogModule + MatDialogModule, + MatMenuModule ], schemas: [CUSTOM_ELEMENTS_SCHEMA], selector: 'gf-user-detail-dialog', @@ -46,7 +48,11 @@ export class GfUserDetailDialogComponent implements OnDestroy, OnInit { private changeDetectorRef: ChangeDetectorRef, @Inject(MAT_DIALOG_DATA) public data: UserDetailDialogParams, public dialogRef: MatDialogRef - ) {} + ) { + addIcons({ + ellipsisVertical + }); + } public ngOnInit() { this.adminService @@ -66,6 +72,13 @@ export class GfUserDetailDialogComponent implements OnDestroy, OnInit { }); } + public deleteUser() { + this.dialogRef.close({ + action: 'delete', + userId: this.data.userId + }); + } + public onClose() { this.dialogRef.close(); } diff --git a/apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html b/apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html index 60f6a2585..570dcf4d6 100644 --- a/apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html +++ b/apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -1,9 +1,28 @@ - - +
+ + + + +
@@ -103,7 +122,8 @@
- +
+ +
diff --git a/apps/client/src/app/pages/accounts/accounts-page.component.ts b/apps/client/src/app/pages/accounts/accounts-page.component.ts index 6c8146f77..f7e6541b5 100644 --- a/apps/client/src/app/pages/accounts/accounts-page.component.ts +++ b/apps/client/src/app/pages/accounts/accounts-page.component.ts @@ -38,6 +38,7 @@ import { GfTransferBalanceDialogComponent } from './transfer-balance/transfer-ba }) export class GfAccountsPageComponent implements OnDestroy, OnInit { public accounts: AccountModel[]; + public activitiesCount = 0; public deviceType: string; public hasImpersonationId: boolean; public hasPermissionToCreateAccount: boolean; @@ -45,7 +46,6 @@ export class GfAccountsPageComponent implements OnDestroy, OnInit { public routeQueryParams: Subscription; public totalBalanceInBaseCurrency = 0; public totalValueInBaseCurrency = 0; - public transactionCount = 0; public user: User; private unsubscribeSubject = new Subject(); @@ -128,14 +128,14 @@ export class GfAccountsPageComponent implements OnDestroy, OnInit { .subscribe( ({ accounts, + activitiesCount, totalBalanceInBaseCurrency, - totalValueInBaseCurrency, - transactionCount + totalValueInBaseCurrency }) => { this.accounts = accounts; + this.activitiesCount = activitiesCount; this.totalBalanceInBaseCurrency = totalBalanceInBaseCurrency; this.totalValueInBaseCurrency = totalValueInBaseCurrency; - this.transactionCount = transactionCount; if (this.accounts?.length <= 0) { this.router.navigate([], { queryParams: { createDialog: true } }); @@ -358,8 +358,8 @@ export class GfAccountsPageComponent implements OnDestroy, OnInit { private reset() { this.accounts = undefined; + this.activitiesCount = 0; this.totalBalanceInBaseCurrency = 0; this.totalValueInBaseCurrency = 0; - this.transactionCount = 0; } } diff --git a/apps/client/src/app/pages/accounts/accounts-page.html b/apps/client/src/app/pages/accounts/accounts-page.html index 6f29a4f7c..0c6b7b8f3 100644 --- a/apps/client/src/app/pages/accounts/accounts-page.html +++ b/apps/client/src/app/pages/accounts/accounts-page.html @@ -4,6 +4,7 @@

Accounts

({ colorScheme, currency = '', groupBy, @@ -45,35 +48,43 @@ export function getTooltipOptions({ groupBy?: GroupBy; locale?: string; unit?: string; -}) { +}): Partial> { return { backgroundColor: getBackgroundColor(colorScheme), bodyColor: `rgb(${getTextColor(colorScheme)})`, borderWidth: 1, borderColor: `rgba(${getTextColor(colorScheme)}, 0.1)`, + // @ts-expect-error: no need to set all attributes in callbacks callbacks: { label: (context) => { - let label = context.dataset.label ?? ''; + let label = (context.dataset as ControllerDatasetOptions).label ?? ''; + if (label) { label += ': '; } - if (context.parsed.y !== null) { + + const yPoint = (context.parsed as Point).y; + + if (yPoint !== null) { if (currency) { - label += `${context.parsed.y.toLocaleString(locale, { + label += `${yPoint.toLocaleString(locale, { maximumFractionDigits: 2, minimumFractionDigits: 2 })} ${currency}`; } else if (unit) { - label += `${context.parsed.y.toFixed(2)} ${unit}`; + label += `${yPoint.toFixed(2)} ${unit}`; } else { - label += context.parsed.y.toFixed(2); + label += yPoint.toFixed(2); } } + return label; }, title: (contexts) => { - if (groupBy) { - return formatGroupedDate({ groupBy, date: contexts[0].parsed.x }); + const xPoint = (contexts[0].parsed as Point).x; + + if (groupBy && xPoint !== null) { + return formatGroupedDate({ groupBy, date: xPoint }); } return contexts[0].label; @@ -98,16 +109,17 @@ export function getTooltipPositionerMapTop( if (!position || !chart?.chartArea) { return false; } + return { x: position.x, y: chart.chartArea.top }; } -export function getVerticalHoverLinePlugin( - chartCanvas: ElementRef, +export function getVerticalHoverLinePlugin( + chartCanvas: ElementRef, colorScheme: ColorScheme -): Plugin { +): Plugin { return { afterDatasetsDraw: (chart, _, options) => { const active = chart.getActiveElements(); @@ -125,13 +137,16 @@ export function getVerticalHoverLinePlugin( const xValue = active[0].element.x; const context = chartCanvas.nativeElement.getContext('2d'); - context.lineWidth = width; - context.strokeStyle = color; - context.beginPath(); - context.moveTo(xValue, top); - context.lineTo(xValue, bottom); - context.stroke(); + if (context) { + context.lineWidth = width; + context.strokeStyle = color; + + context.beginPath(); + context.moveTo(xValue, top); + context.lineTo(xValue, bottom); + context.stroke(); + } }, id: 'verticalHoverLine' }; diff --git a/libs/common/src/lib/config.ts b/libs/common/src/lib/config.ts index a10a828e1..b558ccc42 100644 --- a/libs/common/src/lib/config.ts +++ b/libs/common/src/lib/config.ts @@ -78,6 +78,58 @@ export const DEFAULT_PROCESSOR_GATHER_HISTORICAL_MARKET_DATA_CONCURRENCY = 1; export const DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_CONCURRENCY = 1; export const DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_TIMEOUT = 30000; +export const DEFAULT_REDACTED_PATHS = [ + 'accounts[*].balance', + 'accounts[*].valueInBaseCurrency', + 'activities[*].account.balance', + 'activities[*].account.comment', + 'activities[*].comment', + 'activities[*].fee', + 'activities[*].feeInAssetProfileCurrency', + 'activities[*].feeInBaseCurrency', + 'activities[*].quantity', + 'activities[*].SymbolProfile.symbolMapping', + 'activities[*].SymbolProfile.watchedByCount', + 'activities[*].value', + 'activities[*].valueInBaseCurrency', + 'balance', + 'balanceInBaseCurrency', + 'balances[*].account.balance', + 'balances[*].account.comment', + 'balances[*].value', + 'balances[*].valueInBaseCurrency', + 'comment', + 'dividendInBaseCurrency', + 'feeInBaseCurrency', + 'grossPerformance', + 'grossPerformanceWithCurrencyEffect', + 'historicalData[*].quantity', + 'holdings[*].dividend', + 'holdings[*].grossPerformance', + 'holdings[*].grossPerformanceWithCurrencyEffect', + 'holdings[*].holdings[*].valueInBaseCurrency', + 'holdings[*].investment', + 'holdings[*].netPerformance', + 'holdings[*].netPerformanceWithCurrencyEffect', + 'holdings[*].quantity', + 'holdings[*].valueInBaseCurrency', + 'interestInBaseCurrency', + 'investmentInBaseCurrencyWithCurrencyEffect', + 'netPerformance', + 'netPerformanceWithCurrencyEffect', + 'platforms[*].balance', + 'platforms[*].valueInBaseCurrency', + 'quantity', + 'SymbolProfile.symbolMapping', + 'SymbolProfile.watchedByCount', + 'totalBalanceInBaseCurrency', + 'totalDividendInBaseCurrency', + 'totalInterestInBaseCurrency', + 'totalValueInBaseCurrency', + 'value', + 'valueInBaseCurrency' +]; + // USX is handled separately export const DERIVED_CURRENCIES = [ { diff --git a/libs/common/src/lib/interfaces/admin-data.interface.ts b/libs/common/src/lib/interfaces/admin-data.interface.ts index 23821a86b..dd25b516d 100644 --- a/libs/common/src/lib/interfaces/admin-data.interface.ts +++ b/libs/common/src/lib/interfaces/admin-data.interface.ts @@ -1,12 +1,12 @@ import { DataProviderInfo } from './data-provider-info.interface'; export interface AdminData { + activitiesCount: number; dataProviders: (DataProviderInfo & { assetProfileCount: number; useForExchangeRates: boolean; })[]; settings: { [key: string]: boolean | object | string | string[] }; - transactionCount: number; userCount: number; version: string; } diff --git a/libs/common/src/lib/interfaces/portfolio-position.interface.ts b/libs/common/src/lib/interfaces/portfolio-position.interface.ts index 67a2f3e77..620cc00e9 100644 --- a/libs/common/src/lib/interfaces/portfolio-position.interface.ts +++ b/libs/common/src/lib/interfaces/portfolio-position.interface.ts @@ -39,10 +39,6 @@ export interface PortfolioPosition { sectors: Sector[]; symbol: string; tags?: Tag[]; - - /** @deprecated use activitiesCount instead */ - transactionCount: number; - type?: string; url?: string; valueInBaseCurrency?: number; diff --git a/libs/common/src/lib/interfaces/responses/accounts-response.interface.ts b/libs/common/src/lib/interfaces/responses/accounts-response.interface.ts index 1891b9cbb..90f1303e0 100644 --- a/libs/common/src/lib/interfaces/responses/accounts-response.interface.ts +++ b/libs/common/src/lib/interfaces/responses/accounts-response.interface.ts @@ -7,7 +7,4 @@ export interface AccountsResponse { totalDividendInBaseCurrency: number; totalInterestInBaseCurrency: number; totalValueInBaseCurrency: number; - - /** @deprecated use activitiesCount instead */ - transactionCount: number; } diff --git a/libs/common/src/lib/interfaces/responses/create-stripe-checkout-session-response.interface.ts b/libs/common/src/lib/interfaces/responses/create-stripe-checkout-session-response.interface.ts index 1222ac6e9..8ac1a8279 100644 --- a/libs/common/src/lib/interfaces/responses/create-stripe-checkout-session-response.interface.ts +++ b/libs/common/src/lib/interfaces/responses/create-stripe-checkout-session-response.interface.ts @@ -1,6 +1,3 @@ export interface CreateStripeCheckoutSessionResponse { - /** @deprecated */ - sessionId: string; - sessionUrl: string; } diff --git a/libs/common/src/lib/models/timeline-position.ts b/libs/common/src/lib/models/timeline-position.ts index 244d6595e..13f9001d5 100644 --- a/libs/common/src/lib/models/timeline-position.ts +++ b/libs/common/src/lib/models/timeline-position.ts @@ -35,9 +35,6 @@ export class TimelinePosition { @Type(() => Big) feeInBaseCurrency: Big; - /** @deprecated use dateOfFirstActivity instead */ - firstBuyDate: string; - @Transform(transformToBig, { toClassOnly: true }) @Type(() => Big) grossPerformance: Big; @@ -96,9 +93,6 @@ export class TimelinePosition { @Type(() => Big) timeWeightedInvestmentWithCurrencyEffect: Big; - /** @deprecated use activitiesCount instead */ - transactionCount: number; - @Transform(transformToBig, { toClassOnly: true }) @Type(() => Big) valueInBaseCurrency: Big; diff --git a/libs/common/src/lib/types/account-with-value.type.ts b/libs/common/src/lib/types/account-with-value.type.ts index 7f5fe79ba..23cb14749 100644 --- a/libs/common/src/lib/types/account-with-value.type.ts +++ b/libs/common/src/lib/types/account-with-value.type.ts @@ -7,10 +7,6 @@ export type AccountWithValue = AccountModel & { dividendInBaseCurrency: number; interestInBaseCurrency: number; platform?: Platform; - - /** @deprecated use activitiesCount instead */ - transactionCount: number; - value: number; valueInBaseCurrency: number; }; diff --git a/libs/ui/src/lib/accounts-table/accounts-table.component.html b/libs/ui/src/lib/accounts-table/accounts-table.component.html index f76a5d676..68ae78474 100644 --- a/libs/ui/src/lib/accounts-table/accounts-table.component.html +++ b/libs/ui/src/lib/accounts-table/accounts-table.component.html @@ -115,21 +115,21 @@ > - + # Activities - {{ element.transactionCount }} + {{ element.activitiesCount }} - {{ transactionCount }} + {{ activitiesCount }} @@ -323,7 +323,7 @@