From c975586ade736b1f9a24b33eda6e0e24adf7fc1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Mart=C3=ADn?= Date: Thu, 23 Oct 2025 17:58:13 +0200 Subject: [PATCH] Task/add comprehensive filter functionality to public portfolio retrieval --- .../app/endpoints/public/public.controller.ts | 114 ++++++++++-------- 1 file changed, 62 insertions(+), 52 deletions(-) diff --git a/apps/api/src/app/endpoints/public/public.controller.ts b/apps/api/src/app/endpoints/public/public.controller.ts index 82bb60ba6..d81fe5ce3 100644 --- a/apps/api/src/app/endpoints/public/public.controller.ts +++ b/apps/api/src/app/endpoints/public/public.controller.ts @@ -10,6 +10,7 @@ import { DEFAULT_CURRENCY } from '@ghostfolio/common/config'; import { getSum } from '@ghostfolio/common/helper'; import { AccessSettings, + Filter, PublicPortfolioResponse } from '@ghostfolio/common/interfaces'; import type { RequestWithUser } from '@ghostfolio/common/types'; @@ -23,7 +24,7 @@ import { UseInterceptors } from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; -import { Type as ActivityType } from '@prisma/client'; +import { Type as ActivityType, AssetSubClass } from '@prisma/client'; import { Big } from 'big.js'; import { StatusCodes, getReasonPhrase } from 'http-status-codes'; @@ -66,7 +67,52 @@ export class PublicController { // Get filter configuration from access settings const accessSettings = (access.settings ?? {}) as AccessSettings; - const filter = accessSettings.filter; + const accessFilter = accessSettings.filter; + + // Convert access filter to portfolio filters + const portfolioFilters: Filter[] = []; + + if (accessFilter) { + // Add account filters + if (accessFilter.accountIds && accessFilter.accountIds.length > 0) { + portfolioFilters.push( + ...accessFilter.accountIds.map((accountId) => ({ + id: accountId, + type: 'ACCOUNT' as const + })) + ); + } + + // Add asset class filters + if (accessFilter.assetClasses && accessFilter.assetClasses.length > 0) { + portfolioFilters.push( + ...accessFilter.assetClasses.map((assetClass) => ({ + id: assetClass, + type: 'ASSET_CLASS' as const + })) + ); + } + + // Add tag filters + if (accessFilter.tagIds && accessFilter.tagIds.length > 0) { + portfolioFilters.push( + ...accessFilter.tagIds.map((tagId) => ({ + id: tagId, + type: 'TAG' as const + })) + ); + } + + // Add holding filters (symbol + dataSource) + if (accessFilter.holdings && accessFilter.holdings.length > 0) { + portfolioFilters.push( + ...accessFilter.holdings.map((holding) => ({ + id: `${holding.dataSource}.${holding.symbol}`, + type: 'SYMBOL' as const + })) + ); + } + } const [ { createdAt, holdings, markets }, @@ -75,6 +121,7 @@ export class PublicController { { performance: performanceYtd } ] = await Promise.all([ this.portfolioService.getDetails({ + filters: portfolioFilters.length > 0 ? portfolioFilters : undefined, impersonationId: access.userId, userId: user.id, withMarkets: true @@ -82,53 +129,24 @@ export class PublicController { ...['1d', 'max', 'ytd'].map((dateRange) => { return this.portfolioService.getPerformance({ dateRange, + filters: portfolioFilters.length > 0 ? portfolioFilters : undefined, impersonationId: undefined, userId: user.id }); }) ]); - // Apply filter to holdings if configured - let filteredHoldings = holdings; - if (filter) { - filteredHoldings = Object.fromEntries( - Object.entries(holdings).filter(([, holding]) => { - // Filter by asset class - if ( - filter.assetClasses && - filter.assetClasses.length > 0 && - !filter.assetClasses.includes(holding.assetClass) - ) { - return false; - } - - // Filter by specific holdings (symbol + dataSource) - if (filter.holdings && filter.holdings.length > 0) { - const matchesHolding = filter.holdings.some( - (h) => - h.symbol === holding.symbol && - h.dataSource === holding.dataSource - ); - if (!matchesHolding) { - return false; - } - } - - // Filter by tags - check if holding has at least one of the filtered tags - if (filter.tagIds && filter.tagIds.length > 0) { - const holdingTagIds = holding.tags?.map((tag) => tag.id) ?? []; - const hasMatchingTag = filter.tagIds.some((tagId) => - holdingTagIds.includes(tagId) - ); - if (!hasMatchingTag) { - return false; - } - } - - return true; - }) - ); - } + // Filter out only the base currency cash holdings + const baseCurrency = + user.settings?.settings.baseCurrency ?? DEFAULT_CURRENCY; + const filteredHoldings = Object.fromEntries( + Object.entries(holdings).filter(([, holding]) => { + // Remove only cash holdings that match the base currency + const isCash = holding.assetSubClass === AssetSubClass.CASH; + const isBaseCurrency = holding.symbol === baseCurrency; + return !(isCash && isBaseCurrency); + }) + ); const { activities } = await this.orderService.getOrders({ includeDrafts: false, @@ -141,20 +159,12 @@ export class PublicController { withExcludedAccountsAndActivities: false }); - // Filter activities by account if filter is configured - let filteredActivities = activities; - if (filter?.accountIds && filter.accountIds.length > 0) { - filteredActivities = activities.filter((activity) => - filter.accountIds.includes(activity.accountId) - ); - } - // Experimental const latestActivities = this.configurationService.get( 'ENABLE_FEATURE_SUBSCRIPTION' ) ? [] - : filteredActivities.map( + : activities.map( ({ currency, date,