ghostfolio/apps/api/src/app/portfolio/portfolio.controller.ts

406 lines
11 KiB
TypeScript
Raw Normal View History

import { AccessService } from '@ghostfolio/api/app/access/access.service';
import { UserService } from '@ghostfolio/api/app/user/user.service';
import {
hasNotDefinedValuesInObject,
nullifyValuesInObject
} from '@ghostfolio/api/helper/object.helper';
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
import { baseCurrency } from '@ghostfolio/common/config';
import {
PortfolioChart,
PortfolioDetails,
PortfolioPerformance,
PortfolioPublicDetails,
PortfolioReport,
PortfolioSummary
} from '@ghostfolio/common/interfaces';
2021-08-01 09:41:44 +02:00
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
import type { RequestWithUser } from '@ghostfolio/common/types';
2021-04-13 21:53:58 +02:00
import {
Controller,
Get,
Headers,
HttpException,
Inject,
Param,
Query,
Res,
UseGuards
} from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { Response } from 'express';
2021-07-28 15:15:31 +02:00
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
2021-04-13 21:53:58 +02:00
import { PortfolioPositionDetail } from './interfaces/portfolio-position-detail.interface';
2021-07-20 22:52:50 +02:00
import { PortfolioPositions } from './interfaces/portfolio-positions.interface';
2021-04-13 21:53:58 +02:00
import { PortfolioService } from './portfolio.service';
@Controller('portfolio')
export class PortfolioController {
public constructor(
private readonly accessService: AccessService,
private readonly configurationService: ConfigurationService,
2021-04-13 21:53:58 +02:00
private readonly exchangeRateDataService: ExchangeRateDataService,
private readonly portfolioService: PortfolioService,
@Inject(REQUEST) private readonly request: RequestWithUser,
private readonly userService: UserService
2021-04-13 21:53:58 +02:00
) {}
@Get('investments')
2021-04-13 21:53:58 +02:00
@UseGuards(AuthGuard('jwt'))
public async findAll(
@Headers('impersonation-id') impersonationId,
@Res() res: Response
): Promise<InvestmentItem[]> {
if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
this.request.user.subscription.type === 'Basic'
) {
res.status(StatusCodes.FORBIDDEN);
return <any>res.json([]);
}
let investments = await this.portfolioService.getInvestments(
impersonationId
);
2021-04-13 21:53:58 +02:00
if (
impersonationId ||
this.userService.isRestrictedView(this.request.user)
) {
const maxInvestment = investments.reduce(
(investment, item) => Math.max(investment, item.investment),
1
);
2021-04-13 21:53:58 +02:00
investments = investments.map((item) => ({
date: item.date,
investment: item.investment / maxInvestment
}));
2021-04-13 21:53:58 +02:00
}
return <any>res.json(investments);
2021-04-13 21:53:58 +02:00
}
@Get('chart')
@UseGuards(AuthGuard('jwt'))
public async getChart(
@Headers('impersonation-id') impersonationId,
@Query('range') range,
@Res() res: Response
): Promise<PortfolioChart> {
const historicalDataContainer = await this.portfolioService.getChart(
2021-04-13 21:53:58 +02:00
impersonationId,
range
);
let chartData = historicalDataContainer.items;
2021-04-13 21:53:58 +02:00
let hasNullValue = false;
chartData.forEach((chartDataItem) => {
if (hasNotDefinedValuesInObject(chartDataItem)) {
hasNullValue = true;
}
});
if (hasNullValue) {
res.status(StatusCodes.ACCEPTED);
}
if (
impersonationId ||
this.userService.isRestrictedView(this.request.user)
) {
2021-04-13 21:53:58 +02:00
let maxValue = 0;
chartData.forEach((portfolioItem) => {
if (portfolioItem.value > maxValue) {
maxValue = portfolioItem.value;
}
});
chartData = chartData.map((historicalDataItem) => {
return {
...historicalDataItem,
marketPrice: Number((historicalDataItem.value / maxValue).toFixed(2))
};
});
}
return <any>res.json({
chart: chartData,
isAllTimeHigh: historicalDataContainer.isAllTimeHigh,
isAllTimeLow: historicalDataContainer.isAllTimeLow
});
2021-04-13 21:53:58 +02:00
}
@Get('details')
@UseGuards(AuthGuard('jwt'))
public async getDetails(
@Headers('impersonation-id') impersonationId,
@Query('range') range,
@Res() res: Response
): Promise<PortfolioDetails> {
if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
this.request.user.subscription.type === 'Basic'
) {
res.status(StatusCodes.FORBIDDEN);
return <any>res.json({ accounts: {}, holdings: {} });
}
const { accounts, holdings, hasErrors } =
await this.portfolioService.getDetails(
impersonationId,
this.request.user.id,
range
);
2021-04-13 21:53:58 +02:00
if (hasErrors || hasNotDefinedValuesInObject(holdings)) {
2021-04-13 21:53:58 +02:00
res.status(StatusCodes.ACCEPTED);
}
if (
impersonationId ||
this.userService.isRestrictedView(this.request.user)
) {
const totalInvestment = Object.values(holdings)
2021-04-13 21:53:58 +02:00
.map((portfolioPosition) => {
return portfolioPosition.investment;
})
.reduce((a, b) => a + b, 0);
const totalValue = Object.values(holdings)
2021-04-13 21:53:58 +02:00
.map((portfolioPosition) => {
return this.exchangeRateDataService.toCurrency(
portfolioPosition.quantity * portfolioPosition.marketPrice,
portfolioPosition.currency,
this.request.user.Settings.currency
);
})
.reduce((a, b) => a + b, 0);
for (const [symbol, portfolioPosition] of Object.entries(holdings)) {
2021-04-13 21:53:58 +02:00
portfolioPosition.grossPerformance = null;
portfolioPosition.investment =
portfolioPosition.investment / totalInvestment;
portfolioPosition.netPerformance = null;
2021-04-13 21:53:58 +02:00
portfolioPosition.quantity = null;
portfolioPosition.value = portfolioPosition.value / totalValue;
2021-04-13 21:53:58 +02:00
}
for (const [name, { current, original }] of Object.entries(accounts)) {
accounts[name].current = current / totalValue;
accounts[name].original = original / totalInvestment;
}
2021-04-13 21:53:58 +02:00
}
return <any>res.json({ accounts, holdings });
2021-04-13 21:53:58 +02:00
}
@Get('performance')
@UseGuards(AuthGuard('jwt'))
public async getPerformance(
@Headers('impersonation-id') impersonationId,
@Query('range') range,
@Res() res: Response
): Promise<PortfolioPerformance> {
const performanceInformation = await this.portfolioService.getPerformance(
impersonationId,
range
2021-04-13 21:53:58 +02:00
);
2021-07-27 22:54:00 +02:00
if (performanceInformation?.hasErrors) {
res.status(StatusCodes.ACCEPTED);
}
let performance = performanceInformation.performance;
if (
impersonationId ||
this.userService.isRestrictedView(this.request.user)
) {
2021-04-13 21:53:58 +02:00
performance = nullifyValuesInObject(performance, [
'currentGrossPerformance',
'currentValue'
]);
}
return <any>res.json(performance);
}
2021-07-20 22:52:50 +02:00
@Get('positions')
@UseGuards(AuthGuard('jwt'))
public async getPositions(
2021-07-24 10:53:15 +02:00
@Headers('impersonation-id') impersonationId,
2021-07-27 12:01:35 +02:00
@Query('range') range,
@Res() res: Response
2021-07-20 22:52:50 +02:00
): Promise<PortfolioPositions> {
2021-07-27 12:01:35 +02:00
const result = await this.portfolioService.getPositions(
impersonationId,
range
);
if (result?.hasErrors) {
res.status(StatusCodes.ACCEPTED);
}
if (
impersonationId ||
this.userService.isRestrictedView(this.request.user)
) {
result.positions = result.positions.map((position) => {
return nullifyValuesInObject(position, [
'grossPerformance',
'investment',
'netPerformance',
'quantity'
]);
});
}
2021-07-27 12:01:35 +02:00
return <any>res.json(result);
2021-07-20 22:52:50 +02:00
}
@Get('public/:accessId')
public async getPublic(
@Param('accessId') accessId,
@Res() res: Response
): Promise<PortfolioPublicDetails> {
const access = await this.accessService.access({ id: accessId });
const user = await this.userService.user({
id: access.userId
});
if (!access) {
res.status(StatusCodes.NOT_FOUND);
return <any>res.json({ accounts: {}, holdings: {} });
}
let hasDetails = true;
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
hasDetails = user.subscription.type === 'Premium';
}
const { holdings } = await this.portfolioService.getDetails(
access.userId,
access.userId
);
const portfolioPublicDetails: PortfolioPublicDetails = {
hasDetails,
holdings: {}
};
const totalValue = Object.values(holdings)
.filter((holding) => {
return holding.assetClass === 'EQUITY';
})
.map((portfolioPosition) => {
return this.exchangeRateDataService.toCurrency(
portfolioPosition.quantity * portfolioPosition.marketPrice,
portfolioPosition.currency,
this.request.user?.Settings?.currency ?? baseCurrency
);
})
.reduce((a, b) => a + b, 0);
for (const [symbol, portfolioPosition] of Object.entries(holdings)) {
if (portfolioPosition.assetClass === 'EQUITY') {
portfolioPublicDetails.holdings[symbol] = {
allocationCurrent: portfolioPosition.allocationCurrent,
countries: hasDetails ? portfolioPosition.countries : [],
currency: portfolioPosition.currency,
name: portfolioPosition.name,
sectors: hasDetails ? portfolioPosition.sectors : [],
value: portfolioPosition.value / totalValue
};
}
}
return <any>res.json(portfolioPublicDetails);
}
@Get('summary')
@UseGuards(AuthGuard('jwt'))
public async getSummary(
@Headers('impersonation-id') impersonationId
): Promise<PortfolioSummary> {
let summary = await this.portfolioService.getSummary(impersonationId);
if (
impersonationId ||
this.userService.isRestrictedView(this.request.user)
) {
summary = nullifyValuesInObject(summary, [
'cash',
'committedFunds',
'currentGrossPerformance',
'currentNetPerformance',
'currentValue',
'fees',
'netWorth',
'totalBuy',
'totalSell'
]);
}
return summary;
}
2021-04-13 21:53:58 +02:00
@Get('position/:symbol')
@UseGuards(AuthGuard('jwt'))
public async getPosition(
@Headers('impersonation-id') impersonationId,
@Param('symbol') symbol
): Promise<PortfolioPositionDetail> {
let position = await this.portfolioService.getPosition(
impersonationId,
symbol
);
if (position) {
if (
impersonationId ||
this.userService.isRestrictedView(this.request.user)
) {
position = nullifyValuesInObject(position, [
'grossPerformance',
'investment',
'netPerformance',
'quantity',
'value'
]);
2021-04-13 21:53:58 +02:00
}
return position;
}
throw new HttpException(
getReasonPhrase(StatusCodes.NOT_FOUND),
StatusCodes.NOT_FOUND
);
}
@Get('report')
@UseGuards(AuthGuard('jwt'))
public async getReport(
@Headers('impersonation-id') impersonationId,
@Res() res: Response
2021-04-13 21:53:58 +02:00
): Promise<PortfolioReport> {
if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
this.request.user.subscription.type === 'Basic'
) {
res.status(StatusCodes.FORBIDDEN);
return <any>res.json({ rules: [] });
}
return <any>(
res.json(await this.portfolioService.getReport(impersonationId))
);
2021-04-13 21:53:58 +02:00
}
}