From c4c3845ef2fb66c822ba704e409eb34dfb97baf1 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 4 Jul 2026 17:25:11 +0200 Subject: [PATCH] Bugfix/handling of cash positions in portfolio details with filters (#7215) * Fix handling of cash positions with filters * Update changelog --- CHANGELOG.md | 1 + .../src/app/activities/activities.service.ts | 78 +++++++++---------- .../src/app/portfolio/portfolio.service.ts | 40 +++------- 3 files changed, 52 insertions(+), 67 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7450c3e26..75c2887e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Resolved an issue in the treemap chart component when the holdings list is empty - Fixed the handling of cash positions in the portfolio calculations when filtering by holding or tag +- Fixed the handling of cash positions in the portfolio details when filtering - Fixed the market condition of the benchmarks in the twitter bot service when values round to zero ## 3.19.1 - 2026-07-03 diff --git a/apps/api/src/app/activities/activities.service.ts b/apps/api/src/app/activities/activities.service.ts index 87aa13146..816d2328d 100644 --- a/apps/api/src/app/activities/activities.service.ts +++ b/apps/api/src/app/activities/activities.service.ts @@ -63,6 +63,43 @@ export class ActivitiesService { private readonly symbolProfileService: SymbolProfileService ) {} + public areCashActivitiesExcludedByFilters(filters: Filter[] = []) { + const { + ASSET_CLASS: filtersByAssetClass = [], + DATA_SOURCE: [filterByDataSource] = [], + SYMBOL: [filterBySymbol] = [], + TAG: filtersByTag = [] + } = groupBy(filters, ({ type }) => { + return type; + }); + + const isFilteredByAssetClassOtherThanLiquidity = + filtersByAssetClass.length > 0 && + !filtersByAssetClass.some(({ id }) => { + return id === AssetClass.LIQUIDITY; + }); + + const isFilteredByAssetProfile = !!(filterByDataSource || filterBySymbol); + const isFilteredByTag = filtersByTag.length > 0; + + const isFilteredByUnsupportedType = filters.some(({ type }) => { + return ![ + 'ACCOUNT', + 'ASSET_CLASS', + 'DATA_SOURCE', + 'SYMBOL', + 'TAG' + ].includes(type); + }); + + return ( + isFilteredByAssetClassOtherThanLiquidity || + isFilteredByAssetProfile || + isFilteredByTag || + isFilteredByUnsupportedType + ); + } + public async assignTags({ dataSource, symbol, @@ -393,7 +430,7 @@ export class ActivitiesService { userCurrency: string; userId: string; }): Promise { - if (this.hasNonMatchingFiltersForCashActivities(filters)) { + if (this.areCashActivitiesExcludedByFilters(filters)) { return { activities: [], count: 0 @@ -800,7 +837,7 @@ export class ActivitiesService { withExcludedAccountsAndActivities: false // TODO }); - if (withCash && !this.hasNonMatchingFiltersForCashActivities(filters)) { + if (withCash && !this.areCashActivitiesExcludedByFilters(filters)) { const cashDetails = await this.accountService.getCashDetails({ filters, userId, @@ -935,43 +972,6 @@ export class ActivitiesService { return activity; } - private hasNonMatchingFiltersForCashActivities(filters: Filter[] = []) { - const { - ASSET_CLASS: filtersByAssetClass = [], - DATA_SOURCE: [filterByDataSource] = [], - SYMBOL: [filterBySymbol] = [], - TAG: filtersByTag = [] - } = groupBy(filters, ({ type }) => { - return type; - }); - - const isFilteredByAssetClassOtherThanLiquidity = - filtersByAssetClass.length > 0 && - !filtersByAssetClass.some(({ id }) => { - return id === AssetClass.LIQUIDITY; - }); - - const isFilteredByAssetProfile = !!(filterByDataSource || filterBySymbol); - const isFilteredByTag = filtersByTag.length > 0; - - const isFilteredByUnsupportedType = filters.some(({ type }) => { - return ![ - 'ACCOUNT', - 'ASSET_CLASS', - 'DATA_SOURCE', - 'SYMBOL', - 'TAG' - ].includes(type); - }); - - return ( - isFilteredByAssetClassOtherThanLiquidity || - isFilteredByAssetProfile || - isFilteredByTag || - isFilteredByUnsupportedType - ); - } - private async orders(params: { include?: Prisma.OrderInclude; skip?: number; diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index a09661448..b5ea9ba00 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -528,30 +528,18 @@ export class PortfolioService { const holdings: PortfolioDetails['holdings'] = {}; - const totalValueInBaseCurrency = currentValueInBaseCurrency.plus( - cashDetails.balanceInBaseCurrency - ); + const { + HOLDING_TYPE: [filterByHoldingType] = [], + TAG: [filterByTag] = [] + } = groupBy(filters, ({ type }) => { + return type; + }); + + const isFilteredByClosedHoldings = filterByHoldingType?.id === 'CLOSED'; + + let filteredValueInBaseCurrency = currentValueInBaseCurrency; - const isFilteredByAccount = - filters?.some(({ type }) => { - return type === 'ACCOUNT'; - }) ?? false; - - const isFilteredByClosedHoldings = - filters?.some(({ id, type }) => { - return id === 'CLOSED' && type === 'HOLDING_TYPE'; - }) ?? false; - - let filteredValueInBaseCurrency = isFilteredByAccount - ? totalValueInBaseCurrency - : currentValueInBaseCurrency; - - if ( - filters?.length === 0 || - (filters?.length === 1 && - filters[0].id === AssetClass.LIQUIDITY && - filters[0].type === 'ASSET_CLASS') - ) { + if (!this.activitiesService.areCashActivitiesExcludedByFilters(filters)) { filteredValueInBaseCurrency = filteredValueInBaseCurrency.plus( cashDetails.balanceInBaseCurrency ); @@ -703,11 +691,7 @@ export class PortfolioService { withExcludedAccounts }); - if ( - filters?.length === 1 && - filters[0].id === TAG_ID_EMERGENCY_FUND && - filters[0].type === 'TAG' - ) { + if (filters?.length === 1 && filterByTag?.id === TAG_ID_EMERGENCY_FUND) { const emergencyFundCashPositions = this.getCashPositions({ cashDetails, userCurrency,