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