mirror of https://github.com/ghostfolio/ghostfolio
committed by
GitHub
3 changed files with 221 additions and 209 deletions
@ -0,0 +1,213 @@ |
|||
import { AccessService } from '@ghostfolio/api/app/access/access.service'; |
|||
import { ActivitiesService } from '@ghostfolio/api/app/activities/activities.service'; |
|||
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service'; |
|||
import { UserService } from '@ghostfolio/api/app/user/user.service'; |
|||
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 { SubscriptionType } from '@ghostfolio/common/enums'; |
|||
import { getSum } from '@ghostfolio/common/helper'; |
|||
import { |
|||
AccessSettings, |
|||
PublicPortfolioResponse |
|||
} from '@ghostfolio/common/interfaces'; |
|||
|
|||
import { HttpException, Injectable } from '@nestjs/common'; |
|||
import { |
|||
AssetClass, |
|||
AssetSubClass, |
|||
Type as ActivityType |
|||
} from '@prisma/client'; |
|||
import { Big } from 'big.js'; |
|||
import { StatusCodes, getReasonPhrase } from 'http-status-codes'; |
|||
|
|||
@Injectable() |
|||
export class PublicService { |
|||
public constructor( |
|||
private readonly accessService: AccessService, |
|||
private readonly activitiesService: ActivitiesService, |
|||
private readonly configurationService: ConfigurationService, |
|||
private readonly exchangeRateDataService: ExchangeRateDataService, |
|||
private readonly portfolioService: PortfolioService, |
|||
private readonly userService: UserService |
|||
) {} |
|||
|
|||
public async getPublicPortfolio( |
|||
accessId: string |
|||
): Promise<PublicPortfolioResponse> { |
|||
const access = await this.accessService.access({ |
|||
granteeUserId: null, |
|||
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 === SubscriptionType.Premium; |
|||
} |
|||
|
|||
const { filters } = (access.settings ?? {}) as AccessSettings; |
|||
|
|||
const [ |
|||
{ createdAt, holdings, markets }, |
|||
{ performance: performance1d }, |
|||
{ performance: performanceMax }, |
|||
{ performance: performanceYtd } |
|||
] = await Promise.all([ |
|||
this.portfolioService.getDetails({ |
|||
filters, |
|||
userId: user.id, |
|||
withMarkets: true |
|||
}), |
|||
...['1d', 'max', 'ytd'].map((dateRange) => { |
|||
return this.portfolioService.getPerformance({ |
|||
dateRange, |
|||
filters, |
|||
userId: user.id |
|||
}); |
|||
}) |
|||
]); |
|||
|
|||
const { activities } = await this.activitiesService.getActivities({ |
|||
filters, |
|||
sortColumn: 'date', |
|||
sortDirection: 'desc', |
|||
take: 10, |
|||
types: [ActivityType.BUY, ActivityType.SELL], |
|||
userCurrency: user?.settings?.settings.baseCurrency ?? DEFAULT_CURRENCY, |
|||
userId: user.id, |
|||
withExcludedAccountsAndActivities: false |
|||
}); |
|||
|
|||
// Experimental
|
|||
const latestActivities = this.configurationService.get( |
|||
'ENABLE_FEATURE_SUBSCRIPTION' |
|||
) |
|||
? [] |
|||
: activities.map( |
|||
({ |
|||
assetProfile, |
|||
currency, |
|||
date, |
|||
fee, |
|||
quantity, |
|||
type, |
|||
unitPrice, |
|||
value, |
|||
valueInBaseCurrency |
|||
}) => { |
|||
return { |
|||
assetProfile, |
|||
currency, |
|||
date, |
|||
fee, |
|||
quantity, |
|||
type, |
|||
unitPrice, |
|||
value, |
|||
valueInBaseCurrency |
|||
}; |
|||
} |
|||
); |
|||
|
|||
Object.values(markets ?? {}).forEach((market) => { |
|||
delete market.valueInBaseCurrency; |
|||
}); |
|||
|
|||
const publicPortfolioResponse: PublicPortfolioResponse = { |
|||
createdAt, |
|||
hasDetails, |
|||
latestActivities, |
|||
markets, |
|||
alias: access.alias, |
|||
holdings: {}, |
|||
performance: { |
|||
'1d': { |
|||
relativeChange: |
|||
performance1d.netPerformancePercentageWithCurrencyEffect |
|||
}, |
|||
max: { |
|||
relativeChange: |
|||
performanceMax.netPerformancePercentageWithCurrencyEffect |
|||
}, |
|||
ytd: { |
|||
relativeChange: |
|||
performanceYtd.netPerformancePercentageWithCurrencyEffect |
|||
} |
|||
} |
|||
}; |
|||
|
|||
const totalValue = getSum( |
|||
Object.values(holdings).map(({ assetProfile, marketPrice, quantity }) => { |
|||
return new Big( |
|||
this.exchangeRateDataService.toCurrency( |
|||
quantity * marketPrice, |
|||
assetProfile.currency, |
|||
user?.settings?.settings.baseCurrency ?? DEFAULT_CURRENCY |
|||
) |
|||
); |
|||
}) |
|||
).toNumber(); |
|||
|
|||
for (const [symbol, portfolioPosition] of Object.entries(holdings)) { |
|||
publicPortfolioResponse.holdings[symbol] = { |
|||
allocationInPercentage: |
|||
portfolioPosition.valueInBaseCurrency / totalValue, |
|||
assetProfile: { |
|||
...portfolioPosition.assetProfile, |
|||
assetClass: |
|||
hasDetails || |
|||
portfolioPosition.assetProfile.assetClass === AssetClass.LIQUIDITY |
|||
? portfolioPosition.assetProfile.assetClass |
|||
: undefined, |
|||
assetClassLabel: |
|||
hasDetails || |
|||
portfolioPosition.assetProfile.assetClass === AssetClass.LIQUIDITY |
|||
? portfolioPosition.assetProfile.assetClassLabel |
|||
: undefined, |
|||
assetSubClass: |
|||
hasDetails || |
|||
portfolioPosition.assetProfile.assetSubClass === AssetSubClass.CASH |
|||
? portfolioPosition.assetProfile.assetSubClass |
|||
: undefined, |
|||
assetSubClassLabel: |
|||
hasDetails || |
|||
portfolioPosition.assetProfile.assetSubClass === AssetSubClass.CASH |
|||
? portfolioPosition.assetProfile.assetSubClassLabel |
|||
: undefined, |
|||
holdings: portfolioPosition.assetProfile.holdings?.map( |
|||
({ allocationInPercentage, name }) => { |
|||
return { allocationInPercentage, name }; |
|||
} |
|||
), |
|||
...(hasDetails |
|||
? {} |
|||
: { |
|||
countries: [], |
|||
currency: undefined, |
|||
holdings: [], |
|||
sectors: [] |
|||
}) |
|||
}, |
|||
dateOfFirstActivity: portfolioPosition.dateOfFirstActivity, |
|||
markets: hasDetails ? portfolioPosition.markets : undefined, |
|||
netPerformancePercentWithCurrencyEffect: |
|||
portfolioPosition.netPerformancePercentWithCurrencyEffect, |
|||
valueInPercentage: portfolioPosition.valueInBaseCurrency / totalValue |
|||
}; |
|||
} |
|||
|
|||
return publicPortfolioResponse; |
|||
} |
|||
} |
|||
Loading…
Reference in new issue