From def15e544a7bf73d9f46d8f13e2d25d311d0c605 Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Thu, 16 Jul 2026 17:53:12 +0200
Subject: [PATCH 1/4] Bugfix/liabilities without market data (#7345)
* Fix liabilities without market data
* Update changelog
---
CHANGELOG.md | 7 ++
.../roai/portfolio-calculator-fee.spec.ts | 2 +-
.../calculator/roai/portfolio-calculator.ts | 76 +++++++++++--------
3 files changed, 53 insertions(+), 32 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 76727fefd..894e914a9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,13 @@ 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
+
+### Fixed
+
+- Fixed an issue where the dividends, the interest and the liabilities of asset profiles without market data have been valued at zero in the portfolio calculation
+- Fixed an issue where an error has been reported for asset profiles without market data which do not hold any units
+
## 3.27.0 - 2026-07-15
### Changed
diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-fee.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-fee.spec.ts
index e5798818f..4e413c0c5 100644
--- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-fee.spec.ts
+++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-fee.spec.ts
@@ -113,7 +113,7 @@ describe('PortfolioCalculator', () => {
expect(portfolioSnapshot).toMatchObject({
currentValueInBaseCurrency: new Big('0'),
errors: [],
- hasErrors: true,
+ hasErrors: false,
positions: [],
totalFeesWithCurrencyEffect: new Big('49'),
totalInterestWithCurrencyEffect: new Big('0'),
diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts
index f79e5bc79..747fca7a5 100644
--- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts
+++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts
@@ -236,6 +236,36 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
};
}
+ // The dividends, the interest and the liabilities are derived from the
+ // activities only. Accumulate them upfront so that they survive the bail
+ // out for symbols without a market price below.
+ for (const order of orders) {
+ const exchangeRateAtOrderDate = exchangeRates[order.date];
+
+ if (order.type === 'DIVIDEND') {
+ const dividend = order.quantity.mul(order.unitPrice);
+
+ totalDividend = totalDividend.plus(dividend);
+ totalDividendInBaseCurrency = totalDividendInBaseCurrency.plus(
+ dividend.mul(exchangeRateAtOrderDate ?? 1)
+ );
+ } else if (order.type === 'INTEREST') {
+ const interest = order.quantity.mul(order.unitPrice);
+
+ totalInterest = totalInterest.plus(interest);
+ totalInterestInBaseCurrency = totalInterestInBaseCurrency.plus(
+ interest.mul(exchangeRateAtOrderDate ?? 1)
+ );
+ } else if (order.type === 'LIABILITY') {
+ const liabilities = order.quantity.mul(order.unitPrice);
+
+ totalLiabilities = totalLiabilities.plus(liabilities);
+ totalLiabilitiesInBaseCurrency = totalLiabilitiesInBaseCurrency.plus(
+ liabilities.mul(exchangeRateAtOrderDate ?? 1)
+ );
+ }
+ }
+
const dateOfFirstTransaction = new Date(orders[0].date);
const endDateString = format(end, DATE_FORMAT);
@@ -263,7 +293,20 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
!unitPriceAtEndDate ||
(!unitPriceAtStartDate && isBefore(dateOfFirstTransaction, start))
) {
+ // A missing market price can only affect the units which are held. The
+ // dividends, the interest and the liabilities do not hold any units and
+ // are therefore not in error.
+ const hasActivitiesWithUnits = orders.some(({ type }) => {
+ return ['BUY', 'SELL'].includes(type);
+ });
+
return {
+ totalDividend,
+ totalDividendInBaseCurrency,
+ totalInterest,
+ totalInterestInBaseCurrency,
+ totalLiabilities,
+ totalLiabilitiesInBaseCurrency,
currentValues: {},
currentValuesWithCurrencyEffect: {},
feesWithCurrencyEffect: new Big(0),
@@ -271,7 +314,7 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
grossPerformancePercentage: new Big(0),
grossPerformancePercentageWithCurrencyEffect: new Big(0),
grossPerformanceWithCurrencyEffect: new Big(0),
- hasErrors: true,
+ hasErrors: hasActivitiesWithUnits,
initialValue: new Big(0),
initialValueWithCurrencyEffect: new Big(0),
investmentValuesAccumulated: {},
@@ -288,14 +331,8 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
timeWeightedInvestmentValuesWithCurrencyEffect: {},
timeWeightedInvestmentWithCurrencyEffect: new Big(0),
totalAccountBalanceInBaseCurrency: new Big(0),
- totalDividend: new Big(0),
- totalDividendInBaseCurrency: new Big(0),
- totalInterest: new Big(0),
- totalInterestInBaseCurrency: new Big(0),
totalInvestment: new Big(0),
- totalInvestmentWithCurrencyEffect: new Big(0),
- totalLiabilities: new Big(0),
- totalLiabilitiesInBaseCurrency: new Big(0)
+ totalInvestmentWithCurrencyEffect: new Big(0)
};
}
@@ -415,29 +452,6 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
const exchangeRateAtOrderDate = exchangeRates[order.date];
- if (order.type === 'DIVIDEND') {
- const dividend = order.quantity.mul(order.unitPrice);
-
- totalDividend = totalDividend.plus(dividend);
- totalDividendInBaseCurrency = totalDividendInBaseCurrency.plus(
- dividend.mul(exchangeRateAtOrderDate ?? 1)
- );
- } else if (order.type === 'INTEREST') {
- const interest = order.quantity.mul(order.unitPrice);
-
- totalInterest = totalInterest.plus(interest);
- totalInterestInBaseCurrency = totalInterestInBaseCurrency.plus(
- interest.mul(exchangeRateAtOrderDate ?? 1)
- );
- } else if (order.type === 'LIABILITY') {
- const liabilities = order.quantity.mul(order.unitPrice);
-
- totalLiabilities = totalLiabilities.plus(liabilities);
- totalLiabilitiesInBaseCurrency = totalLiabilitiesInBaseCurrency.plus(
- liabilities.mul(exchangeRateAtOrderDate ?? 1)
- );
- }
-
if (order.itemType === 'start') {
// Take the unit price of the order as the market price if there are no
// orders of this symbol before the start date
From 8caf48d5831f3cd175d297085b1cb8d56892f974 Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Thu, 16 Jul 2026 18:14:26 +0200
Subject: [PATCH 2/4] Task/extend self-hosting section with FAQ link (#7052)
Extend self-hosting section with FAQ link
---
README.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/README.md b/README.md
index ce89412a3..473d5c367 100644
--- a/README.md
+++ b/README.md
@@ -77,6 +77,8 @@ The frontend is built with [Angular](https://angular.dev) and uses [Angular Mate
We provide official container images hosted on [Docker Hub](https://hub.docker.com/r/ghostfolio/ghostfolio) for `linux/amd64`, `linux/arm/v7` and `linux/arm64`.
+Find answers to commonly asked questions about self-hosting Ghostfolio in our [Frequently Asked Questions (FAQ)](https://ghostfol.io/en/faq/self-hosting) section.
+
[

](https://www.buymeacoffee.com/ghostfolio)
From 05b9cd2cc4e65bb5b7edf9f95b7a7686729bdd1e Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Thu, 16 Jul 2026 18:20:36 +0200
Subject: [PATCH 3/4] Bugfix/preserve tags when updating activity (#7347)
* Validate tags when creating or updating an activity
* Fix loss of activity tags when updating an activity fails
* Update changelog
---
CHANGELOG.md | 3 ++
.../account-balance/account-balance.module.ts | 3 +-
apps/api/src/app/account/account.module.ts | 2 +
apps/api/src/app/account/account.service.ts | 35 ++++------------
.../app/activities/activities.controller.ts | 1 +
.../src/app/activities/activities.module.ts | 2 +
.../src/app/activities/activities.service.ts | 42 ++++++++++++++-----
apps/api/src/app/endpoints/ai/ai.module.ts | 2 +
.../endpoints/benchmarks/benchmarks.module.ts | 2 +
.../src/app/endpoints/public/public.module.ts | 2 +
.../roai/portfolio-calculator-cash.spec.ts | 2 +
.../api/src/app/portfolio/portfolio.module.ts | 2 +
.../app/portfolio/portfolio.service.spec.ts | 2 +
apps/api/src/services/tag/tag.service.ts | 38 ++++++++++++++++-
libs/common/src/lib/dtos/create-order.dto.ts | 3 ++
libs/common/src/lib/dtos/update-order.dto.ts | 3 ++
16 files changed, 103 insertions(+), 41 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 894e914a9..da9855a75 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
+- Fixed the missing validation of the tags when creating or updating an activity
+- Fixed the missing validation of the tags when updating the tags of a holding
+- Fixed an issue where the tags of an activity were lost if updating the activity failed
- Fixed an issue where the dividends, the interest and the liabilities of asset profiles without market data have been valued at zero in the portfolio calculation
- Fixed an issue where an error has been reported for asset profiles without market data which do not hold any units
diff --git a/apps/api/src/app/account-balance/account-balance.module.ts b/apps/api/src/app/account-balance/account-balance.module.ts
index 02323acc9..f7b1efc51 100644
--- a/apps/api/src/app/account-balance/account-balance.module.ts
+++ b/apps/api/src/app/account-balance/account-balance.module.ts
@@ -1,6 +1,7 @@
import { AccountService } from '@ghostfolio/api/app/account/account.service';
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
+import { TagModule } from '@ghostfolio/api/services/tag/tag.module';
import { Module } from '@nestjs/common';
@@ -10,7 +11,7 @@ import { AccountBalanceService } from './account-balance.service';
@Module({
controllers: [AccountBalanceController],
exports: [AccountBalanceService],
- imports: [ExchangeRateDataModule, PrismaModule],
+ imports: [ExchangeRateDataModule, PrismaModule, TagModule],
providers: [AccountBalanceService, AccountService]
})
export class AccountBalanceModule {}
diff --git a/apps/api/src/app/account/account.module.ts b/apps/api/src/app/account/account.module.ts
index 253c7fb1d..47b859ba3 100644
--- a/apps/api/src/app/account/account.module.ts
+++ b/apps/api/src/app/account/account.module.ts
@@ -7,6 +7,7 @@ import { ConfigurationModule } from '@ghostfolio/api/services/configuration/conf
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
import { ImpersonationModule } from '@ghostfolio/api/services/impersonation/impersonation.module';
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
+import { TagModule } from '@ghostfolio/api/services/tag/tag.module';
import { Module } from '@nestjs/common';
@@ -25,6 +26,7 @@ import { AccountService } from './account.service';
PortfolioModule,
PrismaModule,
RedactValuesInResponseModule,
+ TagModule,
UserModule
],
providers: [AccountService]
diff --git a/apps/api/src/app/account/account.service.ts b/apps/api/src/app/account/account.service.ts
index d1f4009a9..f84f085a3 100644
--- a/apps/api/src/app/account/account.service.ts
+++ b/apps/api/src/app/account/account.service.ts
@@ -3,10 +3,11 @@ import { PortfolioChangedEvent } from '@ghostfolio/api/events/portfolio-changed.
import { WHERE_ACCOUNT_NOT_EXCLUDED } from '@ghostfolio/api/helper/account.helper';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
+import { TagService } from '@ghostfolio/api/services/tag/tag.service';
import { DATE_FORMAT } from '@ghostfolio/common/helper';
import { Filter } from '@ghostfolio/common/interfaces';
-import { HttpException, Injectable } from '@nestjs/common';
+import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import {
Account,
@@ -19,7 +20,6 @@ import {
} from '@prisma/client';
import { Big } from 'big.js';
import { format } from 'date-fns';
-import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { groupBy } from 'lodash';
import { CashDetails } from './interfaces/cash-details.interface';
@@ -30,7 +30,8 @@ export class AccountService {
private readonly accountBalanceService: AccountBalanceService,
private readonly eventEmitter: EventEmitter2,
private readonly exchangeRateDataService: ExchangeRateDataService,
- private readonly prismaService: PrismaService
+ private readonly prismaService: PrismaService,
+ private readonly tagService: TagService
) {}
public async account({
@@ -127,7 +128,7 @@ export class AccountService {
aUserId: string,
tagIds?: string[]
): Promise
{
- await this.validateTagIds(tagIds, aUserId);
+ await this.tagService.validateTagIds({ tagIds, userId: aUserId });
const account = await this.prismaService.account.create({
data: {
@@ -259,15 +260,15 @@ export class AccountService {
public async updateAccount(
params: {
- where: Prisma.AccountWhereUniqueInput;
data: Prisma.AccountUpdateInput;
+ where: Prisma.AccountWhereUniqueInput;
},
aUserId: string,
tagIds?: string[]
): Promise {
const { data, where } = params;
- await this.validateTagIds(tagIds, aUserId);
+ await this.tagService.validateTagIds({ tagIds, userId: aUserId });
const account = await this.prismaService.account.update({
data: {
@@ -340,26 +341,4 @@ export class AccountService {
});
}
}
-
- private async validateTagIds(tagIds: string[], userId: string) {
- if (!tagIds?.length) {
- return;
- }
-
- const uniqueTagIds = Array.from(new Set(tagIds));
-
- const tagsCount = await this.prismaService.tag.count({
- where: {
- id: { in: uniqueTagIds },
- OR: [{ userId }, { userId: null }]
- }
- });
-
- if (tagsCount !== uniqueTagIds.length) {
- throw new HttpException(
- getReasonPhrase(StatusCodes.BAD_REQUEST),
- StatusCodes.BAD_REQUEST
- );
- }
- }
}
diff --git a/apps/api/src/app/activities/activities.controller.ts b/apps/api/src/app/activities/activities.controller.ts
index d00dd7292..f0f3a2acd 100644
--- a/apps/api/src/app/activities/activities.controller.ts
+++ b/apps/api/src/app/activities/activities.controller.ts
@@ -338,6 +338,7 @@ export class ActivitiesController {
}),
user: { connect: { id: this.request.user.id } }
},
+ userId: this.request.user.id,
where: {
id
}
diff --git a/apps/api/src/app/activities/activities.module.ts b/apps/api/src/app/activities/activities.module.ts
index 661163ff1..34091ba5e 100644
--- a/apps/api/src/app/activities/activities.module.ts
+++ b/apps/api/src/app/activities/activities.module.ts
@@ -14,6 +14,7 @@ import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-da
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { DataGatheringQueueModule } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.module';
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
+import { TagModule } from '@ghostfolio/api/services/tag/tag.module';
import { Module } from '@nestjs/common';
@@ -36,6 +37,7 @@ import { ActivitiesService } from './activities.service';
RedactValuesInResponseModule,
RedisCacheModule,
SymbolProfileModule,
+ TagModule,
TransformDataSourceInRequestModule,
TransformDataSourceInResponseModule
],
diff --git a/apps/api/src/app/activities/activities.service.ts b/apps/api/src/app/activities/activities.service.ts
index 427cddf4c..539c05c66 100644
--- a/apps/api/src/app/activities/activities.service.ts
+++ b/apps/api/src/app/activities/activities.service.ts
@@ -12,6 +12,7 @@ import { MarketDataService } from '@ghostfolio/api/services/market-data/market-d
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { DataGatheringService } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
+import { TagService } from '@ghostfolio/api/services/tag/tag.service';
import {
DATA_GATHERING_QUEUE_PRIORITY_HIGH,
GATHER_ASSET_PROFILE_PROCESS_JOB_NAME,
@@ -61,7 +62,8 @@ export class ActivitiesService {
private readonly exchangeRateDataService: ExchangeRateDataService,
private readonly marketDataService: MarketDataService,
private readonly prismaService: PrismaService,
- private readonly symbolProfileService: SymbolProfileService
+ private readonly symbolProfileService: SymbolProfileService,
+ private readonly tagService: TagService
) {}
public areCashActivitiesExcludedByFilters(filters: Filter[] = []) {
@@ -107,6 +109,13 @@ export class ActivitiesService {
tags,
userId
}: { tags: Tag[]; userId: string } & AssetProfileIdentifier) {
+ await this.tagService.validateTagIds({
+ userId,
+ tagIds: tags.map(({ id }) => {
+ return id;
+ })
+ });
+
const activities = await this.prismaService.order.findMany({
where: {
userId,
@@ -153,6 +162,15 @@ export class ActivitiesService {
userId: string;
}
): Promise {
+ const tags = data.tags ?? [];
+
+ await this.tagService.validateTagIds({
+ tagIds: tags.map(({ id }) => {
+ return id;
+ }),
+ userId: data.userId
+ });
+
let account: Prisma.AccountCreateNestedOneWithoutActivitiesInput;
if (data.accountId) {
@@ -167,7 +185,6 @@ export class ActivitiesService {
}
const accountId = data.accountId;
- const tags = data.tags ?? [];
const updateAccountBalance = data.updateAccountBalance ?? false;
const userId = data.userId;
@@ -932,6 +949,7 @@ export class ActivitiesService {
public async updateActivity({
data,
+ userId,
where
}: {
data: Prisma.OrderUpdateInput & {
@@ -942,14 +960,22 @@ export class ActivitiesService {
tags?: { id: string }[];
type?: ActivityType;
};
+ userId: string;
where: Prisma.OrderWhereUniqueInput;
}): Promise {
+ const tags = data.tags ?? [];
+
+ await this.tagService.validateTagIds({
+ userId,
+ tagIds: tags.map(({ id }) => {
+ return id;
+ })
+ });
+
if (!data.comment) {
data.comment = null;
}
- const tags = data.tags ?? [];
-
let isDraft = false;
if (
@@ -989,19 +1015,13 @@ export class ActivitiesService {
delete data.symbol;
delete data.tags;
- // Remove existing tags
- await this.prismaService.order.update({
- where,
- data: { tags: { set: [] } }
- });
-
const activity = await this.prismaService.order.update({
where,
data: {
...data,
isDraft,
tags: {
- connect: tags
+ set: tags
}
}
});
diff --git a/apps/api/src/app/endpoints/ai/ai.module.ts b/apps/api/src/app/endpoints/ai/ai.module.ts
index 5267f40c8..d5cf0e3e9 100644
--- a/apps/api/src/app/endpoints/ai/ai.module.ts
+++ b/apps/api/src/app/endpoints/ai/ai.module.ts
@@ -20,6 +20,7 @@ import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
import { PortfolioSnapshotQueueModule } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.module';
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
+import { TagModule } from '@ghostfolio/api/services/tag/tag.module';
import { Module } from '@nestjs/common';
@@ -44,6 +45,7 @@ import { AiService } from './ai.service';
PropertyModule,
RedisCacheModule,
SymbolProfileModule,
+ TagModule,
UserModule
],
providers: [
diff --git a/apps/api/src/app/endpoints/benchmarks/benchmarks.module.ts b/apps/api/src/app/endpoints/benchmarks/benchmarks.module.ts
index 2bcd6177d..3c540c337 100644
--- a/apps/api/src/app/endpoints/benchmarks/benchmarks.module.ts
+++ b/apps/api/src/app/endpoints/benchmarks/benchmarks.module.ts
@@ -23,6 +23,7 @@ import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
import { PortfolioSnapshotQueueModule } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.module';
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
+import { TagModule } from '@ghostfolio/api/services/tag/tag.module';
import { Module } from '@nestjs/common';
@@ -46,6 +47,7 @@ import { BenchmarksService } from './benchmarks.service';
RedisCacheModule,
SymbolModule,
SymbolProfileModule,
+ TagModule,
TransformDataSourceInRequestModule,
TransformDataSourceInResponseModule,
UserModule
diff --git a/apps/api/src/app/endpoints/public/public.module.ts b/apps/api/src/app/endpoints/public/public.module.ts
index e8395228f..b992694c5 100644
--- a/apps/api/src/app/endpoints/public/public.module.ts
+++ b/apps/api/src/app/endpoints/public/public.module.ts
@@ -18,6 +18,7 @@ import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-da
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { PortfolioSnapshotQueueModule } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.module';
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
+import { TagModule } from '@ghostfolio/api/services/tag/tag.module';
import { Module } from '@nestjs/common';
@@ -38,6 +39,7 @@ import { PublicController } from './public.controller';
PrismaModule,
RedisCacheModule,
SymbolProfileModule,
+ TagModule,
TransformDataSourceInRequestModule,
UserModule
],
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 589168989..31e451051 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
@@ -91,6 +91,7 @@ describe('PortfolioCalculator', () => {
accountBalanceService,
null,
exchangeRateDataService,
+ null,
null
);
@@ -122,6 +123,7 @@ describe('PortfolioCalculator', () => {
exchangeRateDataService,
null,
null,
+ null,
null
);
diff --git a/apps/api/src/app/portfolio/portfolio.module.ts b/apps/api/src/app/portfolio/portfolio.module.ts
index d818195ca..7f7a894df 100644
--- a/apps/api/src/app/portfolio/portfolio.module.ts
+++ b/apps/api/src/app/portfolio/portfolio.module.ts
@@ -20,6 +20,7 @@ import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { DataGatheringQueueModule } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.module';
import { PortfolioSnapshotQueueModule } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.module';
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
+import { TagModule } from '@ghostfolio/api/services/tag/tag.module';
import { Module } from '@nestjs/common';
@@ -50,6 +51,7 @@ import { RulesService } from './rules.service';
RedactValuesInResponseModule,
RedisCacheModule,
SymbolProfileModule,
+ TagModule,
TransformDataSourceInRequestModule,
TransformDataSourceInResponseModule,
UserModule
diff --git a/apps/api/src/app/portfolio/portfolio.service.spec.ts b/apps/api/src/app/portfolio/portfolio.service.spec.ts
index adddb4013..c16590cce 100644
--- a/apps/api/src/app/portfolio/portfolio.service.spec.ts
+++ b/apps/api/src/app/portfolio/portfolio.service.spec.ts
@@ -54,6 +54,7 @@ describe('PortfolioService', () => {
null,
null,
exchangeRateDataService,
+ null,
null
);
@@ -67,6 +68,7 @@ describe('PortfolioService', () => {
exchangeRateDataService,
null,
null,
+ null,
null
);
diff --git a/apps/api/src/services/tag/tag.service.ts b/apps/api/src/services/tag/tag.service.ts
index d967ce854..38fb90a69 100644
--- a/apps/api/src/services/tag/tag.service.ts
+++ b/apps/api/src/services/tag/tag.service.ts
@@ -1,7 +1,8 @@
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
-import { Injectable } from '@nestjs/common';
+import { HttpException, Injectable } from '@nestjs/common';
import { Prisma, Tag } from '@prisma/client';
+import { StatusCodes, getReasonPhrase } from 'http-status-codes';
@Injectable()
export class TagService {
@@ -124,4 +125,39 @@ export class TagService {
where
});
}
+
+ public async validateTagIds({
+ tagIds,
+ userId
+ }: {
+ tagIds: string[];
+ userId: string;
+ }) {
+ if (!tagIds?.length) {
+ return;
+ }
+
+ if (!userId) {
+ throw new HttpException(
+ getReasonPhrase(StatusCodes.BAD_REQUEST),
+ StatusCodes.BAD_REQUEST
+ );
+ }
+
+ const uniqueTagIds = Array.from(new Set(tagIds));
+
+ const tagsCount = await this.prismaService.tag.count({
+ where: {
+ id: { in: uniqueTagIds },
+ OR: [{ userId }, { userId: null }]
+ }
+ });
+
+ if (tagsCount !== uniqueTagIds.length) {
+ throw new HttpException(
+ getReasonPhrase(StatusCodes.BAD_REQUEST),
+ StatusCodes.BAD_REQUEST
+ );
+ }
+ }
}
diff --git a/libs/common/src/lib/dtos/create-order.dto.ts b/libs/common/src/lib/dtos/create-order.dto.ts
index 9236a12d8..36365b7be 100644
--- a/libs/common/src/lib/dtos/create-order.dto.ts
+++ b/libs/common/src/lib/dtos/create-order.dto.ts
@@ -4,6 +4,7 @@ import { IsCurrencyCode } from '@ghostfolio/common/validators/is-currency-code';
import { AssetClass, AssetSubClass, DataSource, Type } from '@prisma/client';
import { Transform, TransformFnParams } from 'class-transformer';
import {
+ ArrayUnique,
IsArray,
IsBoolean,
IsEnum,
@@ -62,8 +63,10 @@ export class CreateOrderDto {
@IsString()
symbol: string;
+ @ArrayUnique()
@IsArray()
@IsOptional()
+ @IsString({ each: true })
tags?: string[];
@IsEnum(Type)
diff --git a/libs/common/src/lib/dtos/update-order.dto.ts b/libs/common/src/lib/dtos/update-order.dto.ts
index 7843f142c..991479bc8 100644
--- a/libs/common/src/lib/dtos/update-order.dto.ts
+++ b/libs/common/src/lib/dtos/update-order.dto.ts
@@ -4,6 +4,7 @@ import { IsCurrencyCode } from '@ghostfolio/common/validators/is-currency-code';
import { AssetClass, AssetSubClass, DataSource, Type } from '@prisma/client';
import { Transform, TransformFnParams } from 'class-transformer';
import {
+ ArrayUnique,
IsArray,
IsEnum,
IsISO8601,
@@ -63,8 +64,10 @@ export class UpdateOrderDto {
@IsString()
symbol: string;
+ @ArrayUnique()
@IsArray()
@IsOptional()
+ @IsString({ each: true })
tags?: string[];
@IsString()
From 22d68e7ac16ad043e3492cd5dbdce01e7a0ad1f7 Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Thu, 16 Jul 2026 18:25:02 +0200
Subject: [PATCH 4/4] Bugfix/remove linked account from BUY / SELL / DIVIDEND
activity (#7346)
* Fix removing a linked account from a buy, sell or dividend activity
* Update changelog
---
CHANGELOG.md | 1 +
apps/api/src/app/activities/activities.controller.ts | 12 +++++++-----
apps/api/src/app/activities/activities.service.ts | 4 ----
3 files changed, 8 insertions(+), 9 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index da9855a75..b645fb0e7 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
- Fixed an issue where the tags of an activity were lost if updating the activity failed
- Fixed an issue where the dividends, the interest and the liabilities of asset profiles without market data have been valued at zero in the portfolio calculation
- Fixed an issue where an error has been reported for asset profiles without market data which do not hold any units
+- Fixed an issue with removing a linked account from a buy, sell or dividend activity
## 3.27.0 - 2026-07-15
diff --git a/apps/api/src/app/activities/activities.controller.ts b/apps/api/src/app/activities/activities.controller.ts
index f0f3a2acd..21dabc639 100644
--- a/apps/api/src/app/activities/activities.controller.ts
+++ b/apps/api/src/app/activities/activities.controller.ts
@@ -315,11 +315,13 @@ export class ActivitiesController {
data: {
...data,
date,
- account: {
- connect: {
- id_userId: { id: accountId, userId: this.request.user.id }
- }
- },
+ account: accountId
+ ? {
+ connect: {
+ id_userId: { id: accountId, userId: this.request.user.id }
+ }
+ }
+ : { disconnect: true },
SymbolProfile: {
connect: {
dataSource_symbol: {
diff --git a/apps/api/src/app/activities/activities.service.ts b/apps/api/src/app/activities/activities.service.ts
index 539c05c66..df616a5dd 100644
--- a/apps/api/src/app/activities/activities.service.ts
+++ b/apps/api/src/app/activities/activities.service.ts
@@ -983,10 +983,6 @@ export class ActivitiesService {
(data.SymbolProfile.connect.dataSource_symbol.dataSource === 'MANUAL' &&
data.type === 'BUY')
) {
- if (data.account?.connect?.id_userId?.id === null) {
- data.account = { disconnect: true };
- }
-
delete data.SymbolProfile.connect;
delete data.SymbolProfile.update.name;
} else {