- |
+ |
Creation
|
-
+ |
@if (element.createdAt) {
Date: Sat, 5 Sep 2026 10:25:55 +0200
Subject: [PATCH 19/50] Release 3.67.0 (#7822)
---
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 130b2a7fb5..723e84a230 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
+## 3.67.0 - 2026-09-05
### Added
diff --git a/package-lock.json b/package-lock.json
index bf6b5c89e3..39a02c7ad6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "ghostfolio",
- "version": "3.66.0",
+ "version": "3.67.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ghostfolio",
- "version": "3.66.0",
+ "version": "3.67.0",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {
diff --git a/package.json b/package.json
index 3034b70c34..c0439aefb0 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "ghostfolio",
- "version": "3.66.0",
+ "version": "3.67.0",
"homepage": "https://ghostfol.io",
"license": "AGPL-3.0",
"repository": "https://github.com/ghostfolio/ghostfolio",
From 3dfd3486fb4924d71f0e3b8f01d2690b1c972667 Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Sat, 5 Sep 2026 15:03:25 +0200
Subject: [PATCH 20/50] Task/harden validation in Stripe checkout session
endpoint (#7824)
* Harden validation
* Add rate limiting
---
.../app/subscription/subscription.controller.ts | 5 +++--
.../src/app/subscription/subscription.service.ts | 14 ++++++++++----
2 files changed, 13 insertions(+), 6 deletions(-)
diff --git a/apps/api/src/app/subscription/subscription.controller.ts b/apps/api/src/app/subscription/subscription.controller.ts
index 0cfc4d90e3..778e322433 100644
--- a/apps/api/src/app/subscription/subscription.controller.ts
+++ b/apps/api/src/app/subscription/subscription.controller.ts
@@ -1,4 +1,5 @@
import { AllowDuringImpersonation } from '@ghostfolio/api/decorators/allow-during-impersonation.decorator';
+import { CustomThrottlerGuard } from '@ghostfolio/api/guards/custom-throttler.guard';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
@@ -46,7 +47,7 @@ export class SubscriptionController {
@Post('redeem-coupon')
@HttpCode(StatusCodes.OK)
- @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
+ @UseGuards(AuthGuard('jwt'), CustomThrottlerGuard, HasPermissionGuard)
public async redeemCoupon(@Body() { couponCode }: { couponCode: string }) {
if (!this.request.user) {
throw new HttpException(
@@ -119,7 +120,7 @@ export class SubscriptionController {
}
@Post('stripe/checkout-session')
- @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
+ @UseGuards(AuthGuard('jwt'), CustomThrottlerGuard, HasPermissionGuard)
public async createStripeCheckoutSession(
@Body() { couponId, priceId }: { couponId?: string; priceId: string }
): Promise {
diff --git a/apps/api/src/app/subscription/subscription.service.ts b/apps/api/src/app/subscription/subscription.service.ts
index aaa4deb5a5..312b8c3aef 100644
--- a/apps/api/src/app/subscription/subscription.service.ts
+++ b/apps/api/src/app/subscription/subscription.service.ts
@@ -17,7 +17,7 @@ import {
UserWithSettings
} from '@ghostfolio/common/types';
-import { Injectable, Logger } from '@nestjs/common';
+import { BadRequestException, Injectable, Logger } from '@nestjs/common';
import { Prisma, Subscription } from '@prisma/client';
import { addMilliseconds, isBefore } from 'date-fns';
import ms, { StringValue } from 'ms';
@@ -64,6 +64,14 @@ export class SubscriptionService {
}
);
+ if (!subscriptionOffer) {
+ throw new BadRequestException('Invalid priceId');
+ }
+
+ if (couponId && couponId !== subscriptionOffer.couponId) {
+ throw new BadRequestException('Invalid couponId');
+ }
+
const stripeCheckoutSessionCreateParams: Stripe.Checkout.SessionCreateParams =
{
cancel_url: `${this.configurationService.get('ROOT_URL')}/${
@@ -77,9 +85,7 @@ export class SubscriptionService {
}
],
locale: this.getStripeLocale(user.settings?.settings?.language),
- metadata: subscriptionOffer
- ? { subscriptionOffer: JSON.stringify(subscriptionOffer) }
- : {},
+ metadata: { subscriptionOffer: JSON.stringify(subscriptionOffer) },
mode: 'payment',
payment_method_types: ['card'],
success_url: `${this.configurationService.get(
From 3d9018a6a77407ae7a4ee86ef296a181b61c5942 Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Sat, 5 Sep 2026 15:22:50 +0200
Subject: [PATCH 21/50] Task/improve data provider credits component (#7826)
Improve data provider credits
---
.../transform-data-source-in-response.interceptor.ts | 2 ++
.../data-provider-credits.component.html | 9 +++------
2 files changed, 5 insertions(+), 6 deletions(-)
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 cbe5b4363d..3b9addf76c 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
@@ -94,6 +94,8 @@ export class TransformDataSourceInResponseInterceptor<
'watchlist[*].dataSource'
]
});
+
+ data.dataProviderInfo = undefined;
}
return data;
diff --git a/libs/ui/src/lib/data-provider-credits/data-provider-credits.component.html b/libs/ui/src/lib/data-provider-credits/data-provider-credits.component.html
index 9214336200..fb72e72cd7 100644
--- a/libs/ui/src/lib/data-provider-credits/data-provider-credits.component.html
+++ b/libs/ui/src/lib/data-provider-credits/data-provider-credits.component.html
@@ -1,5 +1,5 @@
- Market data provided by
+ Market data provided by{{ ' ' }}
@for (
dataProviderInfo of dataProviderInfos;
track dataProviderInfo;
@@ -7,10 +7,7 @@
) {
{{
dataProviderInfo.name
- }}
- @if (!last) {
- ,
- }
+ }}{{ last ? '.' : ', ' }}
}
- .
From ad9a1d526b394c2460ae628fb60bbdd22b0bb25b Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Sat, 5 Sep 2026 15:27:40 +0200
Subject: [PATCH 22/50] Release 3.67.1 (#7827)
---
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 723e84a230..22ec996dae 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).
-## 3.67.0 - 2026-09-05
+## 3.67.1 - 2026-09-05
### Added
diff --git a/package-lock.json b/package-lock.json
index 39a02c7ad6..2b7b58fd87 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "ghostfolio",
- "version": "3.67.0",
+ "version": "3.67.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ghostfolio",
- "version": "3.67.0",
+ "version": "3.67.1",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {
diff --git a/package.json b/package.json
index c0439aefb0..9ee6a75be6 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "ghostfolio",
- "version": "3.67.0",
+ "version": "3.67.1",
"homepage": "https://ghostfol.io",
"license": "AGPL-3.0",
"repository": "https://github.com/ghostfolio/ghostfolio",
From 57c4f5cb223d8020126e91cd1c6852f2daf8fd54 Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Sun, 6 Sep 2026 09:41:47 +0200
Subject: [PATCH 23/50] Task/improve data provider credits component (part 2)
(#7831)
Improve data provider credits
---
.../transform-data-source-in-response.interceptor.ts | 4 ++--
.../holding-detail-dialog/holding-detail-dialog.html | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
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 3b9addf76c..31bfb620ed 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
@@ -70,6 +70,8 @@ export class TransformDataSourceInResponseInterceptor<
}
}
+ data.dataProviderInfo = undefined;
+
if (Object.keys(valueMap).length === 0) {
return data;
}
@@ -94,8 +96,6 @@ export class TransformDataSourceInResponseInterceptor<
'watchlist[*].dataSource'
]
});
-
- data.dataProviderInfo = undefined;
}
return data;
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 51a0783fc3..dff4c2a1d9 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
@@ -358,7 +358,7 @@
- @if (dataProviderInfo) {
+ @if (dataProviderInfo?.name) {
Date: Sun, 6 Sep 2026 09:58:06 +0200
Subject: [PATCH 24/50] Task/improve handling of excluded holdings (#7828)
* Improve handling of excluded holdings
* Update changelog
---
CHANGELOG.md | 6 +
.../app/activities/activities.service.spec.ts | 20 +++
.../src/app/activities/activities.service.ts | 7 +-
.../portfolio-calculator.factory.ts | 6 +
.../calculator/portfolio-calculator.ts | 10 +-
.../portfolio-calculator-valuable.spec.ts | 6 +-
.../src/app/portfolio/portfolio.controller.ts | 6 +-
.../app/portfolio/portfolio.service.spec.ts | 146 +++++++++++++++++-
.../src/app/portfolio/portfolio.service.ts | 53 +++++--
.../activities-table.component.ts | 21 +--
10 files changed, 245 insertions(+), 36 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 22ec996dae..992af40479 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
+
+- Made the details of holdings excluded from analysis accessible via the activities table
+
## 3.67.1 - 2026-09-05
### Added
diff --git a/apps/api/src/app/activities/activities.service.spec.ts b/apps/api/src/app/activities/activities.service.spec.ts
index 4e935efb3e..72d1950294 100644
--- a/apps/api/src/app/activities/activities.service.spec.ts
+++ b/apps/api/src/app/activities/activities.service.spec.ts
@@ -216,6 +216,26 @@ describe('ActivitiesService', () => {
});
});
+ it('includes excluded accounts and activities when requested', async () => {
+ jest.spyOn(activitiesService, 'getActivities').mockResolvedValue({
+ activities: [],
+ count: 0
+ });
+
+ await activitiesService.getActivitiesForPortfolioCalculator({
+ userCurrency: 'USD',
+ userId: 'user-id',
+ withExcludedAccountsAndActivities: true
+ });
+
+ expect(activitiesService.getActivities).toHaveBeenCalledWith({
+ filters: undefined,
+ userCurrency: 'USD',
+ userId: 'user-id',
+ withExcludedAccountsAndActivities: true
+ });
+ });
+
it('does not adjust synthetic cash activities', async () => {
const activity = createActivity({ symbol: 'AAPL' });
const cashActivity = createActivity({
diff --git a/apps/api/src/app/activities/activities.service.ts b/apps/api/src/app/activities/activities.service.ts
index a0d9ffef57..7f6cbc8b0c 100644
--- a/apps/api/src/app/activities/activities.service.ts
+++ b/apps/api/src/app/activities/activities.service.ts
@@ -759,7 +759,8 @@ export class ActivitiesService {
filters,
userCurrency,
userId,
- withCash = false
+ withCash = false,
+ withExcludedAccountsAndActivities = false
}: {
/** Optional filters to apply to the activities. */
filters?: Filter[];
@@ -769,13 +770,15 @@ export class ActivitiesService {
userId: string;
/** Whether to include cash activities in the result. */
withCash?: boolean;
+ /** Whether to include activities that are excluded from analysis. */
+ withExcludedAccountsAndActivities?: boolean;
}) {
const [activities, splits] = await Promise.all([
this.getActivities({
filters,
userCurrency,
userId,
- withExcludedAccountsAndActivities: false // TODO
+ withExcludedAccountsAndActivities
}),
this.assetProfileSplitService.getSplitsByUserId({ userId })
]);
diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.factory.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.factory.ts
index 7b5ab1a0df..7d95988c1f 100644
--- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.factory.ts
+++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.factory.ts
@@ -34,6 +34,7 @@ export class PortfolioCalculatorFactory {
calculationType,
currency,
filters = [],
+ usePortfolioSnapshotCache = true,
userId
}: {
accountBalanceItems?: HistoricalDataItem[];
@@ -41,6 +42,7 @@ export class PortfolioCalculatorFactory {
calculationType: PerformanceCalculationType;
currency: string;
filters?: Filter[];
+ usePortfolioSnapshotCache?: boolean;
userId: string;
}): PortfolioCalculator {
switch (calculationType) {
@@ -50,6 +52,7 @@ export class PortfolioCalculatorFactory {
activities,
currency,
filters,
+ usePortfolioSnapshotCache,
userId,
configurationService: this.configurationService,
currentRateService: this.currentRateService,
@@ -64,6 +67,7 @@ export class PortfolioCalculatorFactory {
activities,
currency,
filters,
+ usePortfolioSnapshotCache,
userId,
configurationService: this.configurationService,
currentRateService: this.currentRateService,
@@ -78,6 +82,7 @@ export class PortfolioCalculatorFactory {
activities,
currency,
filters,
+ usePortfolioSnapshotCache,
userId,
configurationService: this.configurationService,
currentRateService: this.currentRateService,
@@ -92,6 +97,7 @@ export class PortfolioCalculatorFactory {
activities,
currency,
filters,
+ usePortfolioSnapshotCache,
userId,
configurationService: this.configurationService,
currentRateService: this.currentRateService,
diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
index 971d8b5642..eb3d05a6ba 100644
--- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
+++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
@@ -90,6 +90,7 @@ export abstract class PortfolioCalculator {
private snapshotPromise: Promise;
private startDate: Date;
private transactionPoints: TransactionPoint[];
+ private usePortfolioSnapshotCache: boolean;
private userId: string;
public constructor({
@@ -102,6 +103,7 @@ export abstract class PortfolioCalculator {
filters,
portfolioSnapshotService,
redisCacheService,
+ usePortfolioSnapshotCache = true,
userId
}: {
accountBalanceItems: HistoricalDataItem[];
@@ -113,6 +115,7 @@ export abstract class PortfolioCalculator {
filters: Filter[];
portfolioSnapshotService: PortfolioSnapshotService;
redisCacheService: RedisCacheService;
+ usePortfolioSnapshotCache?: boolean;
userId: string;
}) {
this.accountBalanceItems = accountBalanceItems;
@@ -175,6 +178,7 @@ export abstract class PortfolioCalculator {
this.portfolioSnapshotService = portfolioSnapshotService;
this.redisCacheService = redisCacheService;
+ this.usePortfolioSnapshotCache = usePortfolioSnapshotCache;
this.userId = userId;
const { endDate, startDate } = getIntervalFromDateRange({
@@ -187,7 +191,11 @@ export abstract class PortfolioCalculator {
this.computeTransactionPoints();
- this.snapshotPromise = this.initialize();
+ this.snapshotPromise = this.usePortfolioSnapshotCache
+ ? this.initialize()
+ : this.computeSnapshot().then((snapshot) => {
+ this.snapshot = snapshot;
+ });
// Mark the rejection as handled to prevent an unhandled promise rejection
// in case the snapshot promise is never awaited. Consumers awaiting it
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 ce5f90f5c3..3681bc7547 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
@@ -108,10 +108,11 @@ describe('PortfolioCalculator', () => {
activities,
calculationType: PerformanceCalculationType.ROAI,
currency: 'USD',
+ usePortfolioSnapshotCache: false,
userId: userDummyData.id
});
- const portfolioSnapshot = await portfolioCalculator.computeSnapshot();
+ const portfolioSnapshot = await portfolioCalculator.getSnapshot();
expect(portfolioSnapshot).toMatchObject({
currentValueInBaseCurrency: new Big('500000'),
@@ -169,6 +170,9 @@ describe('PortfolioCalculator', () => {
totalInvestmentValueWithCurrencyEffect: 500000
})
);
+
+ expect(PortfolioSnapshotServiceMock.jobsStore.size).toBe(0);
+ expect(RedisCacheServiceMock.cache.size).toBe(0);
});
});
});
diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts
index 431a8da20b..a65186100a 100644
--- a/apps/api/src/app/portfolio/portfolio.controller.ts
+++ b/apps/api/src/app/portfolio/portfolio.controller.ts
@@ -357,7 +357,8 @@ export class PortfolioController {
const holding = await this.portfolioService.getHolding({
dataSource,
symbol,
- userId
+ userId,
+ withExcludedActivities: true
});
if (!holding) {
@@ -631,7 +632,8 @@ export class PortfolioController {
const holding = await this.portfolioService.getHolding({
dataSource,
symbol,
- userId
+ userId,
+ withExcludedActivities: true
});
if (!holding) {
diff --git a/apps/api/src/app/portfolio/portfolio.service.spec.ts b/apps/api/src/app/portfolio/portfolio.service.spec.ts
index 227262c400..aed14e090f 100644
--- a/apps/api/src/app/portfolio/portfolio.service.spec.ts
+++ b/apps/api/src/app/portfolio/portfolio.service.spec.ts
@@ -9,9 +9,14 @@ import { ConfigurationService } from '@ghostfolio/api/services/configuration/con
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
-import { TAG_ID_EMERGENCY_FUND, UNKNOWN_KEY } from '@ghostfolio/common/config';
+import {
+ TAG_ID_EMERGENCY_FUND,
+ TAG_ID_EXCLUDE_FROM_ANALYSIS,
+ UNKNOWN_KEY
+} from '@ghostfolio/common/config';
import { parseDate } from '@ghostfolio/common/helper';
import {
+ Activity,
AssetProfileIdentifier,
Filter,
PortfolioSummary
@@ -490,6 +495,145 @@ describe('PortfolioService', () => {
});
});
+ describe('getHolding', () => {
+ const dataSource = DataSource.YAHOO;
+ const symbol = 'AAPL';
+ const includedActivity = {
+ assetProfile: { dataSource, symbol },
+ tags: []
+ } as Activity;
+
+ beforeEach(() => {
+ jest.spyOn(userService, 'user').mockResolvedValue({
+ id: userDummyData.id,
+ settings: { settings: { baseCurrency: 'USD' } }
+ } as unknown as Awaited>);
+
+ jest
+ .spyOn(symbolProfileService, 'getSymbolProfiles')
+ .mockResolvedValue([]);
+
+ jest
+ .spyOn(portfolioCalculatorFactory, 'createCalculator')
+ .mockReturnValue({
+ getSnapshot: jest.fn().mockResolvedValue({ positions: [] }),
+ getTransactionPoints: jest.fn().mockReturnValue([])
+ } as unknown as PortfolioCalculator);
+ });
+
+ it('keeps the cached path when the holding has included and excluded activities', async () => {
+ const excludedActivity = {
+ ...includedActivity,
+ tags: [{ id: TAG_ID_EXCLUDE_FROM_ANALYSIS }]
+ } as Activity;
+ const getActivities = jest
+ .spyOn(activitiesService, 'getActivitiesForPortfolioCalculator')
+ .mockResolvedValueOnce({ activities: [includedActivity], count: 1 })
+ .mockResolvedValue({
+ activities: [includedActivity, excludedActivity],
+ count: 2
+ });
+
+ await portfolioService.getHolding({
+ dataSource,
+ symbol,
+ userId: userDummyData.id,
+ withExcludedActivities: true
+ });
+
+ expect(getActivities).toHaveBeenCalledTimes(1);
+ expect(getActivities).toHaveBeenCalledWith({
+ userCurrency: 'USD',
+ userId: userDummyData.id
+ });
+ expect(portfolioCalculatorFactory.createCalculator).toHaveBeenCalledWith(
+ expect.objectContaining({
+ activities: [includedActivity],
+ filters: undefined,
+ usePortfolioSnapshotCache: true
+ })
+ );
+ });
+
+ it.each([
+ {
+ account: { tags: [{ id: TAG_ID_EXCLUDE_FROM_ANALYSIS }] },
+ name: 'account',
+ tags: []
+ },
+ {
+ account: { tags: [] },
+ name: 'activity tag',
+ tags: [{ id: TAG_ID_EXCLUDE_FROM_ANALYSIS }]
+ }
+ ])(
+ 'uses the direct path for a holding excluded by its $name',
+ async ({ account, tags }) => {
+ const excludedActivity = {
+ ...includedActivity,
+ account,
+ tags
+ } as Activity;
+
+ const getActivities = jest
+ .spyOn(activitiesService, 'getActivitiesForPortfolioCalculator')
+ .mockResolvedValueOnce({ activities: [], count: 0 })
+ .mockResolvedValueOnce({
+ activities: [excludedActivity],
+ count: 1
+ });
+
+ await portfolioService.getHolding({
+ dataSource,
+ symbol,
+ userId: userDummyData.id,
+ withExcludedActivities: true
+ });
+
+ expect(getActivities).toHaveBeenCalledTimes(2);
+ expect(getActivities).toHaveBeenLastCalledWith({
+ filters: [
+ { id: dataSource, type: 'DATA_SOURCE' },
+ { id: symbol, type: 'SYMBOL' }
+ ],
+ userCurrency: 'USD',
+ userId: userDummyData.id,
+ withExcludedAccountsAndActivities: true
+ });
+ expect(
+ portfolioCalculatorFactory.createCalculator
+ ).toHaveBeenCalledWith(
+ expect.objectContaining({
+ activities: [excludedActivity],
+ filters: [
+ { id: dataSource, type: 'DATA_SOURCE' },
+ { id: symbol, type: 'SYMBOL' }
+ ],
+ usePortfolioSnapshotCache: false
+ })
+ );
+ }
+ );
+
+ it('does not load excluded activities by default', async () => {
+ const getActivities = jest
+ .spyOn(activitiesService, 'getActivitiesForPortfolioCalculator')
+ .mockResolvedValue({ activities: [], count: 0 });
+
+ const holding = await portfolioService.getHolding({
+ dataSource,
+ symbol,
+ userId: userDummyData.id
+ });
+
+ expect(holding).toBeUndefined();
+ expect(getActivities).toHaveBeenCalledTimes(1);
+ expect(
+ portfolioCalculatorFactory.createCalculator
+ ).not.toHaveBeenCalled();
+ });
+ });
+
describe('getSummary', () => {
const getSummary = (args: object) => {
return (
diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts
index 9f81754f40..19c2dd7948 100644
--- a/apps/api/src/app/portfolio/portfolio.service.ts
+++ b/apps/api/src/app/portfolio/portfolio.service.ts
@@ -888,20 +888,47 @@ export class PortfolioService {
public async getHolding({
dataSource,
symbol,
- userId
+ userId,
+ withExcludedActivities = false
}: {
userId: string;
+ withExcludedActivities?: boolean;
} & AssetProfileIdentifier): Promise {
const user = await this.userService.user({ id: userId });
const userCurrency = this.getUserCurrency(user);
- const { activities } =
+ const holdingFilters: Filter[] = [
+ { id: dataSource, type: 'DATA_SOURCE' },
+ { id: symbol, type: 'SYMBOL' }
+ ];
+
+ let { activities } =
await this.activitiesService.getActivitiesForPortfolioCalculator({
userCurrency,
userId
});
- if (activities.length === 0) {
+ let hasActivitiesOfHolding = activities.some(({ assetProfile }) => {
+ return (
+ assetProfile.dataSource === dataSource && assetProfile.symbol === symbol
+ );
+ });
+
+ const isExcludedHolding = withExcludedActivities && !hasActivitiesOfHolding;
+
+ if (isExcludedHolding) {
+ ({ activities } =
+ await this.activitiesService.getActivitiesForPortfolioCalculator({
+ userCurrency,
+ userId,
+ filters: holdingFilters,
+ withExcludedAccountsAndActivities: true
+ }));
+
+ hasActivitiesOfHolding = activities.length > 0;
+ }
+
+ if (!hasActivitiesOfHolding) {
return undefined;
}
@@ -927,7 +954,9 @@ export class PortfolioService {
activities,
userId,
calculationType: this.getUserPerformanceCalculationType(user),
- currency: userCurrency
+ currency: userCurrency,
+ filters: isExcludedHolding ? holdingFilters : undefined,
+ usePortfolioSnapshotCache: !isExcludedHolding
});
const transactionPoints = portfolioCalculator.getTransactionPoints();
@@ -2033,12 +2062,7 @@ export class PortfolioService {
const nonExcludedActivities: Activity[] = [];
for (const activity of activities) {
- if (
- (activity.account && isAccountExcluded(activity.account)) ||
- activity.tags?.some(({ id }) => {
- return id === TAG_ID_EXCLUDE_FROM_ANALYSIS;
- })
- ) {
+ if (this.isExcludedFromAnalysis(activity)) {
excludedActivities.push(activity);
} else {
nonExcludedActivities.push(activity);
@@ -2418,4 +2442,13 @@ export class PortfolioService {
return { accounts, platforms };
}
+
+ private isExcludedFromAnalysis(activity: Activity) {
+ return (
+ isAccountExcluded(activity.account) ||
+ activity.tags?.some(({ id }) => {
+ return id === TAG_ID_EXCLUDE_FROM_ANALYSIS;
+ }) === true
+ );
+ }
}
diff --git a/libs/ui/src/lib/activities-table/activities-table.component.ts b/libs/ui/src/lib/activities-table/activities-table.component.ts
index acf17c933a..4646606577 100644
--- a/libs/ui/src/lib/activities-table/activities-table.component.ts
+++ b/libs/ui/src/lib/activities-table/activities-table.component.ts
@@ -1,13 +1,6 @@
-import {
- DEFAULT_PAGE_SIZE,
- TAG_ID_EXCLUDE_FROM_ANALYSIS
-} from '@ghostfolio/common/config';
+import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config';
import { ConfirmationDialogType } from '@ghostfolio/common/enums';
-import {
- getLocale,
- isAccountExcluded,
- isDraftActivity
-} from '@ghostfolio/common/helper';
+import { getLocale, isDraftActivity } from '@ghostfolio/common/helper';
import {
Activity,
AssetProfileIdentifier
@@ -286,7 +279,6 @@ export class GfActivitiesTableComponent implements AfterViewInit, OnInit {
public canClickActivity(activity: Activity) {
return (
this.hasPermissionToOpenDetails &&
- this.isExcludedFromAnalysis(activity) === false &&
isDraftActivity(activity) === false &&
['BUY', 'DIVIDEND', 'SELL'].includes(activity.type)
);
@@ -306,15 +298,6 @@ export class GfActivitiesTableComponent implements AfterViewInit, OnInit {
);
}
- public isExcludedFromAnalysis(activity: Activity) {
- return (
- isAccountExcluded(activity.account) ||
- activity.tags?.some(({ id }) => {
- return id === TAG_ID_EXCLUDE_FROM_ANALYSIS;
- }) === true
- );
- }
-
public onChangePage(page: PageEvent) {
this.pageChanged.emit(page);
}
From 7a6f54ab3b2d1a32d3676c0e391a21bb4b795d0f Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Sun, 6 Sep 2026 10:22:54 +0200
Subject: [PATCH 25/50] Feature/simplified mode in holdings table component
(#7830)
* Add simplified mode to holdings table component
* Update changelog
---
CHANGELOG.md | 4 ++++
.../holdings-table.component.html | 18 ++++++++++------
.../holdings-table.component.stories.ts | 15 +++++++++++++
.../holdings-table.component.ts | 21 +++++++++++++++++--
4 files changed, 50 insertions(+), 8 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 992af40479..19baa4cc3b 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 a simplified mode to the holdings table component
+
### Changed
- Made the details of holdings excluded from analysis accessible via the activities table
diff --git a/libs/ui/src/lib/holdings-table/holdings-table.component.html b/libs/ui/src/lib/holdings-table/holdings-table.component.html
index e66dac725a..2ed9484780 100644
--- a/libs/ui/src/lib/holdings-table/holdings-table.component.html
+++ b/libs/ui/src/lib/holdings-table/holdings-table.component.html
@@ -3,9 +3,9 @@
class="gf-table w-100"
mat-table
matSort
- matSortActive="allocationInPercentage"
- matSortDirection="desc"
[dataSource]="dataSource"
+ [matSortActive]="sortActive()"
+ [matSortDirection]="sortDirection()"
>
|
@@ -34,9 +34,11 @@
({{ element.assetProfile.assetSubClassLabel }})
}
-
- {{ element.assetProfile.symbol }}
-
+ @if (mode() === 'default') {
+
+ {{ element.assetProfile.symbol }}
+
+ }
|
@@ -181,7 +183,11 @@
-