Browse Source

Task/add comprehensive filter functionality to public portfolio retrieval

pull/5848/head
Germán Martín 1 week ago
parent
commit
c975586ade
  1. 114
      apps/api/src/app/endpoints/public/public.controller.ts

114
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 { 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,

Loading…
Cancel
Save