|
|
@ -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, |
|
|
|