diff --git a/apps/api/src/app/admin/admin.service.ts b/apps/api/src/app/admin/admin.service.ts index 0bf5c3925..511504939 100644 --- a/apps/api/src/app/admin/admin.service.ts +++ b/apps/api/src/app/admin/admin.service.ts @@ -14,6 +14,7 @@ import { PROPERTY_IS_USER_SIGNUP_ENABLED } from '@ghostfolio/common/config'; import { + applySymbolProfileOverrides, getAssetProfileIdentifier, getCurrencyFromSymbol, isCurrency @@ -29,7 +30,6 @@ import { EnhancedSymbolProfile, Filter } from '@ghostfolio/common/interfaces'; -import { Sector } from '@ghostfolio/common/interfaces/sector.interface'; import { MarketDataPreset } from '@ghostfolio/common/types'; import { @@ -349,87 +349,61 @@ export class AdminService { } let marketData: AdminMarketDataItem[] = await Promise.all( - assetProfiles.map( - async ({ + assetProfiles.map(async (assetProfile) => { + const { _count, activities, - assetClass, - assetSubClass, comment, - countries, currency, dataSource, id, isActive, isUsedByUsersWithSubscription, - name, - sectors, - symbol, - SymbolProfileOverrides - }) => { - let countriesCount = countries ? Object.keys(countries).length : 0; + symbol + } = assetProfile; - const lastMarketPrice = lastMarketPriceMap.get( - getAssetProfileIdentifier({ dataSource, symbol }) + const { assetClass, assetSubClass, countries, name, sectors } = + applySymbolProfileOverrides( + assetProfile, + assetProfile.SymbolProfileOverrides ); - 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; - } + const countriesCount = countries ? Object.keys(countries).length : 0; - name = SymbolProfileOverrides.name ?? name; + const lastMarketPrice = lastMarketPriceMap.get( + getAssetProfileIdentifier({ dataSource, symbol }) + ); - if ( - (SymbolProfileOverrides.sectors as unknown as Sector[])?.length > - 0 - ) { - sectorsCount = ( - SymbolProfileOverrides.sectors as unknown as Prisma.JsonArray - ).length; - } - } + const marketDataItemCount = + marketDataItems.find((marketDataItem) => { + return ( + marketDataItem.dataSource === dataSource && + marketDataItem.symbol === symbol + ); + })?._count ?? 0; - return { - assetClass, - assetSubClass, - comment, - countriesCount, - currency, - dataSource, - id, - isActive, - lastMarketPrice, - marketDataItemCount, - name, - sectorsCount, - symbol, - activitiesCount: _count.activities, - date: activities?.[0]?.date, - isUsedByUsersWithSubscription: await isUsedByUsersWithSubscription, - watchedByCount: _count.watchedBy - }; - } - ) + const sectorsCount = sectors ? Object.keys(sectors).length : 0; + + return { + assetClass, + assetSubClass, + comment, + countriesCount, + currency, + dataSource, + id, + isActive, + lastMarketPrice, + marketDataItemCount, + name, + sectorsCount, + symbol, + activitiesCount: _count.activities, + date: activities?.[0]?.date, + isUsedByUsersWithSubscription: await isUsedByUsersWithSubscription, + watchedByCount: _count.watchedBy + }; + }) ); if (presetId) { diff --git a/apps/api/src/services/queues/data-gathering/data-gathering.service.ts b/apps/api/src/services/queues/data-gathering/data-gathering.service.ts index cec63c3eb..9c3c7b6ca 100644 --- a/apps/api/src/services/queues/data-gathering/data-gathering.service.ts +++ b/apps/api/src/services/queues/data-gathering/data-gathering.service.ts @@ -176,9 +176,28 @@ export class DataGatheringService { ); for (const [symbol, assetProfile] of Object.entries(assetProfiles)) { - const symbolMapping = symbolProfiles.find((symbolProfile) => { - return symbolProfile.symbol === symbol; - })?.symbolMapping; + const symbolProfile = symbolProfiles.find( + ({ symbol: symbolProfileSymbol }) => { + return symbolProfileSymbol === symbol; + } + ); + + const symbolMapping = symbolProfile?.symbolMapping; + + // Persist the data provider’s classification in the base asset profile; + // the asset profile override stays a separate layer applied at read time + const { + assetClass: assetClassFromDataProvider, + assetSubClass: assetSubClassFromDataProvider + } = assetProfile; + + // Apply the asset profile override so the data enhancers treat the asset + // profile correctly (e.g. enrich an ETF that the data provider classifies + // as a stock) + if (symbolProfile) { + assetProfile.assetClass = symbolProfile.assetClass; + assetProfile.assetSubClass = symbolProfile.assetSubClass; + } for (const dataEnhancer of this.dataEnhancers) { try { @@ -197,6 +216,10 @@ export class DataGatheringService { } } + // Restore the data provider’s classification before persisting + assetProfile.assetClass = assetClassFromDataProvider; + assetProfile.assetSubClass = assetSubClassFromDataProvider; + const { assetClass, assetSubClass, diff --git a/apps/api/src/services/symbol-profile/symbol-profile.service.ts b/apps/api/src/services/symbol-profile/symbol-profile.service.ts index 4c2c42589..610808fae 100644 --- a/apps/api/src/services/symbol-profile/symbol-profile.service.ts +++ b/apps/api/src/services/symbol-profile/symbol-profile.service.ts @@ -1,5 +1,6 @@ import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { UNKNOWN_KEY } from '@ghostfolio/common/config'; +import { applySymbolProfileOverrides } from '@ghostfolio/common/helper'; import { AssetProfileIdentifier, EnhancedSymbolProfile, @@ -192,21 +193,28 @@ export class SymbolProfileService { })[] ): EnhancedSymbolProfile[] { return symbolProfiles.map((symbolProfile) => { + const symbolProfileWithOverrides = applySymbolProfileOverrides( + symbolProfile, + symbolProfile.SymbolProfileOverrides + ); + const item = { - ...symbolProfile, + ...symbolProfileWithOverrides, activitiesCount: 0, countries: this.getCountries( - symbolProfile?.countries as unknown as Prisma.JsonArray + symbolProfileWithOverrides?.countries as unknown as Prisma.JsonArray ), dateOfFirstActivity: undefined as Date, holdings: this.getHoldings( - symbolProfile?.holdings as unknown as Prisma.JsonArray + symbolProfileWithOverrides?.holdings as unknown as Prisma.JsonArray + ), + scraperConfiguration: this.getScraperConfiguration( + symbolProfileWithOverrides ), - scraperConfiguration: this.getScraperConfiguration(symbolProfile), sectors: this.getSectors( - symbolProfile?.sectors as unknown as Prisma.JsonArray + symbolProfileWithOverrides?.sectors as unknown as Prisma.JsonArray ), - symbolMapping: this.getSymbolMapping(symbolProfile), + symbolMapping: this.getSymbolMapping(symbolProfileWithOverrides), watchedByCount: 0 }; @@ -217,45 +225,7 @@ export class SymbolProfileService { item.dateOfFirstActivity = symbolProfile.activities?.[0]?.date; delete item.activities; - 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 = this.getHoldings( - item.SymbolProfileOverrides.holdings as unknown as Prisma.JsonArray - ); - } - - item.name = item.SymbolProfileOverrides.name ?? item.name; - - if ( - (item.SymbolProfileOverrides.sectors as unknown as Sector[])?.length > - 0 - ) { - item.sectors = this.getSectors( - item.SymbolProfileOverrides.sectors as unknown as Prisma.JsonArray - ); - } - - item.url = item.SymbolProfileOverrides.url ?? item.url; - - delete item.SymbolProfileOverrides; - } + delete item.SymbolProfileOverrides; return item; }); diff --git a/libs/common/src/lib/helper.ts b/libs/common/src/lib/helper.ts index c5f6cbbb9..4dbb0ceb1 100644 --- a/libs/common/src/lib/helper.ts +++ b/libs/common/src/lib/helper.ts @@ -1,5 +1,12 @@ import { NumberParser } from '@internationalized/number'; -import { Type as ActivityType, DataSource, MarketData } from '@prisma/client'; +import { + Type as ActivityType, + DataSource, + MarketData, + Prisma, + SymbolProfile, + SymbolProfileOverrides +} from '@prisma/client'; import { Big } from 'big.js'; import { isISO4217CurrencyCode } from 'class-validator'; import { @@ -47,6 +54,43 @@ export const DATE_FORMAT = 'yyyy-MM-dd'; export const DATE_FORMAT_MONTHLY = 'MMMM yyyy'; export const DATE_FORMAT_YEARLY = 'yyyy'; +export function applySymbolProfileOverrides>( + symbolProfile: T, + symbolProfileOverrides: SymbolProfileOverrides | null +): T { + if (!symbolProfileOverrides) { + return symbolProfile; + } + + const symbolProfileWithOverrides = { ...symbolProfile } as T; + + symbolProfileWithOverrides.assetClass = + symbolProfileOverrides.assetClass ?? symbolProfile.assetClass; + + symbolProfileWithOverrides.assetSubClass = + symbolProfileOverrides.assetSubClass ?? symbolProfile.assetSubClass; + + if ((symbolProfileOverrides.countries as Prisma.JsonArray)?.length > 0) { + symbolProfileWithOverrides.countries = symbolProfileOverrides.countries; + } + + if ((symbolProfileOverrides.holdings as Prisma.JsonArray)?.length > 0) { + symbolProfileWithOverrides.holdings = symbolProfileOverrides.holdings; + } + + symbolProfileWithOverrides.name = + symbolProfileOverrides.name ?? symbolProfile.name; + + if ((symbolProfileOverrides.sectors as Prisma.JsonArray)?.length > 0) { + symbolProfileWithOverrides.sectors = symbolProfileOverrides.sectors; + } + + symbolProfileWithOverrides.url = + symbolProfileOverrides.url ?? symbolProfile.url; + + return symbolProfileWithOverrides; +} + export function calculateBenchmarkTrend({ days, historicalData