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.
 
 
 
 
 

308 lines
8.2 KiB

import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
import {
AssetProfileIdentifier,
EnhancedSymbolProfile,
Holding,
ScraperConfiguration
} from '@ghostfolio/common/interfaces';
import { Country } from '@ghostfolio/common/interfaces/country.interface';
import { Sector } from '@ghostfolio/common/interfaces/sector.interface';
import { Injectable } from '@nestjs/common';
import { Prisma, SymbolProfile, SymbolProfileOverrides } from '@prisma/client';
import { continents, countries } from 'countries-list';
@Injectable()
export class SymbolProfileService {
public constructor(private readonly prismaService: PrismaService) {}
public async add(
assetProfile: Prisma.SymbolProfileCreateInput
): Promise<SymbolProfile | never> {
return this.prismaService.symbolProfile.create({ data: assetProfile });
}
public async delete({ dataSource, symbol }: AssetProfileIdentifier) {
return this.prismaService.symbolProfile.delete({
where: { dataSource_symbol: { dataSource, symbol } }
});
}
public async deleteById(id: string) {
return this.prismaService.symbolProfile.delete({
where: { id }
});
}
public async getSymbolProfiles(
aAssetProfileIdentifiers: AssetProfileIdentifier[]
): Promise<EnhancedSymbolProfile[]> {
return this.prismaService.symbolProfile
.findMany({
include: {
_count: {
select: { Order: true }
},
Order: {
orderBy: {
date: 'asc'
},
select: { date: true },
take: 1
},
SymbolProfileOverrides: true
},
where: {
OR: aAssetProfileIdentifiers.map(({ dataSource, symbol }) => {
return {
dataSource,
symbol
};
})
}
})
.then((symbolProfiles) => {
return this.enhanceSymbolProfiles(symbolProfiles);
});
}
public async getSymbolProfilesByIds(
symbolProfileIds: string[]
): Promise<EnhancedSymbolProfile[]> {
return this.prismaService.symbolProfile
.findMany({
include: {
_count: {
select: { Order: true }
},
SymbolProfileOverrides: true
},
where: {
id: {
in: symbolProfileIds.map((symbolProfileId) => {
return symbolProfileId;
})
}
}
})
.then((symbolProfiles) => {
return this.enhanceSymbolProfiles(symbolProfiles);
});
}
public async getSymbolProfilesByUserSubscription({
withUserSubscription = false
}: {
withUserSubscription?: boolean;
}) {
return this.prismaService.symbolProfile.findMany({
include: {
Order: {
include: {
User: true
}
}
},
orderBy: [{ symbol: 'asc' }],
where: {
Order: withUserSubscription
? {
some: {
User: {
Subscription: { some: { expiresAt: { gt: new Date() } } }
}
}
}
: {
every: {
User: {
Subscription: { none: { expiresAt: { gt: new Date() } } }
}
}
}
}
});
}
public updateSymbolProfile({
assetClass,
assetSubClass,
comment,
countries,
currency,
dataSource,
holdings,
name,
scraperConfiguration,
sectors,
symbol,
symbolMapping,
SymbolProfileOverrides,
url
}: AssetProfileIdentifier & Prisma.SymbolProfileUpdateInput) {
return this.prismaService.symbolProfile.update({
data: {
assetClass,
assetSubClass,
comment,
countries,
currency,
holdings,
name,
scraperConfiguration,
sectors,
symbolMapping,
SymbolProfileOverrides,
url
},
where: { dataSource_symbol: { dataSource, symbol } }
});
}
private enhanceSymbolProfiles(
symbolProfiles: (SymbolProfile & {
_count: { Order: number };
Order?: {
date: Date;
}[];
SymbolProfileOverrides: SymbolProfileOverrides;
})[]
): EnhancedSymbolProfile[] {
return symbolProfiles.map((symbolProfile) => {
const item = {
...symbolProfile,
activitiesCount: 0,
countries: this.getCountries(
symbolProfile?.countries as unknown as Prisma.JsonArray
),
dateOfFirstActivity: undefined as Date,
holdings: this.getHoldings(symbolProfile),
scraperConfiguration: this.getScraperConfiguration(symbolProfile),
sectors: this.getSectors(symbolProfile),
symbolMapping: this.getSymbolMapping(symbolProfile)
};
item.activitiesCount = symbolProfile._count.Order;
delete item._count;
item.dateOfFirstActivity = symbolProfile.Order?.[0]?.date;
delete item.Order;
if (item.SymbolProfileOverrides) {
item.assetClass =
item.SymbolProfileOverrides.assetClass ?? item.assetClass;
item.assetSubClass =
item.SymbolProfileOverrides.assetSubClass ?? item.assetSubClass;
if (
(item.SymbolProfileOverrides.countries as unknown as Prisma.JsonArray)
?.length > 0
) {
item.countries = this.getCountries(
item.SymbolProfileOverrides
?.countries as unknown as Prisma.JsonArray
);
}
if (
(item.SymbolProfileOverrides.holdings as unknown as Holding[])
?.length > 0
) {
item.holdings = item.SymbolProfileOverrides
.holdings as unknown as Holding[];
}
item.name = item.SymbolProfileOverrides?.name ?? item.name;
if (
(item.SymbolProfileOverrides.sectors as unknown as Sector[])?.length >
0
) {
item.sectors = item.SymbolProfileOverrides
.sectors as unknown as Sector[];
}
item.url = item.SymbolProfileOverrides?.url ?? item.url;
delete item.SymbolProfileOverrides;
}
return item;
});
}
private getCountries(aCountries: Prisma.JsonArray = []): Country[] {
if (aCountries === null) {
return [];
}
return aCountries.map((country: Pick<Country, 'code' | 'weight'>) => {
const { code, weight } = country;
return {
code,
weight,
continent: continents[countries[code]?.continent] ?? UNKNOWN_KEY,
name: countries[code]?.name ?? UNKNOWN_KEY
};
});
}
private getHoldings(symbolProfile: SymbolProfile): Holding[] {
return ((symbolProfile?.holdings as Prisma.JsonArray) ?? []).map(
(holding) => {
const { name, weight } = holding as Prisma.JsonObject;
return {
allocationInPercentage: weight as number,
name: (name as string) ?? UNKNOWN_KEY,
valueInBaseCurrency: undefined
};
}
);
}
private getScraperConfiguration(
symbolProfile: SymbolProfile
): ScraperConfiguration {
const scraperConfiguration =
symbolProfile.scraperConfiguration as Prisma.JsonObject;
if (scraperConfiguration) {
return {
defaultMarketPrice: scraperConfiguration.defaultMarketPrice as number,
headers:
scraperConfiguration.headers as ScraperConfiguration['headers'],
locale: scraperConfiguration.locale as string,
mode:
(scraperConfiguration.mode as ScraperConfiguration['mode']) ?? 'lazy',
selector: scraperConfiguration.selector as string,
url: scraperConfiguration.url as string
};
}
return null;
}
private getSectors(symbolProfile: SymbolProfile): Sector[] {
return ((symbolProfile?.sectors as Prisma.JsonArray) ?? []).map(
(sector) => {
const { name, weight } = sector as Prisma.JsonObject;
return {
name: (name as string) ?? UNKNOWN_KEY,
weight: weight as number
};
}
);
}
private getSymbolMapping(symbolProfile: SymbolProfile) {
return (
(symbolProfile['symbolMapping'] as {
[key: string]: string;
}) ?? {}
);
}
}