mirror of https://github.com/ghostfolio/ghostfolio
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.
854 lines
24 KiB
854 lines
24 KiB
import { OrderService } from '@ghostfolio/api/app/order/order.service';
|
|
import { SubscriptionService } from '@ghostfolio/api/app/subscription/subscription.service';
|
|
import { environment } from '@ghostfolio/api/environments/environment';
|
|
// import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service';
|
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
|
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
|
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
|
|
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
|
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
|
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
|
|
import {
|
|
PROPERTY_CURRENCIES,
|
|
PROPERTY_IS_READ_ONLY_MODE,
|
|
PROPERTY_IS_USER_SIGNUP_ENABLED
|
|
} from '@ghostfolio/common/config';
|
|
import {
|
|
// getAssetProfileIdentifier,
|
|
getCurrencyFromSymbol,
|
|
isCurrency
|
|
} from '@ghostfolio/common/helper';
|
|
import {
|
|
AdminData,
|
|
AdminMarketData,
|
|
AdminMarketDataDetails,
|
|
// AdminMarketDataItem,
|
|
AdminUsers,
|
|
AssetProfileIdentifier,
|
|
EnhancedSymbolProfile,
|
|
Filter
|
|
} from '@ghostfolio/common/interfaces';
|
|
// import { Sector } from '@ghostfolio/common/interfaces/sector.interface';
|
|
import { MarketDataPreset } from '@ghostfolio/common/types';
|
|
|
|
import {
|
|
BadRequestException,
|
|
HttpException,
|
|
Injectable
|
|
// Logger
|
|
} from '@nestjs/common';
|
|
import {
|
|
AssetClass,
|
|
AssetSubClass,
|
|
DataSource,
|
|
Prisma,
|
|
// PrismaClient,
|
|
Property,
|
|
SymbolProfile
|
|
} from '@prisma/client';
|
|
import { differenceInDays } from 'date-fns';
|
|
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
|
|
|
// import { groupBy } from 'lodash';
|
|
|
|
@Injectable()
|
|
export class AdminService {
|
|
public constructor(
|
|
// private readonly benchmarkService: BenchmarkService,
|
|
private readonly configurationService: ConfigurationService,
|
|
private readonly dataProviderService: DataProviderService,
|
|
private readonly exchangeRateDataService: ExchangeRateDataService,
|
|
private readonly marketDataService: MarketDataService,
|
|
private readonly orderService: OrderService,
|
|
private readonly prismaService: PrismaService,
|
|
private readonly propertyService: PropertyService,
|
|
private readonly subscriptionService: SubscriptionService,
|
|
private readonly symbolProfileService: SymbolProfileService
|
|
) {}
|
|
|
|
public async addAssetProfile({
|
|
currency,
|
|
dataSource,
|
|
symbol
|
|
}: AssetProfileIdentifier & { currency?: string }): Promise<
|
|
SymbolProfile | never
|
|
> {
|
|
try {
|
|
if (dataSource === 'MANUAL') {
|
|
return this.symbolProfileService.add({
|
|
currency,
|
|
dataSource,
|
|
symbol
|
|
});
|
|
}
|
|
|
|
const assetProfiles = await this.dataProviderService.getAssetProfiles([
|
|
{ dataSource, symbol }
|
|
]);
|
|
|
|
if (!assetProfiles[symbol]?.currency) {
|
|
throw new BadRequestException(
|
|
`Asset profile not found for ${symbol} (${dataSource})`
|
|
);
|
|
}
|
|
|
|
return this.symbolProfileService.add(
|
|
assetProfiles[symbol] as Prisma.SymbolProfileCreateInput
|
|
);
|
|
} catch (error) {
|
|
if (
|
|
error instanceof Prisma.PrismaClientKnownRequestError &&
|
|
error.code === 'P2002'
|
|
) {
|
|
throw new BadRequestException(
|
|
`Asset profile of ${symbol} (${dataSource}) already exists`
|
|
);
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
public async deleteProfileData({
|
|
dataSource,
|
|
symbol
|
|
}: AssetProfileIdentifier) {
|
|
await this.marketDataService.deleteMany({ dataSource, symbol });
|
|
|
|
const currency = getCurrencyFromSymbol(symbol);
|
|
const customCurrencies = (await this.propertyService.getByKey(
|
|
PROPERTY_CURRENCIES
|
|
)) as string[];
|
|
|
|
if (customCurrencies.includes(currency)) {
|
|
const updatedCustomCurrencies = customCurrencies.filter(
|
|
(customCurrency) => {
|
|
return customCurrency !== currency;
|
|
}
|
|
);
|
|
|
|
await this.putSetting(
|
|
PROPERTY_CURRENCIES,
|
|
JSON.stringify(updatedCustomCurrencies)
|
|
);
|
|
} else {
|
|
await this.symbolProfileService.delete({ dataSource, symbol });
|
|
}
|
|
}
|
|
|
|
public async get(): Promise<AdminData> {
|
|
const [settings, transactionCount, userCount] = await Promise.all([
|
|
this.propertyService.get(),
|
|
this.prismaService.order.count(),
|
|
this.countUsersWithAnalytics()
|
|
]);
|
|
|
|
return {
|
|
settings,
|
|
transactionCount,
|
|
userCount,
|
|
version: environment.version
|
|
};
|
|
}
|
|
|
|
public async getMarketData(
|
|
{
|
|
// filters,
|
|
// presetId,
|
|
// sortColumn,
|
|
// sortDirection,
|
|
// skip,
|
|
// take = Number.MAX_SAFE_INTEGER
|
|
}: {
|
|
filters?: Filter[];
|
|
presetId?: MarketDataPreset;
|
|
skip?: number;
|
|
sortColumn?: string;
|
|
sortDirection?: Prisma.SortOrder;
|
|
take?: number;
|
|
}
|
|
): Promise<AdminMarketData> {
|
|
return Promise.resolve({
|
|
count: 0,
|
|
marketData: []
|
|
});
|
|
|
|
// let orderBy: Prisma.Enumerable<Prisma.SymbolProfileOrderByWithRelationInput> =
|
|
// [{ symbol: 'asc' }];
|
|
// const where: Prisma.SymbolProfileWhereInput = {};
|
|
|
|
// if (presetId === 'BENCHMARKS') {
|
|
// const benchmarkAssetProfiles =
|
|
// await this.benchmarkService.getBenchmarkAssetProfiles();
|
|
|
|
// where.id = {
|
|
// in: benchmarkAssetProfiles.map(({ id }) => {
|
|
// return id;
|
|
// })
|
|
// };
|
|
// } else if (presetId === 'CURRENCIES') {
|
|
// return this.getMarketDataForCurrencies();
|
|
// } else if (
|
|
// presetId === 'ETF_WITHOUT_COUNTRIES' ||
|
|
// presetId === 'ETF_WITHOUT_SECTORS'
|
|
// ) {
|
|
// filters = [{ id: 'ETF', type: 'ASSET_SUB_CLASS' }];
|
|
// }
|
|
|
|
// const searchQuery = filters.find(({ type }) => {
|
|
// return type === 'SEARCH_QUERY';
|
|
// })?.id;
|
|
|
|
// const { ASSET_SUB_CLASS: filtersByAssetSubClass } = groupBy(
|
|
// filters,
|
|
// ({ type }) => {
|
|
// return type;
|
|
// }
|
|
// );
|
|
|
|
// const marketDataItems = await this.prismaService.marketData.groupBy({
|
|
// _count: true,
|
|
// by: ['dataSource', 'symbol']
|
|
// });
|
|
|
|
// if (filtersByAssetSubClass) {
|
|
// where.assetSubClass = AssetSubClass[filtersByAssetSubClass[0].id];
|
|
// }
|
|
|
|
// if (searchQuery) {
|
|
// where.OR = [
|
|
// { id: { mode: 'insensitive', startsWith: searchQuery } },
|
|
// { isin: { mode: 'insensitive', startsWith: searchQuery } },
|
|
// { name: { mode: 'insensitive', startsWith: searchQuery } },
|
|
// { symbol: { mode: 'insensitive', startsWith: searchQuery } }
|
|
// ];
|
|
// }
|
|
|
|
// if (sortColumn) {
|
|
// orderBy = [{ [sortColumn]: sortDirection }];
|
|
|
|
// if (sortColumn === 'activitiesCount') {
|
|
// orderBy = {
|
|
// Order: {
|
|
// _count: sortDirection
|
|
// }
|
|
// };
|
|
// }
|
|
// }
|
|
|
|
// const extendedPrismaClient = this.getExtendedPrismaClient();
|
|
|
|
// try {
|
|
// const symbolProfileResult = await Promise.all([
|
|
// extendedPrismaClient.symbolProfile.findMany({
|
|
// orderBy,
|
|
// skip,
|
|
// take,
|
|
// where,
|
|
// select: {
|
|
// _count: {
|
|
// select: { Order: true }
|
|
// },
|
|
// assetClass: true,
|
|
// assetSubClass: true,
|
|
// comment: true,
|
|
// countries: true,
|
|
// currency: true,
|
|
// dataSource: true,
|
|
// id: true,
|
|
// isActive: true,
|
|
// isUsedByUsersWithSubscription: true,
|
|
// name: true,
|
|
// Order: {
|
|
// orderBy: [{ date: 'asc' }],
|
|
// select: { date: true },
|
|
// take: 1
|
|
// },
|
|
// scraperConfiguration: true,
|
|
// sectors: true,
|
|
// symbol: true,
|
|
// SymbolProfileOverrides: true
|
|
// }
|
|
// }),
|
|
// this.prismaService.symbolProfile.count({ where })
|
|
// ]);
|
|
// const assetProfiles = symbolProfileResult[0];
|
|
// let count = symbolProfileResult[1];
|
|
|
|
// const lastMarketPrices = await this.prismaService.marketData.findMany({
|
|
// distinct: ['dataSource', 'symbol'],
|
|
// orderBy: { date: 'desc' },
|
|
// select: {
|
|
// dataSource: true,
|
|
// marketPrice: true,
|
|
// symbol: true
|
|
// },
|
|
// where: {
|
|
// dataSource: {
|
|
// in: assetProfiles.map(({ dataSource }) => {
|
|
// return dataSource;
|
|
// })
|
|
// },
|
|
// symbol: {
|
|
// in: assetProfiles.map(({ symbol }) => {
|
|
// return symbol;
|
|
// })
|
|
// }
|
|
// }
|
|
// });
|
|
|
|
// const lastMarketPriceMap = new Map<string, number>();
|
|
|
|
// for (const { dataSource, marketPrice, symbol } of lastMarketPrices) {
|
|
// lastMarketPriceMap.set(
|
|
// getAssetProfileIdentifier({ dataSource, symbol }),
|
|
// marketPrice
|
|
// );
|
|
// }
|
|
|
|
// let marketData: AdminMarketDataItem[] = await Promise.all(
|
|
// assetProfiles.map(
|
|
// async ({
|
|
// _count,
|
|
// assetClass,
|
|
// assetSubClass,
|
|
// comment,
|
|
// countries,
|
|
// currency,
|
|
// dataSource,
|
|
// id,
|
|
// isActive,
|
|
// isUsedByUsersWithSubscription,
|
|
// name,
|
|
// Order,
|
|
// sectors,
|
|
// symbol,
|
|
// SymbolProfileOverrides
|
|
// }) => {
|
|
// let countriesCount = countries ? Object.keys(countries).length : 0;
|
|
|
|
// const lastMarketPrice = lastMarketPriceMap.get(
|
|
// getAssetProfileIdentifier({ dataSource, symbol })
|
|
// );
|
|
|
|
// const marketDataItemCount =
|
|
// marketDataItems.find((marketDataItem) => {
|
|
// return (
|
|
// marketDataItem.dataSource === dataSource &&
|
|
// marketDataItem.symbol === symbol
|
|
// );
|
|
// })?._count ?? 0;
|
|
|
|
// let sectorsCount = sectors ? Object.keys(sectors).length : 0;
|
|
|
|
// if (SymbolProfileOverrides) {
|
|
// assetClass = SymbolProfileOverrides.assetClass ?? assetClass;
|
|
// assetSubClass =
|
|
// SymbolProfileOverrides.assetSubClass ?? assetSubClass;
|
|
|
|
// if (
|
|
// (
|
|
// SymbolProfileOverrides.countries as unknown as Prisma.JsonArray
|
|
// )?.length > 0
|
|
// ) {
|
|
// countriesCount = (
|
|
// SymbolProfileOverrides.countries as unknown as Prisma.JsonArray
|
|
// ).length;
|
|
// }
|
|
|
|
// name = SymbolProfileOverrides.name ?? name;
|
|
|
|
// if (
|
|
// (SymbolProfileOverrides.sectors as unknown as Sector[])
|
|
// ?.length > 0
|
|
// ) {
|
|
// sectorsCount = (
|
|
// SymbolProfileOverrides.sectors as unknown as Prisma.JsonArray
|
|
// ).length;
|
|
// }
|
|
// }
|
|
|
|
// return {
|
|
// assetClass,
|
|
// assetSubClass,
|
|
// comment,
|
|
// currency,
|
|
// countriesCount,
|
|
// dataSource,
|
|
// id,
|
|
// isActive,
|
|
// lastMarketPrice,
|
|
// name,
|
|
// symbol,
|
|
// marketDataItemCount,
|
|
// sectorsCount,
|
|
// activitiesCount: _count.Order,
|
|
// date: Order?.[0]?.date,
|
|
// isUsedByUsersWithSubscription: await isUsedByUsersWithSubscription
|
|
// };
|
|
// }
|
|
// )
|
|
// );
|
|
|
|
// if (presetId) {
|
|
// if (presetId === 'ETF_WITHOUT_COUNTRIES') {
|
|
// marketData = marketData.filter(({ countriesCount }) => {
|
|
// return countriesCount === 0;
|
|
// });
|
|
// } else if (presetId === 'ETF_WITHOUT_SECTORS') {
|
|
// marketData = marketData.filter(({ sectorsCount }) => {
|
|
// return sectorsCount === 0;
|
|
// });
|
|
// }
|
|
|
|
// count = marketData.length;
|
|
// }
|
|
|
|
// return {
|
|
// count,
|
|
// marketData
|
|
// };
|
|
// } finally {
|
|
// await extendedPrismaClient.$disconnect();
|
|
|
|
// Logger.debug('Disconnect extended prisma client', 'AdminService');
|
|
// }
|
|
}
|
|
|
|
public async getMarketDataBySymbol({
|
|
dataSource,
|
|
symbol
|
|
}: AssetProfileIdentifier): Promise<AdminMarketDataDetails> {
|
|
let activitiesCount: EnhancedSymbolProfile['activitiesCount'] = 0;
|
|
let currency: EnhancedSymbolProfile['currency'] = '-';
|
|
let dateOfFirstActivity: EnhancedSymbolProfile['dateOfFirstActivity'];
|
|
|
|
if (isCurrency(getCurrencyFromSymbol(symbol))) {
|
|
currency = getCurrencyFromSymbol(symbol);
|
|
({ activitiesCount, dateOfFirstActivity } =
|
|
await this.orderService.getStatisticsByCurrency(currency));
|
|
}
|
|
|
|
const [[assetProfile], marketData] = await Promise.all([
|
|
this.symbolProfileService.getSymbolProfiles([
|
|
{
|
|
dataSource,
|
|
symbol
|
|
}
|
|
]),
|
|
this.marketDataService.marketDataItems({
|
|
orderBy: {
|
|
date: 'asc'
|
|
},
|
|
where: {
|
|
dataSource,
|
|
symbol
|
|
}
|
|
})
|
|
]);
|
|
|
|
if (assetProfile) {
|
|
assetProfile.dataProviderInfo = this.dataProviderService
|
|
.getDataProvider(assetProfile.dataSource)
|
|
.getDataProviderInfo();
|
|
}
|
|
|
|
return {
|
|
marketData,
|
|
assetProfile: assetProfile ?? {
|
|
activitiesCount,
|
|
currency,
|
|
dataSource,
|
|
dateOfFirstActivity,
|
|
symbol
|
|
}
|
|
};
|
|
}
|
|
|
|
public async getUsers({
|
|
skip,
|
|
take = Number.MAX_SAFE_INTEGER
|
|
}: {
|
|
skip?: number;
|
|
take?: number;
|
|
}): Promise<AdminUsers> {
|
|
const [count, users] = await Promise.all([
|
|
this.countUsersWithAnalytics(),
|
|
this.getUsersWithAnalytics({ skip, take })
|
|
]);
|
|
|
|
return { count, users };
|
|
}
|
|
|
|
public async patchAssetProfileData(
|
|
{ dataSource, symbol }: AssetProfileIdentifier,
|
|
{
|
|
assetClass,
|
|
assetSubClass,
|
|
comment,
|
|
countries,
|
|
currency,
|
|
dataSource: newDataSource,
|
|
holdings,
|
|
isActive,
|
|
name,
|
|
scraperConfiguration,
|
|
sectors,
|
|
symbol: newSymbol,
|
|
symbolMapping,
|
|
url
|
|
}: Prisma.SymbolProfileUpdateInput
|
|
) {
|
|
if (
|
|
newSymbol &&
|
|
newDataSource &&
|
|
(newSymbol !== symbol || newDataSource !== dataSource)
|
|
) {
|
|
const [assetProfile] = await this.symbolProfileService.getSymbolProfiles([
|
|
{
|
|
dataSource: DataSource[newDataSource.toString()],
|
|
symbol: newSymbol as string
|
|
}
|
|
]);
|
|
|
|
if (assetProfile) {
|
|
throw new HttpException(
|
|
getReasonPhrase(StatusCodes.CONFLICT),
|
|
StatusCodes.CONFLICT
|
|
);
|
|
}
|
|
|
|
try {
|
|
Promise.all([
|
|
await this.symbolProfileService.updateAssetProfileIdentifier(
|
|
{
|
|
dataSource,
|
|
symbol
|
|
},
|
|
{
|
|
dataSource: DataSource[newDataSource.toString()],
|
|
symbol: newSymbol as string
|
|
}
|
|
),
|
|
await this.marketDataService.updateAssetProfileIdentifier(
|
|
{
|
|
dataSource,
|
|
symbol
|
|
},
|
|
{
|
|
dataSource: DataSource[newDataSource.toString()],
|
|
symbol: newSymbol as string
|
|
}
|
|
)
|
|
]);
|
|
|
|
return this.symbolProfileService.getSymbolProfiles([
|
|
{
|
|
dataSource: DataSource[newDataSource.toString()],
|
|
symbol: newSymbol as string
|
|
}
|
|
])?.[0];
|
|
} catch {
|
|
throw new HttpException(
|
|
getReasonPhrase(StatusCodes.BAD_REQUEST),
|
|
StatusCodes.BAD_REQUEST
|
|
);
|
|
}
|
|
} else {
|
|
const symbolProfileOverrides = {
|
|
assetClass: assetClass as AssetClass,
|
|
assetSubClass: assetSubClass as AssetSubClass,
|
|
name: name as string,
|
|
url: url as string
|
|
};
|
|
|
|
const updatedSymbolProfile: Prisma.SymbolProfileUpdateInput = {
|
|
comment,
|
|
countries,
|
|
currency,
|
|
dataSource,
|
|
holdings,
|
|
isActive,
|
|
scraperConfiguration,
|
|
sectors,
|
|
symbol,
|
|
symbolMapping,
|
|
...(dataSource === 'MANUAL'
|
|
? { assetClass, assetSubClass, name, url }
|
|
: {
|
|
SymbolProfileOverrides: {
|
|
upsert: {
|
|
create: symbolProfileOverrides,
|
|
update: symbolProfileOverrides
|
|
}
|
|
}
|
|
})
|
|
};
|
|
|
|
await this.symbolProfileService.updateSymbolProfile(
|
|
{
|
|
dataSource,
|
|
symbol
|
|
},
|
|
updatedSymbolProfile
|
|
);
|
|
|
|
return this.symbolProfileService.getSymbolProfiles([
|
|
{
|
|
dataSource: dataSource as DataSource,
|
|
symbol: symbol as string
|
|
}
|
|
])?.[0];
|
|
}
|
|
}
|
|
|
|
public async putSetting(key: string, value: string) {
|
|
let response: Property;
|
|
|
|
if (value) {
|
|
response = await this.propertyService.put({ key, value });
|
|
} else {
|
|
response = await this.propertyService.delete({ key });
|
|
}
|
|
|
|
if (key === PROPERTY_IS_READ_ONLY_MODE && value === 'true') {
|
|
await this.putSetting(PROPERTY_IS_USER_SIGNUP_ENABLED, 'false');
|
|
} else if (key === PROPERTY_CURRENCIES) {
|
|
await this.exchangeRateDataService.initialize();
|
|
}
|
|
|
|
return response;
|
|
}
|
|
|
|
private async countUsersWithAnalytics() {
|
|
let where: Prisma.UserWhereInput;
|
|
|
|
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
|
|
where = {
|
|
NOT: {
|
|
Analytics: null
|
|
}
|
|
};
|
|
}
|
|
|
|
return this.prismaService.user.count({
|
|
where
|
|
});
|
|
}
|
|
|
|
// private getExtendedPrismaClient() {
|
|
// Logger.debug('Connect extended prisma client', 'AdminService');
|
|
|
|
// const symbolProfileExtension = Prisma.defineExtension((client) => {
|
|
// return client.$extends({
|
|
// result: {
|
|
// symbolProfile: {
|
|
// isUsedByUsersWithSubscription: {
|
|
// compute: async ({ id }) => {
|
|
// const { _count } =
|
|
// await this.prismaService.symbolProfile.findUnique({
|
|
// select: {
|
|
// _count: {
|
|
// select: {
|
|
// Order: {
|
|
// where: {
|
|
// User: {
|
|
// Subscription: {
|
|
// some: {
|
|
// expiresAt: {
|
|
// gt: new Date()
|
|
// }
|
|
// }
|
|
// }
|
|
// }
|
|
// }
|
|
// }
|
|
// }
|
|
// }
|
|
// },
|
|
// where: {
|
|
// id
|
|
// }
|
|
// });
|
|
|
|
// return _count.Order > 0;
|
|
// }
|
|
// }
|
|
// }
|
|
// }
|
|
// });
|
|
// });
|
|
|
|
// return new PrismaClient().$extends(symbolProfileExtension);
|
|
// }
|
|
|
|
// private async getMarketDataForCurrencies(): Promise<AdminMarketData> {
|
|
// const currencyPairs = this.exchangeRateDataService.getCurrencyPairs();
|
|
|
|
// const [lastMarketPrices, marketDataItems] = await Promise.all([
|
|
// this.prismaService.marketData.findMany({
|
|
// distinct: ['dataSource', 'symbol'],
|
|
// orderBy: { date: 'desc' },
|
|
// select: {
|
|
// dataSource: true,
|
|
// marketPrice: true,
|
|
// symbol: true
|
|
// },
|
|
// where: {
|
|
// dataSource: {
|
|
// in: currencyPairs.map(({ dataSource }) => {
|
|
// return dataSource;
|
|
// })
|
|
// },
|
|
// symbol: {
|
|
// in: currencyPairs.map(({ symbol }) => {
|
|
// return symbol;
|
|
// })
|
|
// }
|
|
// }
|
|
// }),
|
|
// this.prismaService.marketData.groupBy({
|
|
// _count: true,
|
|
// by: ['dataSource', 'symbol']
|
|
// })
|
|
// ]);
|
|
|
|
// const lastMarketPriceMap = new Map<string, number>();
|
|
|
|
// for (const { dataSource, marketPrice, symbol } of lastMarketPrices) {
|
|
// lastMarketPriceMap.set(
|
|
// getAssetProfileIdentifier({ dataSource, symbol }),
|
|
// marketPrice
|
|
// );
|
|
// }
|
|
|
|
// const marketDataPromise: Promise<AdminMarketDataItem>[] = currencyPairs.map(
|
|
// async ({ dataSource, symbol }) => {
|
|
// let activitiesCount: EnhancedSymbolProfile['activitiesCount'] = 0;
|
|
// let currency: EnhancedSymbolProfile['currency'] = '-';
|
|
// let dateOfFirstActivity: EnhancedSymbolProfile['dateOfFirstActivity'];
|
|
|
|
// if (isCurrency(getCurrencyFromSymbol(symbol))) {
|
|
// currency = getCurrencyFromSymbol(symbol);
|
|
// ({ activitiesCount, dateOfFirstActivity } =
|
|
// await this.orderService.getStatisticsByCurrency(currency));
|
|
// }
|
|
|
|
// const lastMarketPrice = lastMarketPriceMap.get(
|
|
// getAssetProfileIdentifier({ dataSource, symbol })
|
|
// );
|
|
|
|
// const marketDataItemCount =
|
|
// marketDataItems.find((marketDataItem) => {
|
|
// return (
|
|
// marketDataItem.dataSource === dataSource &&
|
|
// marketDataItem.symbol === symbol
|
|
// );
|
|
// })?._count ?? 0;
|
|
|
|
// return {
|
|
// activitiesCount,
|
|
// currency,
|
|
// dataSource,
|
|
// lastMarketPrice,
|
|
// marketDataItemCount,
|
|
// symbol,
|
|
// assetClass: AssetClass.LIQUIDITY,
|
|
// assetSubClass: AssetSubClass.CASH,
|
|
// countriesCount: 0,
|
|
// date: dateOfFirstActivity,
|
|
// id: undefined,
|
|
// isActive: true,
|
|
// name: symbol,
|
|
// sectorsCount: 0
|
|
// };
|
|
// }
|
|
// );
|
|
|
|
// const marketData = await Promise.all(marketDataPromise);
|
|
// return { marketData, count: marketData.length };
|
|
// }
|
|
|
|
private async getUsersWithAnalytics({
|
|
skip,
|
|
take
|
|
}: {
|
|
skip?: number;
|
|
take?: number;
|
|
}): Promise<AdminUsers['users']> {
|
|
let orderBy: Prisma.UserOrderByWithRelationInput = {
|
|
createdAt: 'desc'
|
|
};
|
|
let where: Prisma.UserWhereInput;
|
|
|
|
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
|
|
orderBy = {
|
|
Analytics: {
|
|
lastRequestAt: 'desc'
|
|
}
|
|
};
|
|
where = {
|
|
NOT: {
|
|
Analytics: null
|
|
}
|
|
};
|
|
}
|
|
|
|
const usersWithAnalytics = await this.prismaService.user.findMany({
|
|
orderBy,
|
|
skip,
|
|
take,
|
|
where,
|
|
select: {
|
|
_count: {
|
|
select: { Account: true, Order: true }
|
|
},
|
|
Analytics: {
|
|
select: {
|
|
activityCount: true,
|
|
country: true,
|
|
dataProviderGhostfolioDailyRequests: true,
|
|
updatedAt: true
|
|
}
|
|
},
|
|
createdAt: true,
|
|
id: true,
|
|
role: true,
|
|
Subscription: true
|
|
}
|
|
});
|
|
|
|
return usersWithAnalytics.map(
|
|
({ _count, Analytics, createdAt, id, role, Subscription }) => {
|
|
const daysSinceRegistration =
|
|
differenceInDays(new Date(), createdAt) + 1;
|
|
const engagement = Analytics
|
|
? Analytics.activityCount / daysSinceRegistration
|
|
: undefined;
|
|
|
|
const subscription = this.configurationService.get(
|
|
'ENABLE_FEATURE_SUBSCRIPTION'
|
|
)
|
|
? this.subscriptionService.getSubscription({
|
|
createdAt,
|
|
subscriptions: Subscription
|
|
})
|
|
: undefined;
|
|
|
|
return {
|
|
createdAt,
|
|
engagement,
|
|
id,
|
|
role,
|
|
subscription,
|
|
accountCount: _count.Account || 0,
|
|
country: Analytics?.country,
|
|
dailyApiRequests: Analytics?.dataProviderGhostfolioDailyRequests || 0,
|
|
lastActivity: Analytics?.updatedAt,
|
|
transactionCount: _count.Order || 0
|
|
};
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|