import { AccessService } from '@ghostfolio/api/app/access/access.service'; import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service'; import { UserService } from '@ghostfolio/api/app/user/user.service'; import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { DEFAULT_CURRENCY } from '@ghostfolio/common/config'; import { getSum } from '@ghostfolio/common/helper'; import { PublicPortfolioResponse } from '@ghostfolio/common/interfaces'; import type { RequestWithUser } from '@ghostfolio/common/types'; import { Controller, Get, HttpException, Inject, Param, UseInterceptors } from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; import { Big } from 'big.js'; import { StatusCodes, getReasonPhrase } from 'http-status-codes'; @Controller('public') export class PublicController { public constructor( private readonly accessService: AccessService, private readonly configurationService: ConfigurationService, private readonly exchangeRateDataService: ExchangeRateDataService, private readonly portfolioService: PortfolioService, @Inject(REQUEST) private readonly request: RequestWithUser, private readonly userService: UserService ) {} @Get(':accessId/portfolio') @UseInterceptors(TransformDataSourceInResponseInterceptor) public async getPublicPortfolio( @Param('accessId') accessId ): Promise { const access = await this.accessService.access({ id: accessId }); if (!access) { throw new HttpException( getReasonPhrase(StatusCodes.NOT_FOUND), StatusCodes.NOT_FOUND ); } let hasDetails = true; const user = await this.userService.user({ id: access.userId }); if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) { hasDetails = user.subscription.type === 'Premium'; } const [ { holdings, markets }, { performance: performance1d }, { performance: performanceMax }, { performance: performanceYtd } ] = await Promise.all([ this.portfolioService.getDetails({ impersonationId: access.userId, userId: user.id, withMarkets: true }), ...['1d', 'max', 'ytd'].map((dateRange) => { return this.portfolioService.getPerformance({ dateRange, impersonationId: undefined, userId: user.id }); }) ]); Object.values(markets ?? {}).forEach((market) => { delete market.valueInBaseCurrency; }); const publicPortfolioResponse: PublicPortfolioResponse = { hasDetails, markets, alias: access.alias, createdAt: performance1d.createdAt, holdings: {}, performance: { '1d': { relativeChange: performance1d.netPerformancePercentageWithCurrencyEffect }, max: { relativeChange: performanceMax.netPerformancePercentageWithCurrencyEffect }, ytd: { relativeChange: performanceYtd.netPerformancePercentageWithCurrencyEffect } } }; const totalValue = getSum( Object.values(holdings).map(({ currency, marketPrice, quantity }) => { return new Big( this.exchangeRateDataService.toCurrency( quantity * marketPrice, currency, this.request.user?.Settings?.settings.baseCurrency ?? DEFAULT_CURRENCY ) ); }) ).toNumber(); for (const [symbol, portfolioPosition] of Object.entries(holdings)) { publicPortfolioResponse.holdings[symbol] = { allocationInPercentage: portfolioPosition.valueInBaseCurrency / totalValue, assetClass: hasDetails ? portfolioPosition.assetClass : undefined, countries: hasDetails ? portfolioPosition.countries : [], currency: hasDetails ? portfolioPosition.currency : undefined, dataSource: portfolioPosition.dataSource, dateOfFirstActivity: portfolioPosition.dateOfFirstActivity, markets: hasDetails ? portfolioPosition.markets : undefined, name: portfolioPosition.name, netPerformancePercentWithCurrencyEffect: portfolioPosition.netPerformancePercentWithCurrencyEffect, sectors: hasDetails ? portfolioPosition.sectors : [], symbol: portfolioPosition.symbol, url: portfolioPosition.url, valueInPercentage: portfolioPosition.valueInBaseCurrency / totalValue }; } return publicPortfolioResponse; } }