You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

828 lines
21 KiB

import { CreateAccessDto } from '@ghostfolio/api/app/access/create-access.dto';
import { CreateAccountBalanceDto } from '@ghostfolio/api/app/account-balance/create-account-balance.dto';
import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto';
import { TransferBalanceDto } from '@ghostfolio/api/app/account/transfer-balance.dto';
import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto';
import { UpdateBulkMarketDataDto } from '@ghostfolio/api/app/admin/update-bulk-market-data.dto';
import { CreateTagDto } from '@ghostfolio/api/app/endpoints/tags/create-tag.dto';
import { UpdateTagDto } from '@ghostfolio/api/app/endpoints/tags/update-tag.dto';
import { CreateWatchlistItemDto } from '@ghostfolio/api/app/endpoints/watchlist/create-watchlist-item.dto';
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
import {
Activities,
Activity
} from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
import { SymbolItem } from '@ghostfolio/api/app/symbol/interfaces/symbol-item.interface';
import { DeleteOwnUserDto } from '@ghostfolio/api/app/user/delete-own-user.dto';
import { UserItem } from '@ghostfolio/api/app/user/interfaces/user-item.interface';
import { UpdateUserSettingDto } from '@ghostfolio/api/app/user/update-user-setting.dto';
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
import { PropertyDto } from '@ghostfolio/api/services/property/property.dto';
import { DATE_FORMAT } from '@ghostfolio/common/helper';
import {
Access,
AccessTokenResponse,
AccountBalancesResponse,
AccountsResponse,
AiPromptResponse,
ApiKeyResponse,
AssetProfileIdentifier,
BenchmarkMarketDataDetails,
BenchmarkResponse,
Export,
Filter,
ImportResponse,
InfoItem,
LookupResponse,
MarketDataDetailsResponse,
OAuthResponse,
PortfolioDetails,
PortfolioDividends,
PortfolioHoldingResponse,
PortfolioHoldingsResponse,
PortfolioInvestments,
PortfolioPerformanceResponse,
PortfolioReportResponse,
PublicPortfolioResponse,
User,
WatchlistResponse
} from '@ghostfolio/common/interfaces';
import { filterGlobalPermissions } from '@ghostfolio/common/permissions';
import type {
AccountWithValue,
AiPromptMode,
DateRange,
GroupBy
} from '@ghostfolio/common/types';
import { translate } from '@ghostfolio/ui/i18n';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { SortDirection } from '@angular/material/sort';
import { utc } from '@date-fns/utc';
import {
AccountBalance,
DataSource,
MarketData,
Order as OrderModel,
Tag
} from '@prisma/client';
import { format, parseISO } from 'date-fns';
import { cloneDeep, groupBy, isNumber } from 'lodash';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class DataService {
public constructor(private http: HttpClient) {}
public buildFiltersAsQueryParams({ filters }: { filters?: Filter[] }) {
let params = new HttpParams();
if (filters?.length > 0) {
const {
ACCOUNT: filtersByAccount,
ASSET_CLASS: filtersByAssetClass,
ASSET_SUB_CLASS: filtersByAssetSubClass,
DATA_SOURCE: [filterByDataSource] = [],
HOLDING_TYPE: filtersByHoldingType,
PRESET_ID: filtersByPresetId,
SEARCH_QUERY: filtersBySearchQuery,
SYMBOL: [filterBySymbol] = [],
TAG: filtersByTag
} = groupBy(filters, (filter) => {
return filter.type;
});
if (filterByDataSource) {
params = params.append('dataSource', filterByDataSource.id);
}
if (filterBySymbol) {
params = params.append('symbol', filterBySymbol.id);
}
if (filtersByAccount) {
params = params.append(
'accounts',
filtersByAccount
.map(({ id }) => {
return id;
})
.join(',')
);
}
if (filtersByAssetClass) {
params = params.append(
'assetClasses',
filtersByAssetClass
.map(({ id }) => {
return id;
})
.join(',')
);
}
if (filtersByAssetSubClass) {
params = params.append(
'assetSubClasses',
filtersByAssetSubClass
.map(({ id }) => {
return id;
})
.join(',')
);
}
if (filtersByHoldingType) {
params = params.append('holdingType', filtersByHoldingType[0].id);
}
if (filtersByPresetId) {
params = params.append('presetId', filtersByPresetId[0].id);
}
if (filtersBySearchQuery) {
params = params.append('query', filtersBySearchQuery[0].id);
}
if (filtersByTag) {
params = params.append(
'tags',
filtersByTag
.map(({ id }) => {
return id;
})
.join(',')
);
}
}
return params;
}
public createCheckoutSession({
couponId,
priceId
}: {
couponId?: string;
priceId: string;
}) {
return this.http.post('/api/v1/subscription/stripe/checkout-session', {
couponId,
priceId
});
}
public fetchAccount(aAccountId: string) {
return this.http.get<AccountWithValue>(`/api/v1/account/${aAccountId}`);
}
public fetchAccountBalances(aAccountId: string) {
return this.http.get<AccountBalancesResponse>(
`/api/v1/account/${aAccountId}/balances`
);
}
public fetchAccounts({ filters }: { filters?: Filter[] } = {}) {
const params = this.buildFiltersAsQueryParams({ filters });
return this.http.get<AccountsResponse>('/api/v1/account', { params });
}
public fetchActivities({
filters,
range,
skip,
sortColumn,
sortDirection,
take
}: {
filters?: Filter[];
range?: DateRange;
skip?: number;
sortColumn?: string;
sortDirection?: SortDirection;
take?: number;
}): Observable<Activities> {
let params = this.buildFiltersAsQueryParams({ filters });
if (range) {
params = params.append('range', range);
}
if (skip) {
params = params.append('skip', skip);
}
if (sortColumn) {
params = params.append('sortColumn', sortColumn);
}
if (sortDirection) {
params = params.append('sortDirection', sortDirection);
}
if (take) {
params = params.append('take', take);
}
return this.http.get<any>('/api/v1/order', { params }).pipe(
map(({ activities, count }) => {
for (const activity of activities) {
activity.createdAt = parseISO(activity.createdAt);
activity.date = parseISO(activity.date);
}
return { activities, count };
})
);
}
public fetchActivity(aActivityId: string) {
return this.http.get<Activity>(`/api/v1/order/${aActivityId}`).pipe(
map((activity) => {
activity.createdAt = parseISO(activity.createdAt as unknown as string);
activity.date = parseISO(activity.date as unknown as string);
return activity;
})
);
}
public fetchDividends({
filters,
groupBy = 'month',
range
}: {
filters?: Filter[];
groupBy?: GroupBy;
range: DateRange;
}) {
let params = this.buildFiltersAsQueryParams({ filters });
params = params.append('groupBy', groupBy);
params = params.append('range', range);
return this.http.get<PortfolioDividends>('/api/v1/portfolio/dividends', {
params
});
}
public fetchDividendsImport({ dataSource, symbol }: AssetProfileIdentifier) {
return this.http.get<ImportResponse>(
`/api/v1/import/dividends/${dataSource}/${symbol}`
);
}
public fetchExchangeRateForDate({
date,
symbol
}: {
date: Date;
symbol: string;
}) {
return this.http.get<IDataProviderHistoricalResponse>(
`/api/v1/exchange-rate/${symbol}/${format(date, DATE_FORMAT, { in: utc })}`
);
}
public deleteAccess(aId: string) {
return this.http.delete<any>(`/api/v1/access/${aId}`);
}
public deleteAccount(aId: string) {
return this.http.delete<any>(`/api/v1/account/${aId}`);
}
public deleteAccountBalance(aId: string) {
return this.http.delete<any>(`/api/v1/account-balance/${aId}`);
}
public deleteActivities({ filters }) {
const params = this.buildFiltersAsQueryParams({ filters });
return this.http.delete<any>('/api/v1/order', { params });
}
public deleteActivity(aId: string) {
return this.http.delete<any>(`/api/v1/order/${aId}`);
}
public deleteBenchmark({ dataSource, symbol }: AssetProfileIdentifier) {
return this.http.delete<any>(`/api/v1/benchmarks/${dataSource}/${symbol}`);
}
public deleteOwnUser(aData: DeleteOwnUserDto) {
return this.http.delete<any>(`/api/v1/user`, { body: aData });
}
public deleteTag(aId: string) {
return this.http.delete<void>(`/api/v1/tags/${aId}`);
}
public deleteUser(aId: string) {
return this.http.delete<any>(`/api/v1/user/${aId}`);
}
public deleteWatchlistItem({ dataSource, symbol }: AssetProfileIdentifier) {
return this.http.delete<any>(`/api/v1/watchlist/${dataSource}/${symbol}`);
}
public fetchAccesses() {
return this.http.get<Access[]>('/api/v1/access');
}
public fetchAsset({
dataSource,
symbol
}: AssetProfileIdentifier): Observable<MarketDataDetailsResponse> {
return this.http.get<any>(`/api/v1/asset/${dataSource}/${symbol}`).pipe(
map((data) => {
for (const item of data.marketData) {
item.date = parseISO(item.date);
}
return data;
})
);
}
public fetchBenchmarkForUser({
dataSource,
filters,
range,
startDate,
symbol,
withExcludedAccounts
}: {
filters?: Filter[];
range: DateRange;
startDate: Date;
withExcludedAccounts?: boolean;
} & AssetProfileIdentifier): Observable<BenchmarkMarketDataDetails> {
let params = this.buildFiltersAsQueryParams({ filters });
params = params.append('range', range);
if (withExcludedAccounts) {
params = params.append('withExcludedAccounts', withExcludedAccounts);
}
return this.http.get<BenchmarkMarketDataDetails>(
`/api/v1/benchmarks/${dataSource}/${symbol}/${format(startDate, DATE_FORMAT, { in: utc })}`,
{ params }
);
}
public fetchBenchmarks() {
return this.http.get<BenchmarkResponse>('/api/v1/benchmarks');
}
public fetchExport({
activityIds,
filters
}: {
activityIds?: string[];
filters?: Filter[];
} = {}) {
let params = this.buildFiltersAsQueryParams({ filters });
if (activityIds) {
params = params.append('activityIds', activityIds.join(','));
}
return this.http.get<Export>('/api/v1/export', {
params
});
}
public fetchHoldingDetail({
dataSource,
symbol
}: {
dataSource: DataSource;
symbol: string;
}) {
return this.http
.get<PortfolioHoldingResponse>(
`/api/v1/portfolio/holding/${dataSource}/${symbol}`
)
.pipe(
map((data) => {
if (data.activities) {
for (const order of data.activities) {
order.createdAt = parseISO(order.createdAt as unknown as string);
order.date = parseISO(order.date as unknown as string);
}
}
return data;
})
);
}
public fetchInfo(): InfoItem {
const info = cloneDeep((window as any).info);
const utmSource = window.localStorage.getItem('utm_source') as
| 'ios'
| 'trusted-web-activity';
info.globalPermissions = filterGlobalPermissions(
info.globalPermissions,
utmSource
);
return info;
}
public fetchInvestments({
filters,
groupBy = 'month',
range
}: {
filters?: Filter[];
groupBy?: GroupBy;
range: DateRange;
}) {
let params = this.buildFiltersAsQueryParams({ filters });
params = params.append('groupBy', groupBy);
params = params.append('range', range);
return this.http.get<PortfolioInvestments>(
'/api/v1/portfolio/investments',
{ params }
);
}
public fetchMarketDataBySymbol({
dataSource,
symbol
}: {
dataSource: DataSource;
symbol: string;
}): Observable<MarketDataDetailsResponse> {
return this.http
.get<any>(`/api/v1/market-data/${dataSource}/${symbol}`)
.pipe(
map((data) => {
for (const item of data.marketData) {
item.date = parseISO(item.date);
}
return data;
})
);
}
public fetchSymbolItem({
dataSource,
includeHistoricalData,
symbol
}: {
dataSource: DataSource | string;
includeHistoricalData?: number;
symbol: string;
}) {
let params = new HttpParams();
if (includeHistoricalData) {
params = params.append('includeHistoricalData', includeHistoricalData);
}
return this.http.get<SymbolItem>(`/api/v1/symbol/${dataSource}/${symbol}`, {
params
});
}
public fetchSymbols({
includeIndices = false,
query
}: {
includeIndices?: boolean;
query: string;
}) {
let params = new HttpParams().set('query', query);
if (includeIndices) {
params = params.append('includeIndices', includeIndices);
}
return this.http
.get<LookupResponse>('/api/v1/symbol/lookup', { params })
.pipe(
map(({ items }) => {
return items;
})
);
}
public fetchPortfolioDetails({
filters,
withMarkets = false
}: {
filters?: Filter[];
withMarkets?: boolean;
} = {}): Observable<PortfolioDetails> {
let params = this.buildFiltersAsQueryParams({ filters });
if (withMarkets) {
params = params.append('withMarkets', withMarkets);
}
return this.http
.get<any>('/api/v1/portfolio/details', {
params
})
.pipe(
map((response) => {
if (response.holdings) {
for (const symbol of Object.keys(response.holdings)) {
response.holdings[symbol].assetClassLabel = translate(
response.holdings[symbol].assetClass
);
response.holdings[symbol].assetSubClassLabel = translate(
response.holdings[symbol].assetSubClass
);
response.holdings[symbol].dateOfFirstActivity = response.holdings[
symbol
].dateOfFirstActivity
? parseISO(response.holdings[symbol].dateOfFirstActivity)
: undefined;
response.holdings[symbol].value = isNumber(
response.holdings[symbol].value
)
? response.holdings[symbol].value
: response.holdings[symbol].valueInPercentage;
}
}
return response;
})
);
}
public fetchPortfolioHoldings({
filters,
range
}: {
filters?: Filter[];
range?: DateRange;
} = {}) {
let params = this.buildFiltersAsQueryParams({ filters });
if (range) {
params = params.append('range', range);
}
return this.http
.get<PortfolioHoldingsResponse>('/api/v1/portfolio/holdings', {
params
})
.pipe(
map((response) => {
if (response.holdings) {
for (const symbol of Object.keys(response.holdings)) {
response.holdings[symbol].assetClassLabel = translate(
response.holdings[symbol].assetClass
);
response.holdings[symbol].assetSubClassLabel = translate(
response.holdings[symbol].assetSubClass
);
response.holdings[symbol].dateOfFirstActivity = response.holdings[
symbol
].dateOfFirstActivity
? parseISO(response.holdings[symbol].dateOfFirstActivity)
: undefined;
response.holdings[symbol].value = isNumber(
response.holdings[symbol].value
)
? response.holdings[symbol].value
: response.holdings[symbol].valueInPercentage;
}
}
return response;
})
);
}
public fetchPortfolioPerformance({
filters,
range,
withExcludedAccounts = false,
withItems = false
}: {
filters?: Filter[];
range: DateRange;
withExcludedAccounts?: boolean;
withItems?: boolean;
}): Observable<PortfolioPerformanceResponse> {
let params = this.buildFiltersAsQueryParams({ filters });
params = params.append('range', range);
if (withExcludedAccounts) {
params = params.append('withExcludedAccounts', withExcludedAccounts);
}
if (withItems) {
params = params.append('withItems', withItems);
}
return this.http
.get<any>(`/api/v2/portfolio/performance`, {
params
})
.pipe(
map((response) => {
if (response.firstOrderDate) {
response.firstOrderDate = parseISO(response.firstOrderDate);
}
return response;
})
);
}
public fetchPortfolioReport() {
return this.http.get<PortfolioReportResponse>('/api/v1/portfolio/report');
}
public fetchPrompt({
filters,
mode
}: {
filters?: Filter[];
mode: AiPromptMode;
}) {
const params = this.buildFiltersAsQueryParams({ filters });
return this.http.get<AiPromptResponse>(`/api/v1/ai/prompt/${mode}`, {
params
});
}
public fetchPublicPortfolio(aAccessId: string) {
return this.http
.get<PublicPortfolioResponse>(`/api/v1/public/${aAccessId}/portfolio`)
.pipe(
map((response) => {
if (response.holdings) {
for (const symbol of Object.keys(response.holdings)) {
response.holdings[symbol].valueInBaseCurrency = isNumber(
response.holdings[symbol].valueInBaseCurrency
)
? response.holdings[symbol].valueInBaseCurrency
: response.holdings[symbol].valueInPercentage;
}
}
return response;
})
);
}
public fetchTags() {
return this.http.get<Tag[]>('/api/v1/tags');
}
public fetchWatchlist() {
return this.http.get<WatchlistResponse>('/api/v1/watchlist');
}
public generateAccessToken(aUserId: string) {
return this.http.post<AccessTokenResponse>(
`/api/v1/user/${aUserId}/access-token`,
{}
);
}
public loginAnonymous(accessToken: string) {
return this.http.post<OAuthResponse>('/api/v1/auth/anonymous', {
accessToken
});
}
public postAccess(aAccess: CreateAccessDto) {
return this.http.post<OrderModel>('/api/v1/access', aAccess);
}
public postAccount(aAccount: CreateAccountDto) {
return this.http.post<OrderModel>('/api/v1/account', aAccount);
}
public postAccountBalance(aAccountBalance: CreateAccountBalanceDto) {
return this.http.post<AccountBalance>(
'/api/v1/account-balance',
aAccountBalance
);
}
public postApiKey() {
return this.http.post<ApiKeyResponse>('/api/v1/api-keys', {});
}
public postBenchmark(benchmark: AssetProfileIdentifier) {
return this.http.post('/api/v1/benchmarks', benchmark);
}
public postMarketData({
dataSource,
marketData,
symbol
}: {
dataSource: DataSource;
marketData: UpdateBulkMarketDataDto;
symbol: string;
}) {
const url = `/api/v1/market-data/${dataSource}/${symbol}`;
return this.http.post<MarketData>(url, marketData);
}
public postOrder(aOrder: CreateOrderDto) {
return this.http.post<OrderModel>('/api/v1/order', aOrder);
}
public postTag(aTag: CreateTagDto) {
return this.http.post<Tag>(`/api/v1/tags`, aTag);
}
public postUser() {
return this.http.post<UserItem>('/api/v1/user', {});
}
public postWatchlistItem(watchlistItem: CreateWatchlistItemDto) {
return this.http.post('/api/v1/watchlist', watchlistItem);
}
public putAccount(aAccount: UpdateAccountDto) {
return this.http.put<UserItem>(`/api/v1/account/${aAccount.id}`, aAccount);
}
public putAdminSetting(key: string, aData: PropertyDto) {
return this.http.put<void>(`/api/v1/admin/settings/${key}`, aData);
}
public putHoldingTags({
dataSource,
symbol,
tags
}: { tags: Tag[] } & AssetProfileIdentifier) {
return this.http.put<void>(
`/api/v1/portfolio/holding/${dataSource}/${symbol}/tags`,
{ tags }
);
}
public putOrder(aOrder: UpdateOrderDto) {
return this.http.put<UserItem>(`/api/v1/order/${aOrder.id}`, aOrder);
}
public putTag(aTag: UpdateTagDto) {
return this.http.put<Tag>(`/api/v1/tags/${aTag.id}`, aTag);
}
public putUserSetting(aData: UpdateUserSettingDto) {
return this.http.put<User>('/api/v1/user/setting', aData);
}
public redeemCoupon(couponCode: string) {
return this.http.post('/api/v1/subscription/redeem-coupon', {
couponCode
});
}
public transferAccountBalance({
accountIdFrom,
accountIdTo,
balance
}: TransferBalanceDto) {
return this.http.post('/api/v1/account/transfer-balance', {
accountIdFrom,
accountIdTo,
balance
});
}
public updateInfo() {
this.http.get<InfoItem>('/api/v1/info').subscribe((info) => {
const utmSource = window.localStorage.getItem('utm_source') as
| 'ios'
| 'trusted-web-activity';
info.globalPermissions = filterGlobalPermissions(
info.globalPermissions,
utmSource
);
(window as any).info = info;
});
}
}