|  | @ -10,6 +10,7 @@ import { DEFAULT_CURRENCY } from '@ghostfolio/common/config'; | 
			
		
	
		
		
			
				
					|  |  | import { getSum } from '@ghostfolio/common/helper'; |  |  | import { getSum } from '@ghostfolio/common/helper'; | 
			
		
	
		
		
			
				
					|  |  | import { |  |  | import { | 
			
		
	
		
		
			
				
					|  |  |   AccessSettings, |  |  |   AccessSettings, | 
			
		
	
		
		
			
				
					|  |  |  |  |  |   Filter, | 
			
		
	
		
		
			
				
					|  |  |   PublicPortfolioResponse |  |  |   PublicPortfolioResponse | 
			
		
	
		
		
			
				
					|  |  | } from '@ghostfolio/common/interfaces'; |  |  | } from '@ghostfolio/common/interfaces'; | 
			
		
	
		
		
			
				
					|  |  | import type { RequestWithUser } from '@ghostfolio/common/types'; |  |  | import type { RequestWithUser } from '@ghostfolio/common/types'; | 
			
		
	
	
		
		
			
				
					|  | @ -23,7 +24,7 @@ import { | 
			
		
	
		
		
			
				
					|  |  |   UseInterceptors |  |  |   UseInterceptors | 
			
		
	
		
		
			
				
					|  |  | } from '@nestjs/common'; |  |  | } from '@nestjs/common'; | 
			
		
	
		
		
			
				
					|  |  | import { REQUEST } from '@nestjs/core'; |  |  | 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 { Big } from 'big.js'; | 
			
		
	
		
		
			
				
					|  |  | import { StatusCodes, getReasonPhrase } from 'http-status-codes'; |  |  | import { StatusCodes, getReasonPhrase } from 'http-status-codes'; | 
			
		
	
		
		
			
				
					|  |  | 
 |  |  | 
 | 
			
		
	
	
		
		
			
				
					|  | @ -66,7 +67,52 @@ export class PublicController { | 
			
		
	
		
		
			
				
					|  |  | 
 |  |  | 
 | 
			
		
	
		
		
			
				
					|  |  |     // Get filter configuration from access settings
 |  |  |     // Get filter configuration from access settings
 | 
			
		
	
		
		
			
				
					|  |  |     const accessSettings = (access.settings ?? {}) as AccessSettings; |  |  |     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 [ |  |  |     const [ | 
			
		
	
		
		
			
				
					|  |  |       { createdAt, holdings, markets }, |  |  |       { createdAt, holdings, markets }, | 
			
		
	
	
		
		
			
				
					|  | @ -75,6 +121,7 @@ export class PublicController { | 
			
		
	
		
		
			
				
					|  |  |       { performance: performanceYtd } |  |  |       { performance: performanceYtd } | 
			
		
	
		
		
			
				
					|  |  |     ] = await Promise.all([ |  |  |     ] = await Promise.all([ | 
			
		
	
		
		
			
				
					|  |  |       this.portfolioService.getDetails({ |  |  |       this.portfolioService.getDetails({ | 
			
		
	
		
		
			
				
					|  |  |  |  |  |         filters: portfolioFilters.length > 0 ? portfolioFilters : undefined, | 
			
		
	
		
		
			
				
					|  |  |         impersonationId: access.userId, |  |  |         impersonationId: access.userId, | 
			
		
	
		
		
			
				
					|  |  |         userId: user.id, |  |  |         userId: user.id, | 
			
		
	
		
		
			
				
					|  |  |         withMarkets: true |  |  |         withMarkets: true | 
			
		
	
	
		
		
			
				
					|  | @ -82,53 +129,24 @@ export class PublicController { | 
			
		
	
		
		
			
				
					|  |  |       ...['1d', 'max', 'ytd'].map((dateRange) => { |  |  |       ...['1d', 'max', 'ytd'].map((dateRange) => { | 
			
		
	
		
		
			
				
					|  |  |         return this.portfolioService.getPerformance({ |  |  |         return this.portfolioService.getPerformance({ | 
			
		
	
		
		
			
				
					|  |  |           dateRange, |  |  |           dateRange, | 
			
		
	
		
		
			
				
					|  |  |  |  |  |           filters: portfolioFilters.length > 0 ? portfolioFilters : undefined, | 
			
		
	
		
		
			
				
					|  |  |           impersonationId: undefined, |  |  |           impersonationId: undefined, | 
			
		
	
		
		
			
				
					|  |  |           userId: user.id |  |  |           userId: user.id | 
			
		
	
		
		
			
				
					|  |  |         }); |  |  |         }); | 
			
		
	
		
		
			
				
					|  |  |       }) |  |  |       }) | 
			
		
	
		
		
			
				
					|  |  |     ]); |  |  |     ]); | 
			
		
	
		
		
			
				
					|  |  | 
 |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |     // Apply filter to holdings if configured
 |  |  |     // Filter out only the base currency cash holdings
 | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |     let filteredHoldings = holdings; |  |  |     const baseCurrency = | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |     if (filter) { |  |  |       user.settings?.settings.baseCurrency ?? DEFAULT_CURRENCY; | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |       filteredHoldings = Object.fromEntries( |  |  |     const filteredHoldings = Object.fromEntries( | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |         Object.entries(holdings).filter(([, holding]) => { |  |  |       Object.entries(holdings).filter(([, holding]) => { | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |           // Filter by asset class
 |  |  |         // Remove only cash holdings that match the base currency
 | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |           if ( |  |  |         const isCash = holding.assetSubClass === AssetSubClass.CASH; | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |             filter.assetClasses && |  |  |         const isBaseCurrency = holding.symbol === baseCurrency; | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |             filter.assetClasses.length > 0 && |  |  |         return !(isCash && isBaseCurrency); | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |             !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; |  |  |  | 
			
		
	
		
		
			
				
					|  |  |         }) |  |  |  | 
			
		
	
		
		
			
				
					|  |  |       ); |  |  |  | 
			
		
	
		
		
			
				
					|  |  |     } |  |  |  | 
			
		
	
		
		
	
		
		
	
		
		
	
		
		
	
		
		
	
		
		
	
		
		
	
		
		
	
		
		
	
		
		
	
		
		
	
		
		
			
				
					|  |  | 
 |  |  | 
 | 
			
		
	
		
		
			
				
					|  |  |     const { activities } = await this.orderService.getOrders({ |  |  |     const { activities } = await this.orderService.getOrders({ | 
			
		
	
		
		
			
				
					|  |  |       includeDrafts: false, |  |  |       includeDrafts: false, | 
			
		
	
	
		
		
			
				
					|  | @ -141,20 +159,12 @@ export class PublicController { | 
			
		
	
		
		
			
				
					|  |  |       withExcludedAccountsAndActivities: false |  |  |       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
 |  |  |     // Experimental
 | 
			
		
	
		
		
			
				
					|  |  |     const latestActivities = this.configurationService.get( |  |  |     const latestActivities = this.configurationService.get( | 
			
		
	
		
		
			
				
					|  |  |       'ENABLE_FEATURE_SUBSCRIPTION' |  |  |       'ENABLE_FEATURE_SUBSCRIPTION' | 
			
		
	
		
		
			
				
					|  |  |     ) |  |  |     ) | 
			
		
	
		
		
			
				
					|  |  |       ? [] |  |  |       ? [] | 
			
		
	
		
		
			
				
					
					|  |  |       : filteredActivities.map( |  |  |       : activities.map( | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					|  |  |           ({ |  |  |           ({ | 
			
		
	
		
		
			
				
					|  |  |             currency, |  |  |             currency, | 
			
		
	
		
		
			
				
					|  |  |             date, |  |  |             date, | 
			
		
	
	
		
		
			
				
					|  | 
 |