From 418e8bc843f1aa9e1584d122371f62e527de8708 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 31 Jan 2026 08:32:33 +0100 Subject: [PATCH 01/22] Task/remove deprecated first buy date in portfolio calculator (#6244) * Remove deprecated firstBuyDate * Update changelog --- CHANGELOG.md | 6 +++++ .../calculator/portfolio-calculator.ts | 3 --- ...tfolio-calculator-baln-buy-and-buy.spec.ts | 1 - ...aln-buy-and-sell-in-two-activities.spec.ts | 1 - ...folio-calculator-baln-buy-and-sell.spec.ts | 1 - .../portfolio-calculator-baln-buy.spec.ts | 1 - .../roai/portfolio-calculator-btceur.spec.ts | 1 - ...ator-btcusd-buy-and-sell-partially.spec.ts | 1 - .../roai/portfolio-calculator-btcusd.spec.ts | 1 - .../roai/portfolio-calculator-cash.spec.ts | 1 - .../portfolio-calculator-googl-buy.spec.ts | 1 - ...-calculator-msft-buy-with-dividend.spec.ts | 1 - ...ulator-novn-buy-and-sell-partially.spec.ts | 1 - ...folio-calculator-novn-buy-and-sell.spec.ts | 1 - .../portfolio-calculator-valuable.spec.ts | 1 - .../transaction-point-symbol.interface.ts | 4 ---- .../src/app/portfolio/portfolio.service.ts | 22 ++++++++++++------- .../src/lib/models/timeline-position.ts | 3 --- 18 files changed, 20 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f65bbd458..d1092b049 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Changed + +- Removed the deprecated `firstBuyDate` in the portfolio calculator + ## 2.234.0 - 2026-01-30 ### Changed diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index b3b1d3410..9612ad1c4 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) @@ -1004,7 +1003,6 @@ 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), @@ -1024,7 +1022,6 @@ 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), 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..7858d7546 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( 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..8b40c7b70 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( 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..fc372f68f 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( 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..926fae6dc 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( 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..b216438b8 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( 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..14cd4f217 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( 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..066f33ea3 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( 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..bd8afddd7 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( 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..28b44e159 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( 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..87ef9ed8b 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( 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..7a8dc010a 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( 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..02a4e80d8 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( 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..610a52c06 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'), 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..7e7d741ea 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,10 +11,6 @@ export interface TransactionPointSymbol { dividend: Big; fee: Big; feeInBaseCurrency: Big; - - /** @deprecated use dateOfFirstActivity instead */ - firstBuyDate: string; - includeInHoldings: boolean; investment: Big; quantity: Big; diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 7db743a43..05df6a8fc 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -576,8 +576,8 @@ export class PortfolioService { for (const { activitiesCount, currency, + dateOfFirstActivity, dividend, - firstBuyDate, grossPerformance, grossPerformanceWithCurrencyEffect, grossPerformancePercentage, @@ -633,7 +633,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 +801,9 @@ export class PortfolioService { activitiesCount, averagePrice, currency, + dateOfFirstActivity, dividendInBaseCurrency, feeInBaseCurrency, - firstBuyDate, grossPerformance, grossPerformancePercentage, grossPerformancePercentageWithCurrencyEffect, @@ -828,7 +828,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 +839,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 +851,7 @@ export class PortfolioService { const historicalData = await this.dataProviderService.getHistorical( [{ dataSource, symbol }], 'day', - parseISO(firstBuyDate), + parseISO(dateOfFirstActivity), new Date() ); @@ -910,7 +916,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 +930,7 @@ export class PortfolioService { return { activitiesCount, + dateOfFirstActivity, marketPrice, marketPriceMax, marketPriceMin, @@ -931,7 +938,6 @@ export class PortfolioService { tags, averagePrice: averagePrice.toNumber(), dataProviderInfo: portfolioCalculator.getDataProviderInfos()?.[0], - dateOfFirstActivity: firstBuyDate, dividendInBaseCurrency: dividendInBaseCurrency.toNumber(), dividendYieldPercent: dividendYieldPercent.toNumber(), dividendYieldPercentWithCurrencyEffect: diff --git a/libs/common/src/lib/models/timeline-position.ts b/libs/common/src/lib/models/timeline-position.ts index 244d6595e..9cfb2df04 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; From 7dbacdbf0fa835f0093bd70907dbdc75b7132522 Mon Sep 17 00:00:00 2001 From: Kenrick Tandrian <60643640+KenTandrian@users.noreply.github.com> Date: Sat, 31 Jan 2026 14:43:04 +0700 Subject: [PATCH 02/22] Feature/extract top holdings from Yahoo Finance for ETF and mutual funds (#6254) * Extract top holdings from Yahoo Finance for ETF and mutual funds * Update changelog --- CHANGELOG.md | 4 ++++ .../yahoo-finance/yahoo-finance.service.ts | 17 +++++++++++++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d1092b049..a3fb00a66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Added the ability to fetch top holdings for ETF and mutual fund assets from _Yahoo Finance_ + ### Changed - Removed the deprecated `firstBuyDate` in the portfolio calculator 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..97c875360 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; @@ -217,6 +217,15 @@ export class YahooFinanceDataEnhancerService implements DataEnhancerInterface { }); } } + + response.holdings = assetProfile.topHoldings.holdings.map( + ({ holdingName, holdingPercent }) => { + return { + name: this.formatName({ longName: holdingName }), + weight: holdingPercent + }; + } + ); } else if ( assetSubClass === 'STOCK' && assetProfile.summaryProfile?.country From eae2c20dfefb48c11e3affc15e8119f7b4534c98 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 31 Jan 2026 11:48:51 +0100 Subject: [PATCH 03/22] Task/deprecate transactionCount in GET api/v1/admin endpoint (#6265) * Deprecate transactionCount in favor of activitiesCount * Update changelog --- CHANGELOG.md | 1 + apps/api/src/app/admin/admin.service.ts | 7 ++++--- .../components/admin-overview/admin-overview.component.ts | 6 +++--- .../src/app/components/admin-overview/admin-overview.html | 6 +++--- libs/common/src/lib/interfaces/admin-data.interface.ts | 4 ++++ 5 files changed, 15 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a3fb00a66..39634dfc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Deprecated `transactionCount` in favor of `activitiesCount` in the endpoint `GET api/v1/admin` - Removed the deprecated `firstBuyDate` in the portfolio calculator ## 2.234.0 - 2026-01-30 diff --git a/apps/api/src/app/admin/admin.service.ts b/apps/api/src/app/admin/admin.service.ts index 705085a48..cd18eb239 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,10 +182,11 @@ export class AdminService { ).filter(Boolean); return { + activitiesCount, dataProviders, settings, - transactionCount, userCount, + transactionCount: activitiesCount, version: environment.version }; } 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/libs/common/src/lib/interfaces/admin-data.interface.ts b/libs/common/src/lib/interfaces/admin-data.interface.ts index 23821a86b..63588300c 100644 --- a/libs/common/src/lib/interfaces/admin-data.interface.ts +++ b/libs/common/src/lib/interfaces/admin-data.interface.ts @@ -1,12 +1,16 @@ 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[] }; + + /** @deprecated use activitiesCount instead */ transactionCount: number; + userCount: number; version: string; } From 2520d0d9619672b0125de4c49cf1b1ba4400bfd1 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 2 Feb 2026 20:44:04 +0100 Subject: [PATCH 04/22] Task/upgrade yahoo-finance2 to version 3.13.0 (#6263) * Upgrade yahoo-finance2 to version 3.13.0 * Update changelog --- CHANGELOG.md | 1 + package-lock.json | 8 ++++---- package.json | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39634dfc4..371a8e6f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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 diff --git a/package-lock.json b/package-lock.json index b8d343edd..38789fd53 100644 --- a/package-lock.json +++ b/package-lock.json @@ -88,7 +88,7 @@ "svgmap": "2.14.0", "tablemark": "4.1.0", "twitter-api-v2": "1.27.0", - "yahoo-finance2": "3.11.2", + "yahoo-finance2": "3.13.0", "zone.js": "0.16.0" }, "devDependencies": { @@ -35361,9 +35361,9 @@ } }, "node_modules/yahoo-finance2": { - "version": "3.11.2", - "resolved": "https://registry.npmjs.org/yahoo-finance2/-/yahoo-finance2-3.11.2.tgz", - "integrity": "sha512-SIvMXjrOktBRD8m+qXAGCK+vR1vwBKuMgCnvmbxv29+t6LTDu0vAUxNYfbigsMRTmBzS4F9TQwbYF90g3Om4HA==", + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/yahoo-finance2/-/yahoo-finance2-3.13.0.tgz", + "integrity": "sha512-czBj2q/MD68YEsB7aXNnGhJvWxYZn01O5r/i7VYiQV2m2sWwhca6tKgjwf/LT7zHHEVxhKNiGLB46glLnmq9Ag==", "license": "MIT", "dependencies": { "@deno/shim-deno": "~0.18.0", diff --git a/package.json b/package.json index 44df5228a..9dc3a9914 100644 --- a/package.json +++ b/package.json @@ -132,7 +132,7 @@ "svgmap": "2.14.0", "tablemark": "4.1.0", "twitter-api-v2": "1.27.0", - "yahoo-finance2": "3.11.2", + "yahoo-finance2": "3.13.0", "zone.js": "0.16.0" }, "devDependencies": { From 9c4638c185d434ef765f2257f2b2c482dc8dd56a Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Tue, 3 Feb 2026 19:30:37 +0100 Subject: [PATCH 05/22] Feature/add impersonation mode in get account balances endpoint (#6272) * Add impersonation mode * Update changelog --- CHANGELOG.md | 1 + apps/api/src/app/account/account.controller.ts | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 371a8e6f4..f3bdf396b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### 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` ### 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 }); } From 5b6d6a419fa308a83b218e9c7cf6ec7526bcb8d9 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Tue, 3 Feb 2026 19:37:51 +0100 Subject: [PATCH 06/22] Task/refactor transactions in accounts table component (#6266) * Refactor showTransactions and transactionCount * Update changelog --- CHANGELOG.md | 2 ++ .../holding-detail-dialog.html | 2 +- .../app/pages/accounts/accounts-page.component.ts | 10 +++++----- .../src/app/pages/accounts/accounts-page.html | 2 +- .../accounts-table/accounts-table.component.html | 4 ++-- .../accounts-table.component.stories.ts | 14 +++++++------- .../lib/accounts-table/accounts-table.component.ts | 8 ++++---- 7 files changed, 22 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f3bdf396b..df8851c66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- 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` 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/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

- + - {{ transactionCount }} + {{ activitiesCount }} diff --git a/libs/ui/src/lib/accounts-table/accounts-table.component.stories.ts b/libs/ui/src/lib/accounts-table/accounts-table.component.stories.ts index 53c59a95f..96da4419d 100644 --- a/libs/ui/src/lib/accounts-table/accounts-table.component.stories.ts +++ b/libs/ui/src/lib/accounts-table/accounts-table.component.stories.ts @@ -115,10 +115,10 @@ export const Loading: Story = { hasPermissionToOpenDetails: false, locale: 'en-US', showActions: false, + showActivitiesCount: true, showAllocationInPercentage: false, showBalance: true, showFooter: true, - showTransactions: true, showValue: true, showValueInBaseCurrency: true } @@ -127,39 +127,39 @@ export const Loading: Story = { export const Default: Story = { args: { accounts, + activitiesCount: 12, baseCurrency: 'USD', deviceType: 'desktop', hasPermissionToOpenDetails: false, locale: 'en-US', showActions: false, + showActivitiesCount: true, showAllocationInPercentage: false, showBalance: true, showFooter: true, - showTransactions: true, showValue: true, showValueInBaseCurrency: true, totalBalanceInBaseCurrency: 12428.2, - totalValueInBaseCurrency: 107971.70321466809, - transactionCount: 12 + totalValueInBaseCurrency: 107971.70321466809 } }; export const WithoutFooter: Story = { args: { accounts, + activitiesCount: 12, baseCurrency: 'USD', deviceType: 'desktop', hasPermissionToOpenDetails: false, locale: 'en-US', showActions: false, + showActivitiesCount: true, showAllocationInPercentage: false, showBalance: true, showFooter: false, - showTransactions: true, showValue: true, showValueInBaseCurrency: true, totalBalanceInBaseCurrency: 12428.2, - totalValueInBaseCurrency: 107971.70321466809, - transactionCount: 12 + totalValueInBaseCurrency: 107971.70321466809 } }; diff --git a/libs/ui/src/lib/accounts-table/accounts-table.component.ts b/libs/ui/src/lib/accounts-table/accounts-table.component.ts index fe91e1eda..21300fdcc 100644 --- a/libs/ui/src/lib/accounts-table/accounts-table.component.ts +++ b/libs/ui/src/lib/accounts-table/accounts-table.component.ts @@ -55,20 +55,20 @@ import { Subject, Subscription } from 'rxjs'; }) export class GfAccountsTableComponent implements OnChanges, OnDestroy { @Input() accounts: Account[]; + @Input() activitiesCount: number; @Input() baseCurrency: string; @Input() deviceType: string; @Input() hasPermissionToOpenDetails = true; @Input() locale = getLocale(); @Input() showActions: boolean; + @Input() showActivitiesCount = true; @Input() showAllocationInPercentage: boolean; @Input() showBalance = true; @Input() showFooter = true; - @Input() showTransactions = true; @Input() showValue = true; @Input() showValueInBaseCurrency = true; @Input() totalBalanceInBaseCurrency: number; @Input() totalValueInBaseCurrency: number; - @Input() transactionCount: number; @Output() accountDeleted = new EventEmitter(); @Output() accountToUpdate = new EventEmitter(); @@ -101,8 +101,8 @@ export class GfAccountsTableComponent implements OnChanges, OnDestroy { public ngOnChanges() { this.displayedColumns = ['status', 'account', 'platform']; - if (this.showTransactions) { - this.displayedColumns.push('transactions'); + if (this.showActivitiesCount) { + this.displayedColumns.push('activitiesCount'); } if (this.showBalance) { From 0034ed0adf9f31b1b830827852ba31a431483374 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Tue, 3 Feb 2026 19:55:18 +0100 Subject: [PATCH 07/22] Feature/introduce fast-redact in value redaction interceptor (#6269) * Introduce fast-redact * Update changelog --------- Co-authored-by: Valentin Zickner --- CHANGELOG.md | 1 + .../src/app/portfolio/portfolio.controller.ts | 2 + apps/api/src/helper/object.helper.spec.ts | 91 ++++++------------- apps/api/src/helper/object.helper.ts | 69 ++++---------- .../redact-values-in-response.interceptor.ts | 42 ++------- ...orm-data-source-in-response.interceptor.ts | 19 ++-- libs/common/src/lib/config.ts | 52 +++++++++++ package-lock.json | 18 ++++ package.json | 2 + 9 files changed, 142 insertions(+), 154 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index df8851c66..0a565308b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### 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` 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/helper/object.helper.spec.ts b/apps/api/src/helper/object.helper.spec.ts index e1ec81b8f..5ddff164b 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': { @@ -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: { @@ -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, @@ -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, @@ -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, @@ -1831,7 +1798,7 @@ describe('redactAttributes', () => { countries: [], dataSource: 'COINGECKO', dateOfFirstActivity: '2017-08-15T22:00:00.000Z', - dividend: 0, + dividend: null, grossPerformance: null, grossPerformancePercent: 17.4925166352, grossPerformancePercentWithCurrencyEffect: 17.4925166352, @@ -1882,7 +1849,7 @@ describe('redactAttributes', () => { countries: [], dataSource: 'MANUAL', dateOfFirstActivity: '2021-01-31T23:00:00.000Z', - dividend: 11.45, + dividend: null, grossPerformance: null, grossPerformancePercent: 0, grossPerformancePercentWithCurrencyEffect: -0.06153834320225245, @@ -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, @@ -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, @@ -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, @@ -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, @@ -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, @@ -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, @@ -2964,7 +2931,7 @@ describe('redactAttributes', () => { assetClass: 'LIQUIDITY', assetSubClass: 'CASH', countries: [], - dividend: 0, + dividend: null, grossPerformance: null, grossPerformancePercent: 0, grossPerformancePercentWithCurrencyEffect: 0, 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/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/package-lock.json b/package-lock.json index 38789fd53..d5e3c39fa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -62,6 +62,7 @@ "dotenv": "17.2.3", "dotenv-expand": "12.0.3", "envalid": "8.1.1", + "fast-redact": "3.5.0", "fuse.js": "7.1.0", "google-spreadsheet": "3.2.0", "helmet": "7.0.0", @@ -122,6 +123,7 @@ "@storybook/angular": "10.1.10", "@trivago/prettier-plugin-sort-imports": "5.2.2", "@types/big.js": "6.2.2", + "@types/fast-redact": "3.0.4", "@types/google-spreadsheet": "3.1.5", "@types/jest": "30.0.0", "@types/jsonpath": "0.2.4", @@ -12936,6 +12938,13 @@ "@types/send": "*" } }, + "node_modules/@types/fast-redact": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/fast-redact/-/fast-redact-3.0.4.tgz", + "integrity": "sha512-tgGJaXucrCH4Yx2l/AI6e/JQksZhKGIQsVwBMTh+nxUhQDv5tXScTs5DHTw+qSKDXnHL2dTAh1e2rd5pcFQyNQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/geojson": { "version": "7946.0.16", "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", @@ -19940,6 +19949,15 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "license": "MIT" }, + "node_modules/fast-redact": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", + "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/fast-safe-stringify": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", diff --git a/package.json b/package.json index 9dc3a9914..5452f3e95 100644 --- a/package.json +++ b/package.json @@ -106,6 +106,7 @@ "dotenv": "17.2.3", "dotenv-expand": "12.0.3", "envalid": "8.1.1", + "fast-redact": "3.5.0", "fuse.js": "7.1.0", "google-spreadsheet": "3.2.0", "helmet": "7.0.0", @@ -166,6 +167,7 @@ "@storybook/angular": "10.1.10", "@trivago/prettier-plugin-sort-imports": "5.2.2", "@types/big.js": "6.2.2", + "@types/fast-redact": "3.0.4", "@types/google-spreadsheet": "3.1.5", "@types/jest": "30.0.0", "@types/jsonpath": "0.2.4", From e27f258ac69e9d3134933b6701a24139503cb9fe Mon Sep 17 00:00:00 2001 From: Karel De Smet Date: Tue, 3 Feb 2026 20:23:35 +0100 Subject: [PATCH 08/22] Task/extend user detail dialog by actions menu (#6256) * Extend user detail dialog * Update changelog --- CHANGELOG.md | 1 + .../admin-users/admin-users.component.ts | 58 +++++++++++-------- .../interfaces/interfaces.ts | 1 + .../user-detail-dialog.component.ts | 25 ++++++-- .../user-detail-dialog.html | 40 +++++++++---- 5 files changed, 84 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a565308b..30cc42eda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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 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/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 @@
- +
+ +
From c6d1260e07f64ba894cbd73b2d06bf95a6904b0c Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Tue, 3 Feb 2026 20:25:42 +0100 Subject: [PATCH 09/22] Release 2.235.0 (#6275) --- CHANGELOG.md | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 30cc42eda..0883d78b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Unreleased +## 2.235.0 - 2026-02-03 ### Added diff --git a/package-lock.json b/package-lock.json index d5e3c39fa..7c472abc9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ghostfolio", - "version": "2.234.0", + "version": "2.235.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ghostfolio", - "version": "2.234.0", + "version": "2.235.0", "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { diff --git a/package.json b/package.json index 5452f3e95..e379b0f32 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "2.234.0", + "version": "2.235.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "repository": "https://github.com/ghostfolio/ghostfolio", From 0365b9b614c89d0d3dacc6a6282f82d3a610a1e1 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Wed, 4 Feb 2026 17:27:38 +0100 Subject: [PATCH 10/22] Task/upgrade stripe to version 20.3.0 (#6273) * Upgrade stripe to version 20.3.0 * Update changelog --- CHANGELOG.md | 6 ++++++ apps/api/src/app/subscription/subscription.service.ts | 2 +- package-lock.json | 11 ++++------- package.json | 2 +- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0883d78b6..f64e6dc12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Changed + +- Upgraded `stripe` from version `20.1.0` to `20.3.0` + ## 2.235.0 - 2026-02-03 ### Added diff --git a/apps/api/src/app/subscription/subscription.service.ts b/apps/api/src/app/subscription/subscription.service.ts index b38b07bb4..2c0226937 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' } ); } diff --git a/package-lock.json b/package-lock.json index 7c472abc9..25cee635d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -85,7 +85,7 @@ "passport-openidconnect": "0.1.2", "reflect-metadata": "0.2.2", "rxjs": "7.8.1", - "stripe": "20.1.0", + "stripe": "20.3.0", "svgmap": "2.14.0", "tablemark": "4.1.0", "twitter-api-v2": "1.27.0", @@ -32105,13 +32105,10 @@ } }, "node_modules/stripe": { - "version": "20.1.0", - "resolved": "https://registry.npmjs.org/stripe/-/stripe-20.1.0.tgz", - "integrity": "sha512-o1VNRuMkY76ZCq92U3EH3/XHm/WHp7AerpzDs4Zyo8uE5mFL4QUcv/2SudWsSnhBSp4moO2+ZoGCZ7mT8crPmQ==", + "version": "20.3.0", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-20.3.0.tgz", + "integrity": "sha512-DYzcmV1MfYhycr1GwjCjeQVYk9Gu8dpxyTlu7qeDCsuguug7oUTxPsUQuZeSf/OPzK7pofqobvOKVqAwlpgf/Q==", "license": "MIT", - "dependencies": { - "qs": "^6.11.0" - }, "engines": { "node": ">=16" }, diff --git a/package.json b/package.json index e379b0f32..9d7a37979 100644 --- a/package.json +++ b/package.json @@ -129,7 +129,7 @@ "passport-openidconnect": "0.1.2", "reflect-metadata": "0.2.2", "rxjs": "7.8.1", - "stripe": "20.1.0", + "stripe": "20.3.0", "svgmap": "2.14.0", "tablemark": "4.1.0", "twitter-api-v2": "1.27.0", From af034e87c991bf9bfa9701e128a6a3718975481b Mon Sep 17 00:00:00 2001 From: Kenrick Tandrian <60643640+KenTandrian@users.noreply.github.com> Date: Thu, 5 Feb 2026 23:25:20 +0700 Subject: [PATCH 11/22] Task/improve chart type safety (#6277) * Improve chart type safety --- .../benchmark-comparator.component.ts | 20 ++++--- .../investment-chart.component.ts | 50 ++++++++-------- libs/common/src/lib/chart-helper.ts | 55 +++++++++++------- libs/ui/src/lib/chart/chart.registry.ts | 29 ++++++++++ libs/ui/src/lib/chart/index.ts | 1 + .../fire-calculator.component.ts | 18 +++--- .../lib/line-chart/line-chart.component.ts | 58 ++++++++++--------- .../portfolio-proportion-chart.component.ts | 56 +++++++++++------- .../treemap-chart/interfaces/interfaces.ts | 16 +++++ .../treemap-chart/treemap-chart.component.ts | 49 +++++++++------- 10 files changed, 221 insertions(+), 131 deletions(-) create mode 100644 libs/ui/src/lib/chart/chart.registry.ts create mode 100644 libs/ui/src/lib/chart/index.ts 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/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/libs/common/src/lib/chart-helper.ts b/libs/common/src/lib/chart-helper.ts index da6473645..1f385e901 100644 --- a/libs/common/src/lib/chart-helper.ts +++ b/libs/common/src/lib/chart-helper.ts @@ -1,8 +1,11 @@ import type { ElementRef } from '@angular/core'; import type { Chart, - ChartTypeRegistry, + ChartType, + ControllerDatasetOptions, Plugin, + Point, + TooltipOptions, TooltipPosition } from 'chart.js'; import { format } from 'date-fns'; @@ -21,7 +24,7 @@ export function formatGroupedDate({ date, groupBy }: { - date: Date; + date: number; groupBy: GroupBy; }) { if (groupBy === 'month') { @@ -33,7 +36,7 @@ export function formatGroupedDate({ return format(date, DATE_FORMAT); } -export function getTooltipOptions({ +export function getTooltipOptions({ 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/ui/src/lib/chart/chart.registry.ts b/libs/ui/src/lib/chart/chart.registry.ts new file mode 100644 index 000000000..465d6e716 --- /dev/null +++ b/libs/ui/src/lib/chart/chart.registry.ts @@ -0,0 +1,29 @@ +import { getTooltipPositionerMapTop } from '@ghostfolio/common/chart-helper'; + +import { Tooltip, TooltipPositionerFunction, ChartType } from 'chart.js'; + +interface VerticalHoverLinePluginOptions { + color?: string; + width?: number; +} + +declare module 'chart.js' { + interface PluginOptionsByType { + verticalHoverLine: TType extends 'line' | 'bar' + ? VerticalHoverLinePluginOptions + : never; + } + interface TooltipPositionerMap { + top: TooltipPositionerFunction; + } +} + +export function registerChartConfiguration() { + if (Tooltip.positioners['top']) { + return; + } + + Tooltip.positioners.top = function (_elements, eventPosition) { + return getTooltipPositionerMapTop(this.chart, eventPosition); + }; +} diff --git a/libs/ui/src/lib/chart/index.ts b/libs/ui/src/lib/chart/index.ts new file mode 100644 index 000000000..2a3d3b358 --- /dev/null +++ b/libs/ui/src/lib/chart/index.ts @@ -0,0 +1 @@ +export * from './chart.registry'; diff --git a/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts b/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts index 6b0bc8dcb..7461f6729 100644 --- a/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts +++ b/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts @@ -38,6 +38,8 @@ import { BarElement, CategoryScale, Chart, + type ChartData, + type ChartDataset, LinearScale, Tooltip } from 'chart.js'; @@ -270,7 +272,7 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy { this.chart.update(); } else { - this.chart = new Chart(this.chartCanvas.nativeElement, { + this.chart = new Chart<'bar'>(this.chartCanvas.nativeElement, { data: chartData, options: { plugins: { @@ -280,7 +282,7 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy { callbacks: { footer: (items) => { const totalAmount = items.reduce( - (a, b) => a + b.parsed.y, + (a, b) => a + (b.parsed.y ?? 0), 0 ); @@ -302,8 +304,6 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy { if (context.parsed.y !== null) { label += new Intl.NumberFormat(this.locale, { currency: this.currency, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore: Only supported from ES2020 or later currencyDisplay: 'code', style: 'currency' }).format(context.parsed.y); @@ -345,9 +345,9 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy { this.isLoading = false; } - private getChartData() { + private getChartData(): ChartData<'bar'> { const currentYear = new Date().getFullYear(); - const labels = []; + const labels: number[] = []; // Principal investment amount const P: number = this.getP(); @@ -371,13 +371,13 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy { labels.push(year); } - const datasetDeposit = { + const datasetDeposit: ChartDataset<'bar'> = { backgroundColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`, data: [], label: $localize`Deposit` }; - const datasetInterest = { + const datasetInterest: ChartDataset<'bar'> = { backgroundColor: Color( `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})` ) @@ -387,7 +387,7 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy { label: $localize`Interest` }; - const datasetSavings = { + const datasetSavings: ChartDataset<'bar'> = { backgroundColor: Color( `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})` ) diff --git a/libs/ui/src/lib/line-chart/line-chart.component.ts b/libs/ui/src/lib/line-chart/line-chart.component.ts index 0afef5959..dd972bc5a 100644 --- a/libs/ui/src/lib/line-chart/line-chart.component.ts +++ b/libs/ui/src/lib/line-chart/line-chart.component.ts @@ -1,6 +1,5 @@ import { getTooltipOptions, - getTooltipPositionerMapTop, getVerticalHoverLinePlugin } from '@ghostfolio/common/chart-helper'; import { primaryColorRgb, secondaryColorRgb } from '@ghostfolio/common/config'; @@ -19,12 +18,14 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, + type ElementRef, Input, OnChanges, OnDestroy, ViewChild } from '@angular/core'; import { + type AnimationsSpec, Chart, Filler, LinearScale, @@ -33,11 +34,13 @@ import { PointElement, TimeScale, Tooltip, - TooltipPosition + type TooltipOptions } from 'chart.js'; import 'chartjs-adapter-date-fns'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; +import { registerChartConfiguration } from '../chart'; + @Component({ changeDetection: ChangeDetectionStrategy.OnPush, imports: [CommonModule, NgxSkeletonLoaderModule], @@ -67,7 +70,7 @@ export class GfLineChartComponent @Input() yMin: number; @Input() yMinLabel: string; - @ViewChild('chartCanvas') chartCanvas; + @ViewChild('chartCanvas') chartCanvas: ElementRef; public chart: Chart<'line'>; public isLoading = true; @@ -85,8 +88,7 @@ export class GfLineChartComponent Tooltip ); - Tooltip.positioners['top'] = (_elements, position: TooltipPosition) => - getTooltipPositionerMapTop(this.chart, position); + registerChartConfiguration(); } public ngAfterViewInit() { @@ -117,9 +119,9 @@ export class GfLineChartComponent private initialize() { this.isLoading = true; - const benchmarkPrices = []; + const benchmarkPrices: number[] = []; const labels: string[] = []; - const marketPrices = []; + const marketPrices: number[] = []; this.historicalDataItems?.forEach((historicalDataItem, index) => { benchmarkPrices.push(this.benchmarkDataItems?.[index]?.value); @@ -129,11 +131,14 @@ export class GfLineChartComponent const gradient = this.chartCanvas?.nativeElement ?.getContext('2d') - .createLinearGradient( + ?.createLinearGradient( 0, 0, 0, - (this.chartCanvas.nativeElement.parentNode.offsetHeight * 4) / 5 + ((this.chartCanvas.nativeElement.parentNode as HTMLElement) + .offsetHeight * + 4) / + 5 ); if (gradient && this.showGradient) { @@ -169,27 +174,26 @@ export class GfLineChartComponent }; if (this.chartCanvas) { + const animations = { + x: this.getAnimationConfigurationForAxis({ labels, axis: 'x' }), + y: this.getAnimationConfigurationForAxis({ labels, axis: 'y' }) + }; + if (this.chart) { this.chart.data = data; + this.chart.options.plugins ??= {}; this.chart.options.plugins.tooltip = - this.getTooltipPluginConfiguration() as unknown; - this.chart.options.animation = - this.isAnimated && - ({ - x: this.getAnimationConfigurationForAxis({ labels, axis: 'x' }), - y: this.getAnimationConfigurationForAxis({ labels, axis: 'y' }) - } as unknown); + this.getTooltipPluginConfiguration(); + this.chart.options.animations = this.isAnimated + ? animations + : undefined; + this.chart.update(); } else { this.chart = new Chart(this.chartCanvas.nativeElement, { data, options: { - animation: - this.isAnimated && - ({ - x: this.getAnimationConfigurationForAxis({ labels, axis: 'x' }), - y: this.getAnimationConfigurationForAxis({ labels, axis: 'y' }) - } as unknown), + animations: this.isAnimated ? animations : undefined, aspectRatio: 16 / 9, elements: { point: { @@ -208,7 +212,7 @@ export class GfLineChartComponent verticalHoverLine: { color: `rgba(${getTextColor(this.colorScheme)}, 0.1)` } - } as unknown, + }, scales: { x: { border: { @@ -298,7 +302,7 @@ export class GfLineChartComponent }: { axis: 'x' | 'y'; labels: string[]; - }) { + }): Partial[string]> { const delayBetweenPoints = this.ANIMATION_DURATION / labels.length; return { @@ -308,7 +312,7 @@ export class GfLineChartComponent } context[`${axis}Started`] = true; - return context.index * delayBetweenPoints; + return context.dataIndex * delayBetweenPoints; }, duration: delayBetweenPoints, easing: 'linear', @@ -317,7 +321,7 @@ export class GfLineChartComponent }; } - private getTooltipPluginConfiguration() { + private getTooltipPluginConfiguration(): Partial> { return { ...getTooltipOptions({ colorScheme: this.colorScheme, @@ -326,7 +330,7 @@ export class GfLineChartComponent unit: this.unit }), mode: 'index', - position: 'top' as unknown, + position: 'top', xAlign: 'center', yAlign: 'bottom' }; diff --git a/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts b/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts index fb11897eb..7d0203e9c 100644 --- a/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts +++ b/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts @@ -22,11 +22,16 @@ import { } from '@angular/core'; import { DataSource } from '@prisma/client'; import { Big } from 'big.js'; -import { ChartConfiguration, Tooltip } from 'chart.js'; -import { LinearScale } from 'chart.js'; -import { ArcElement } from 'chart.js'; -import { DoughnutController } from 'chart.js'; -import { Chart } from 'chart.js'; +import { + ArcElement, + Chart, + type ChartData, + type ChartDataset, + DoughnutController, + LinearScale, + Tooltip, + type TooltipOptions +} from 'chart.js'; import ChartDataLabels from 'chartjs-plugin-datalabels'; import { isUUID } from 'class-validator'; import Color from 'color'; @@ -286,7 +291,7 @@ export class GfPortfolioProportionChartComponent }); }); - const datasets: ChartConfiguration<'doughnut'>['data']['datasets'] = [ + const datasets: ChartDataset<'doughnut'>[] = [ { backgroundColor: chartDataSorted.map(([, item]) => { return item.color; @@ -324,7 +329,7 @@ export class GfPortfolioProportionChartComponent datasets[1].data[1] = Number.MAX_SAFE_INTEGER; } - const data: ChartConfiguration<'doughnut'>['data'] = { + const data: ChartData<'doughnut'> = { datasets, labels }; @@ -332,9 +337,10 @@ export class GfPortfolioProportionChartComponent if (this.chartCanvas) { if (this.chart) { this.chart.data = data; - this.chart.options.plugins.tooltip = this.getTooltipPluginConfiguration( - data - ) as unknown; + this.chart.options.plugins ??= {}; + this.chart.options.plugins.tooltip = + this.getTooltipPluginConfiguration(data); + this.chart.update(); } else { this.chart = new Chart<'doughnut'>(this.chartCanvas.nativeElement, { @@ -345,21 +351,22 @@ export class GfPortfolioProportionChartComponent layout: { padding: this.showLabels === true ? 100 : 0 }, - onClick: (event, activeElements) => { + onClick: (_, activeElements, chart) => { try { const dataIndex = activeElements[0].index; - const symbol: string = event.chart.data.labels[dataIndex]; + const symbol = chart.data.labels?.[dataIndex] as string; - const dataSource = this.data[symbol]?.dataSource; + const dataSource = this.data[symbol].dataSource; - this.proportionChartClicked.emit({ dataSource, symbol }); + if (dataSource) { + this.proportionChartClicked.emit({ dataSource, symbol }); + } } catch {} }, onHover: (event, chartElement) => { if (this.cursor) { - event.native.target.style.cursor = chartElement[0] - ? this.cursor - : 'default'; + (event.native?.target as HTMLElement).style.cursor = + chartElement[0] ? this.cursor : 'default'; } }, plugins: { @@ -392,7 +399,7 @@ export class GfPortfolioProportionChartComponent legend: { display: false }, tooltip: this.getTooltipPluginConfiguration(data) } - } as unknown, + }, plugins: [ChartDataLabels], type: 'doughnut' }); @@ -419,19 +426,24 @@ export class GfPortfolioProportionChartComponent ]; } - private getTooltipPluginConfiguration(data: ChartConfiguration['data']) { + private getTooltipPluginConfiguration( + data: ChartData<'doughnut'> + ): Partial> { return { ...getTooltipOptions({ colorScheme: this.colorScheme, currency: this.baseCurrency, locale: this.locale }), + // @ts-expect-error: no need to set all attributes in callbacks callbacks: { label: (context) => { const labelIndex = (data.datasets[context.datasetIndex - 1]?.data?.length ?? 0) + context.dataIndex; - let symbol = context.chart.data.labels?.[labelIndex] ?? ''; + + let symbol = + (context.chart.data.labels?.[labelIndex] as string) ?? ''; if (symbol === this.OTHER_KEY) { symbol = $localize`Other`; @@ -439,9 +451,10 @@ export class GfPortfolioProportionChartComponent symbol = $localize`No data available`; } - const name = translate(this.data[symbol as string]?.name); + const name = translate(this.data[symbol]?.name); let sum = 0; + for (const item of context.dataset.data) { sum += item; } @@ -454,6 +467,7 @@ export class GfPortfolioProportionChartComponent return [`${name ?? symbol}`, `${percentage.toFixed(2)}%`]; } else { const value = context.raw as number; + return [ `${name ?? symbol}`, `${value.toLocaleString(this.locale, { diff --git a/libs/ui/src/lib/treemap-chart/interfaces/interfaces.ts b/libs/ui/src/lib/treemap-chart/interfaces/interfaces.ts index bb673ed64..e8d182adb 100644 --- a/libs/ui/src/lib/treemap-chart/interfaces/interfaces.ts +++ b/libs/ui/src/lib/treemap-chart/interfaces/interfaces.ts @@ -1,5 +1,21 @@ +import { PortfolioPosition } from '@ghostfolio/common/interfaces'; + +import { ScriptableContext, TooltipItem } from 'chart.js'; +import { TreemapDataPoint } from 'chartjs-chart-treemap'; + export interface GetColorParams { annualizedNetPerformancePercent: number; negativeNetPerformancePercentsRange: { max: number; min: number }; positiveNetPerformancePercentsRange: { max: number; min: number }; } + +interface GfTreemapDataPoint extends TreemapDataPoint { + _data: PortfolioPosition; +} + +export interface GfTreemapScriptableContext extends ScriptableContext<'treemap'> { + raw: GfTreemapDataPoint; +} +export interface GfTreemapTooltipItem extends TooltipItem<'treemap'> { + raw: GfTreemapDataPoint; +} diff --git a/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts b/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts index 6ae958b83..ce85c300e 100644 --- a/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts +++ b/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts @@ -25,7 +25,7 @@ import { } from '@angular/core'; import { DataSource } from '@prisma/client'; import { Big } from 'big.js'; -import { ChartConfiguration } from 'chart.js'; +import type { TooltipOptions, ChartData } from 'chart.js'; import { LinearScale } from 'chart.js'; import { Chart, Tooltip } from 'chart.js'; import { TreemapController, TreemapElement } from 'chartjs-chart-treemap'; @@ -35,7 +35,11 @@ import { orderBy } from 'lodash'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import OpenColor from 'open-color'; -import { GetColorParams } from './interfaces/interfaces'; +import type { + GetColorParams, + GfTreemapScriptableContext, + GfTreemapTooltipItem +} from './interfaces/interfaces'; const { gray, green, red } = OpenColor; @@ -198,10 +202,10 @@ export class GfTreemapChartComponent min: Math.min(...negativeNetPerformancePercents) }; - const data: ChartConfiguration<'treemap'>['data'] = { + const data: ChartData<'treemap'> = { datasets: [ { - backgroundColor: (context) => { + backgroundColor: (context: GfTreemapScriptableContext) => { let annualizedNetPerformancePercent = getAnnualizedPerformancePercent({ daysInMarket: differenceInDays( @@ -232,7 +236,7 @@ export class GfTreemapChartComponent key: 'allocationInPercentage', labels: { align: 'left', - color: (context) => { + color: (context: GfTreemapScriptableContext) => { let annualizedNetPerformancePercent = getAnnualizedPerformancePercent({ daysInMarket: differenceInDays( @@ -261,7 +265,7 @@ export class GfTreemapChartComponent }, display: true, font: [{ size: 16 }, { lineHeight: 1.5, size: 14 }], - formatter: ({ raw }) => { + formatter: ({ raw }: GfTreemapScriptableContext) => { // Round to 4 decimal places let netPerformancePercentWithCurrencyEffect = Math.round( @@ -286,32 +290,35 @@ export class GfTreemapChartComponent position: 'top' }, spacing: 1, + // @ts-expect-error: should be PortfolioPosition[] tree: this.holdings } ] - } as any; + }; 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, { + this.chart = new Chart<'treemap'>(this.chartCanvas.nativeElement, { data, options: { animation: false, - onClick: (event, activeElements) => { + onClick: (_, activeElements, chart: Chart<'treemap'>) => { try { const dataIndex = activeElements[0].index; const datasetIndex = activeElements[0].datasetIndex; const dataset = orderBy( - event.chart.data.datasets[datasetIndex].tree, + chart.data.datasets[datasetIndex].tree, ['allocationInPercentage'], ['desc'] - ); + ) as PortfolioPosition[]; const dataSource: DataSource = dataset[dataIndex].dataSource; const symbol: string = dataset[dataIndex].symbol; @@ -321,15 +328,14 @@ export class GfTreemapChartComponent }, onHover: (event, chartElement) => { if (this.cursor) { - event.native.target.style.cursor = chartElement[0] - ? this.cursor - : 'default'; + (event.native?.target as HTMLElement).style.cursor = + chartElement[0] ? this.cursor : 'default'; } }, plugins: { tooltip: this.getTooltipPluginConfiguration() } - } as unknown, + }, type: 'treemap' }); } @@ -338,16 +344,17 @@ export class GfTreemapChartComponent this.isLoading = false; } - private getTooltipPluginConfiguration() { + private getTooltipPluginConfiguration(): Partial> { return { ...getTooltipOptions({ colorScheme: this.colorScheme, currency: this.baseCurrency, locale: this.locale }), + // @ts-expect-error: no need to set all attributes in callbacks callbacks: { - label: ({ raw }) => { - const allocationInPercentage = `${((raw._data.allocationInPercentage as number) * 100).toFixed(2)}%`; + label: ({ raw }: GfTreemapTooltipItem) => { + const allocationInPercentage = `${(raw._data.allocationInPercentage * 100).toFixed(2)}%`; const name = raw._data.name; const sign = raw._data.netPerformancePercentWithCurrencyEffect > 0 ? '+' : ''; @@ -356,11 +363,11 @@ export class GfTreemapChartComponent const netPerformanceInPercentageWithSign = `${sign}${(raw._data.netPerformancePercentWithCurrencyEffect * 100).toFixed(2)}%`; if (raw._data.valueInBaseCurrency !== null) { - const value = raw._data.valueInBaseCurrency as number; + const value = raw._data.valueInBaseCurrency; return [ `${name ?? symbol} (${allocationInPercentage})`, - `${value.toLocaleString(this.locale, { + `${value?.toLocaleString(this.locale, { maximumFractionDigits: 2, minimumFractionDigits: 2 })} ${this.baseCurrency}`, From eb6177d2e56aebaed2b35e2811f5d944c22667c4 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Thu, 5 Feb 2026 19:40:41 +0100 Subject: [PATCH 12/22] Bugfix/fix exception when fetching top holdings in Yahoo Finance service (#6279) * Add missing guard * Update changelog --- CHANGELOG.md | 4 +++ .../yahoo-finance/yahoo-finance.service.ts | 34 +++++++++---------- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f64e6dc12..e4d88d75f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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 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 97c875360..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 @@ -206,26 +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 - }); - } - } - - response.holdings = assetProfile.topHoldings.holdings.map( - ({ holdingName, holdingPercent }) => { - return { - name: this.formatName({ longName: holdingName }), - weight: holdingPercent }; - } - ); + }); + }); } else if ( assetSubClass === 'STOCK' && assetProfile.summaryProfile?.country From 4ff372f0205366e1afdb36c8c8ade4dbb491878b Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Thu, 5 Feb 2026 20:24:04 +0100 Subject: [PATCH 13/22] Task/remove deprecated transaction count in get admin endpoint (#6281) * Remove deprecated transaction count * Update changelog --- CHANGELOG.md | 1 + apps/api/src/app/admin/admin.service.ts | 1 - libs/common/src/lib/interfaces/admin-data.interface.ts | 4 ---- 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e4d88d75f..5e1c8b91a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Removed the deprecated `transactionCount` in the endpoint `GET api/v1/admin` - Upgraded `stripe` from version `20.1.0` to `20.3.0` ### Fixed diff --git a/apps/api/src/app/admin/admin.service.ts b/apps/api/src/app/admin/admin.service.ts index cd18eb239..2cc8bbfb8 100644 --- a/apps/api/src/app/admin/admin.service.ts +++ b/apps/api/src/app/admin/admin.service.ts @@ -186,7 +186,6 @@ export class AdminService { dataProviders, settings, userCount, - transactionCount: activitiesCount, version: environment.version }; } diff --git a/libs/common/src/lib/interfaces/admin-data.interface.ts b/libs/common/src/lib/interfaces/admin-data.interface.ts index 63588300c..dd25b516d 100644 --- a/libs/common/src/lib/interfaces/admin-data.interface.ts +++ b/libs/common/src/lib/interfaces/admin-data.interface.ts @@ -7,10 +7,6 @@ export interface AdminData { useForExchangeRates: boolean; })[]; settings: { [key: string]: boolean | object | string | string[] }; - - /** @deprecated use activitiesCount instead */ - transactionCount: number; - userCount: number; version: string; } From bf9f8d49e9301c676cb2195f8950812ad4b77e82 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Thu, 5 Feb 2026 20:28:23 +0100 Subject: [PATCH 14/22] Release 2.236.0 (#6282) --- CHANGELOG.md | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e1c8b91a..53c9b42d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Unreleased +## 2.236.0 - 2026-02-05 ### Changed diff --git a/package-lock.json b/package-lock.json index 25cee635d..b90f9050f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ghostfolio", - "version": "2.235.0", + "version": "2.236.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ghostfolio", - "version": "2.235.0", + "version": "2.236.0", "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { diff --git a/package.json b/package.json index 9d7a37979..0aa520beb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "2.235.0", + "version": "2.236.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "repository": "https://github.com/ghostfolio/ghostfolio", From 5adacda88fd4010ef217409ab635eb6c2aed0625 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 6 Feb 2026 12:22:39 +0100 Subject: [PATCH 15/22] Task/remove deprecated session id from create stripe checkout session response (#6280) * Remove deprecated sessionId --- apps/api/src/app/subscription/subscription.service.ts | 1 - .../create-stripe-checkout-session-response.interface.ts | 3 --- 2 files changed, 4 deletions(-) diff --git a/apps/api/src/app/subscription/subscription.service.ts b/apps/api/src/app/subscription/subscription.service.ts index 2c0226937..689ee3e6a 100644 --- a/apps/api/src/app/subscription/subscription.service.ts +++ b/apps/api/src/app/subscription/subscription.service.ts @@ -100,7 +100,6 @@ export class SubscriptionService { ); return { - sessionId: session.id, sessionUrl: session.url }; } 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; } From 348ee5de8d6e62f3271327090d9765633b4bae13 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 7 Feb 2026 14:04:10 +0100 Subject: [PATCH 16/22] Task/add missing transform data source interceptors in market data controller (#6287) * Add missing transform data source interceptors --- .../app/endpoints/market-data/market-data.controller.ts | 7 ++++++- .../src/app/endpoints/market-data/market-data.module.ts | 6 +++++- 2 files changed, 11 insertions(+), 2 deletions(-) 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 {} From 63e75942a8e227dff18fe7667bc0cea30fab58e4 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 7 Feb 2026 14:51:09 +0100 Subject: [PATCH 17/22] Task/remove deprecated transaction count in portfolio calculator and service (#6288) * Remove deprecated transactionCount in portfolio calculator and service * Update changelog --- CHANGELOG.md | 6 +++ apps/api/src/app/account/account.service.ts | 6 +-- .../calculator/portfolio-calculator.ts | 7 +-- ...tfolio-calculator-baln-buy-and-buy.spec.ts | 1 - ...aln-buy-and-sell-in-two-activities.spec.ts | 1 - ...folio-calculator-baln-buy-and-sell.spec.ts | 1 - .../portfolio-calculator-baln-buy.spec.ts | 1 - .../roai/portfolio-calculator-btceur.spec.ts | 1 - ...ator-btcusd-buy-and-sell-partially.spec.ts | 1 - .../roai/portfolio-calculator-btcusd.spec.ts | 1 - .../roai/portfolio-calculator-cash.spec.ts | 1 - .../portfolio-calculator-googl-buy.spec.ts | 1 - ...-calculator-msft-buy-with-dividend.spec.ts | 3 +- ...ulator-novn-buy-and-sell-partially.spec.ts | 1 - ...folio-calculator-novn-buy-and-sell.spec.ts | 1 - .../portfolio-calculator-valuable.spec.ts | 1 - .../transaction-point-symbol.interface.ts | 3 -- .../src/app/portfolio/portfolio.service.ts | 8 ---- apps/api/src/helper/object.helper.spec.ts | 48 +++++++++---------- .../account-detail-dialog.component.ts | 6 +-- .../account-detail-dialog.html | 2 +- .../portfolio-position.interface.ts | 4 -- .../responses/accounts-response.interface.ts | 3 -- .../src/lib/models/timeline-position.ts | 3 -- .../src/lib/types/account-with-value.type.ts | 4 -- .../accounts-table.component.html | 6 +-- .../accounts-table.component.stories.ts | 6 +-- libs/ui/src/lib/mocks/holdings.ts | 7 --- 28 files changed, 46 insertions(+), 88 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 53c9b42d8..094266869 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Changed + +- Removed the `transactionCount` in the portfolio calculator and service + ## 2.236.0 - 2026-02-05 ### Changed 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/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index 9612ad1c4..2e58a4ef5 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts @@ -445,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 ) @@ -1005,8 +1004,7 @@ export abstract class PortfolioCalculator { oldAccumulatedSymbol.feeInBaseCurrency.plus(feeInBaseCurrency), includeInHoldings: oldAccumulatedSymbol.includeInHoldings, quantity: newQuantity, - tags: oldAccumulatedSymbol.tags.concat(tags), - transactionCount: oldAccumulatedSymbol.transactionCount + 1 + tags: oldAccumulatedSymbol.tags.concat(tags) }; } else { currentTransactionPointItem = { @@ -1024,8 +1022,7 @@ export abstract class PortfolioCalculator { dividend: new Big(0), 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 7858d7546..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 @@ -178,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 8b40c7b70..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 @@ -192,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 fc372f68f..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 @@ -176,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 926fae6dc..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 @@ -172,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 b216438b8..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 @@ -227,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 14cd4f217..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 @@ -194,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 066f33ea3..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 @@ -227,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 bd8afddd7..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 @@ -276,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 28b44e159..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 @@ -172,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 87ef9ed8b..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 @@ -162,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 7a8dc010a..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 @@ -174,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 02a4e80d8..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 @@ -225,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 610a52c06..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 @@ -146,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 7e7d741ea..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 @@ -17,7 +17,4 @@ export interface TransactionPointSymbol { skipErrors: boolean; symbol: string; tags?: Tag[]; - - /** @deprecated use activitiesCount instead */ - transactionCount: number; } diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 05df6a8fc..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(), @@ -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(), @@ -1696,7 +1689,6 @@ export class PortfolioService { sectors: [], symbol: currency, tags: [], - transactionCount: 0, valueInBaseCurrency: balance }; } diff --git a/apps/api/src/helper/object.helper.spec.ts b/apps/api/src/helper/object.helper.spec.ts index 5ddff164b..ed821390f 100644 --- a/apps/api/src/helper/object.helper.spec.ts +++ b/apps/api/src/helper/object.helper.spec.ts @@ -111,6 +111,7 @@ describe('redactAttributes', () => { hasError: false, holdings: { 'AAPL.US': { + activitiesCount: 1, currency: 'USD', markets: { UNKNOWN: 0, @@ -130,7 +131,6 @@ describe('redactAttributes', () => { marketPrice: 220.79, symbol: 'AAPL.US', tags: [], - transactionCount: 1, allocationInPercentage: 0.044900865255793135, assetClass: 'EQUITY', assetSubClass: 'STOCK', @@ -163,6 +163,7 @@ describe('redactAttributes', () => { valueInPercentage: 0.0694356974830054 }, 'ALV.DE': { + activitiesCount: 2, currency: 'EUR', markets: { UNKNOWN: 0, @@ -182,7 +183,6 @@ describe('redactAttributes', () => { marketPrice: 296.5, symbol: 'ALV.DE', tags: [], - transactionCount: 2, allocationInPercentage: 0.026912563036519527, assetClass: 'EQUITY', assetSubClass: 'STOCK', @@ -210,6 +210,7 @@ describe('redactAttributes', () => { valueInPercentage: 0.04161818652826481 }, AMZN: { + activitiesCount: 1, currency: 'USD', markets: { UNKNOWN: 0, @@ -229,7 +230,6 @@ describe('redactAttributes', () => { marketPrice: 187.99, symbol: 'AMZN', tags: [], - transactionCount: 1, allocationInPercentage: 0.07646101417126275, assetClass: 'EQUITY', assetSubClass: 'STOCK', @@ -262,6 +262,7 @@ describe('redactAttributes', () => { valueInPercentage: 0.11824101426541227 }, bitcoin: { + activitiesCount: 1, currency: 'USD', markets: { UNKNOWN: 36985.0332704, @@ -287,7 +288,6 @@ describe('redactAttributes', () => { userId: null } ], - transactionCount: 1, allocationInPercentage: 0.15042891393226654, assetClass: 'LIQUIDITY', assetSubClass: 'CRYPTOCURRENCY', @@ -313,6 +313,7 @@ describe('redactAttributes', () => { valueInPercentage: 0.232626620912395 }, BONDORA_GO_AND_GROW: { + activitiesCount: 5, currency: 'EUR', markets: { UNKNOWN: 2231.644722160232, @@ -338,7 +339,6 @@ describe('redactAttributes', () => { userId: null } ], - transactionCount: 5, allocationInPercentage: 0.009076749759365777, assetClass: 'FIXED_INCOME', assetSubClass: 'BOND', @@ -364,6 +364,7 @@ describe('redactAttributes', () => { valueInPercentage: 0.014036487867880205 }, FRANKLY95P: { + activitiesCount: 6, currency: 'CHF', markets: { UNKNOWN: 0, @@ -389,7 +390,6 @@ describe('redactAttributes', () => { userId: null } ], - transactionCount: 6, allocationInPercentage: 0.09095764645669335, assetClass: 'EQUITY', assetSubClass: 'ETF', @@ -488,6 +488,7 @@ describe('redactAttributes', () => { valueInPercentage: 0.14065892911313693 }, MSFT: { + activitiesCount: 1, currency: 'USD', markets: { UNKNOWN: 0, @@ -507,7 +508,6 @@ describe('redactAttributes', () => { marketPrice: 428.02, symbol: 'MSFT', tags: [], - transactionCount: 1, allocationInPercentage: 0.05222646409742627, assetClass: 'EQUITY', assetSubClass: 'STOCK', @@ -540,6 +540,7 @@ describe('redactAttributes', () => { valueInPercentage: 0.08076416659271518 }, TSLA: { + activitiesCount: 1, currency: 'USD', markets: { UNKNOWN: 0, @@ -559,7 +560,6 @@ describe('redactAttributes', () => { marketPrice: 260.46, symbol: 'TSLA', tags: [], - transactionCount: 1, allocationInPercentage: 0.1589050142378352, assetClass: 'EQUITY', assetSubClass: 'STOCK', @@ -592,6 +592,7 @@ describe('redactAttributes', () => { valueInPercentage: 0.2457342510950259 }, VTI: { + activitiesCount: 5, currency: 'USD', markets: { UNKNOWN: 0, @@ -611,7 +612,6 @@ describe('redactAttributes', () => { marketPrice: 282.05, symbol: 'VTI', tags: [], - transactionCount: 5, allocationInPercentage: 0.057358979326040366, assetClass: 'EQUITY', assetSubClass: 'ETF', @@ -764,6 +764,7 @@ describe('redactAttributes', () => { valueInPercentage: 0.08870120238725339 }, 'VWRL.SW': { + activitiesCount: 5, currency: 'CHF', markets: { UNKNOWN: 0, @@ -783,7 +784,6 @@ describe('redactAttributes', () => { marketPrice: 117.62, symbol: 'VWRL.SW', tags: [], - transactionCount: 5, allocationInPercentage: 0.09386983901959013, assetClass: 'EQUITY', assetSubClass: 'ETF', @@ -1172,6 +1172,7 @@ describe('redactAttributes', () => { valueInPercentage: 0.145162408515095 }, 'XDWD.DE': { + activitiesCount: 1, currency: 'EUR', markets: { UNKNOWN: 0, @@ -1191,7 +1192,6 @@ describe('redactAttributes', () => { marketPrice: 105.72, symbol: 'XDWD.DE', tags: [], - transactionCount: 1, allocationInPercentage: 0.03598477442100562, assetClass: 'EQUITY', assetSubClass: 'ETF', @@ -1450,6 +1450,7 @@ describe('redactAttributes', () => { valueInPercentage: 0.055647656152211074 }, USD: { + activitiesCount: 0, currency: 'USD', allocationInPercentage: 0.20291717628620132, assetClass: 'LIQUIDITY', @@ -1472,7 +1473,6 @@ describe('redactAttributes', () => { sectors: [], symbol: 'USD', tags: [], - transactionCount: 0, valueInBaseCurrency: 49890, valueInPercentage: 0.3137956381563603 } @@ -1615,6 +1615,7 @@ describe('redactAttributes', () => { hasError: false, holdings: { 'AAPL.US': { + activitiesCount: 1, currency: 'USD', markets: { UNKNOWN: 0, @@ -1634,7 +1635,6 @@ describe('redactAttributes', () => { marketPrice: 220.79, symbol: 'AAPL.US', tags: [], - transactionCount: 1, allocationInPercentage: 0.044900865255793135, assetClass: 'EQUITY', assetSubClass: 'STOCK', @@ -1667,6 +1667,7 @@ describe('redactAttributes', () => { valueInPercentage: 0.0694356974830054 }, 'ALV.DE': { + activitiesCount: 2, currency: 'EUR', markets: { UNKNOWN: 0, @@ -1686,7 +1687,6 @@ describe('redactAttributes', () => { marketPrice: 296.5, symbol: 'ALV.DE', tags: [], - transactionCount: 2, allocationInPercentage: 0.026912563036519527, assetClass: 'EQUITY', assetSubClass: 'STOCK', @@ -1714,6 +1714,7 @@ describe('redactAttributes', () => { valueInPercentage: 0.04161818652826481 }, AMZN: { + activitiesCount: 1, currency: 'USD', markets: { UNKNOWN: 0, @@ -1733,7 +1734,6 @@ describe('redactAttributes', () => { marketPrice: 187.99, symbol: 'AMZN', tags: [], - transactionCount: 1, allocationInPercentage: 0.07646101417126275, assetClass: 'EQUITY', assetSubClass: 'STOCK', @@ -1766,6 +1766,7 @@ describe('redactAttributes', () => { valueInPercentage: 0.11824101426541227 }, bitcoin: { + activitiesCount: 1, currency: 'USD', markets: { UNKNOWN: 36985.0332704, @@ -1791,7 +1792,6 @@ describe('redactAttributes', () => { userId: null } ], - transactionCount: 1, allocationInPercentage: 0.15042891393226654, assetClass: 'LIQUIDITY', assetSubClass: 'CRYPTOCURRENCY', @@ -1817,6 +1817,7 @@ describe('redactAttributes', () => { valueInPercentage: 0.232626620912395 }, BONDORA_GO_AND_GROW: { + activitiesCount: 5, currency: 'EUR', markets: { UNKNOWN: 2231.644722160232, @@ -1842,7 +1843,6 @@ describe('redactAttributes', () => { userId: null } ], - transactionCount: 5, allocationInPercentage: 0.009076749759365777, assetClass: 'FIXED_INCOME', assetSubClass: 'BOND', @@ -1868,6 +1868,7 @@ describe('redactAttributes', () => { valueInPercentage: 0.014036487867880205 }, FRANKLY95P: { + activitiesCount: 6, currency: 'CHF', markets: { UNKNOWN: 0, @@ -1893,7 +1894,6 @@ describe('redactAttributes', () => { userId: null } ], - transactionCount: 6, allocationInPercentage: 0.09095764645669335, assetClass: 'EQUITY', assetSubClass: 'ETF', @@ -1972,6 +1972,7 @@ describe('redactAttributes', () => { valueInPercentage: 0.14065892911313693 }, MSFT: { + activitiesCount: 1, currency: 'USD', markets: { UNKNOWN: 0, @@ -1991,7 +1992,6 @@ describe('redactAttributes', () => { marketPrice: 428.02, symbol: 'MSFT', tags: [], - transactionCount: 1, allocationInPercentage: 0.05222646409742627, assetClass: 'EQUITY', assetSubClass: 'STOCK', @@ -2024,6 +2024,7 @@ describe('redactAttributes', () => { valueInPercentage: 0.08076416659271518 }, TSLA: { + activitiesCount: 1, currency: 'USD', markets: { UNKNOWN: 0, @@ -2043,7 +2044,6 @@ describe('redactAttributes', () => { marketPrice: 260.46, symbol: 'TSLA', tags: [], - transactionCount: 1, allocationInPercentage: 0.1589050142378352, assetClass: 'EQUITY', assetSubClass: 'STOCK', @@ -2076,6 +2076,7 @@ describe('redactAttributes', () => { valueInPercentage: 0.2457342510950259 }, VTI: { + activitiesCount: 5, currency: 'USD', markets: { UNKNOWN: 0, @@ -2095,7 +2096,6 @@ describe('redactAttributes', () => { marketPrice: 282.05, symbol: 'VTI', tags: [], - transactionCount: 5, allocationInPercentage: 0.057358979326040366, assetClass: 'EQUITY', assetSubClass: 'ETF', @@ -2248,6 +2248,7 @@ describe('redactAttributes', () => { valueInPercentage: 0.08870120238725339 }, 'VWRL.SW': { + activitiesCount: 5, currency: 'CHF', markets: { UNKNOWN: 0, @@ -2267,7 +2268,6 @@ describe('redactAttributes', () => { marketPrice: 117.62, symbol: 'VWRL.SW', tags: [], - transactionCount: 5, allocationInPercentage: 0.09386983901959013, assetClass: 'EQUITY', assetSubClass: 'ETF', @@ -2648,6 +2648,7 @@ describe('redactAttributes', () => { valueInPercentage: 0.145162408515095 }, 'XDWD.DE': { + activitiesCount: 1, currency: 'EUR', markets: { UNKNOWN: 0, @@ -2667,7 +2668,6 @@ describe('redactAttributes', () => { marketPrice: 105.72, symbol: 'XDWD.DE', tags: [], - transactionCount: 1, allocationInPercentage: 0.03598477442100562, assetClass: 'EQUITY', assetSubClass: 'ETF', @@ -2926,6 +2926,7 @@ describe('redactAttributes', () => { valueInPercentage: 0.055647656152211074 }, USD: { + activitiesCount: 0, currency: 'USD', allocationInPercentage: 0.20291717628620132, assetClass: 'LIQUIDITY', @@ -2948,7 +2949,6 @@ describe('redactAttributes', () => { sectors: [], symbol: 'USD', tags: [], - transactionCount: 0, valueInBaseCurrency: null, valueInPercentage: 0.3137956381563603 } 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/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/models/timeline-position.ts b/libs/common/src/lib/models/timeline-position.ts index 9cfb2df04..13f9001d5 100644 --- a/libs/common/src/lib/models/timeline-position.ts +++ b/libs/common/src/lib/models/timeline-position.ts @@ -93,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 c9124820c..68ae78474 100644 --- a/libs/ui/src/lib/accounts-table/accounts-table.component.html +++ b/libs/ui/src/lib/accounts-table/accounts-table.component.html @@ -120,13 +120,13 @@ *matHeaderCellDef class="justify-content-end px-1" mat-header-cell - mat-sort-header="transactionCount" + mat-sort-header="activitiesCount" > # Activities - {{ element.transactionCount }} + {{ element.activitiesCount }} {{ activitiesCount }} @@ -323,7 +323,7 @@