|
|
@ -3,7 +3,6 @@ import { OrderService } from '@ghostfolio/api/app/order/order.service'; |
|
|
import { PlatformService } from '@ghostfolio/api/app/platform/platform.service'; |
|
|
import { PlatformService } from '@ghostfolio/api/app/platform/platform.service'; |
|
|
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service'; |
|
|
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service'; |
|
|
import { ApiService } from '@ghostfolio/api/services/api/api.service'; |
|
|
import { ApiService } from '@ghostfolio/api/services/api/api.service'; |
|
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; |
|
|
|
|
|
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.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 { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; |
|
|
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; |
|
|
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; |
|
|
@ -33,7 +32,7 @@ import { |
|
|
} from '@ghostfolio/common/types'; |
|
|
} from '@ghostfolio/common/types'; |
|
|
|
|
|
|
|
|
import { Injectable } from '@nestjs/common'; |
|
|
import { Injectable } from '@nestjs/common'; |
|
|
import { DataSource, Prisma, SymbolProfile } from '@prisma/client'; |
|
|
import { DataSource, Prisma } from '@prisma/client'; |
|
|
import { Big } from 'big.js'; |
|
|
import { Big } from 'big.js'; |
|
|
import { endOfToday, isAfter, isSameSecond, parseISO } from 'date-fns'; |
|
|
import { endOfToday, isAfter, isSameSecond, parseISO } from 'date-fns'; |
|
|
import { omit, uniqBy } from 'lodash'; |
|
|
import { omit, uniqBy } from 'lodash'; |
|
|
@ -46,7 +45,6 @@ export class ImportService { |
|
|
public constructor( |
|
|
public constructor( |
|
|
private readonly accountService: AccountService, |
|
|
private readonly accountService: AccountService, |
|
|
private readonly apiService: ApiService, |
|
|
private readonly apiService: ApiService, |
|
|
private readonly configurationService: ConfigurationService, |
|
|
|
|
|
private readonly dataGatheringService: DataGatheringService, |
|
|
private readonly dataGatheringService: DataGatheringService, |
|
|
private readonly dataProviderService: DataProviderService, |
|
|
private readonly dataProviderService: DataProviderService, |
|
|
private readonly exchangeRateDataService: ExchangeRateDataService, |
|
|
private readonly exchangeRateDataService: ExchangeRateDataService, |
|
|
@ -395,7 +393,7 @@ export class ImportService { |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const assetProfiles = await this.validateActivities({ |
|
|
const assetProfiles = await this.dataProviderService.validateActivities({ |
|
|
activitiesDto, |
|
|
activitiesDto, |
|
|
assetProfilesWithMarketDataDto, |
|
|
assetProfilesWithMarketDataDto, |
|
|
maxActivitiesToImport, |
|
|
maxActivitiesToImport, |
|
|
@ -729,132 +727,4 @@ export class ImportService { |
|
|
|
|
|
|
|
|
return uniqueAccountIds.size === 1; |
|
|
return uniqueAccountIds.size === 1; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
private async validateActivities({ |
|
|
|
|
|
activitiesDto, |
|
|
|
|
|
assetProfilesWithMarketDataDto, |
|
|
|
|
|
maxActivitiesToImport, |
|
|
|
|
|
user |
|
|
|
|
|
}: { |
|
|
|
|
|
activitiesDto: Partial<CreateOrderDto>[]; |
|
|
|
|
|
assetProfilesWithMarketDataDto: ImportDataDto['assetProfiles']; |
|
|
|
|
|
maxActivitiesToImport: number; |
|
|
|
|
|
user: UserWithSettings; |
|
|
|
|
|
}) { |
|
|
|
|
|
if (activitiesDto?.length > maxActivitiesToImport) { |
|
|
|
|
|
throw new Error(`Too many activities (${maxActivitiesToImport} at most)`); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const assetProfiles: { |
|
|
|
|
|
[assetProfileIdentifier: string]: Partial<SymbolProfile>; |
|
|
|
|
|
} = {}; |
|
|
|
|
|
const dataSources = await this.dataProviderService.getDataSources(); |
|
|
|
|
|
|
|
|
|
|
|
for (const [ |
|
|
|
|
|
index, |
|
|
|
|
|
{ currency, dataSource, symbol, type } |
|
|
|
|
|
] of activitiesDto.entries()) { |
|
|
|
|
|
if (!dataSources.includes(dataSource)) { |
|
|
|
|
|
throw new Error( |
|
|
|
|
|
`activities.${index}.dataSource ("${dataSource}") is not valid` |
|
|
|
|
|
); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if ( |
|
|
|
|
|
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') && |
|
|
|
|
|
user.subscription.type === 'Basic' |
|
|
|
|
|
) { |
|
|
|
|
|
const dataProvider = this.dataProviderService.getDataProvider( |
|
|
|
|
|
DataSource[dataSource] |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
if (dataProvider.getDataProviderInfo().isPremium) { |
|
|
|
|
|
throw new Error( |
|
|
|
|
|
`activities.${index}.dataSource ("${dataSource}") is not valid` |
|
|
|
|
|
); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (!assetProfiles[getAssetProfileIdentifier({ dataSource, symbol })]) { |
|
|
|
|
|
if (['FEE', 'INTEREST', 'LIABILITY'].includes(type)) { |
|
|
|
|
|
// Skip asset profile validation for FEE, INTEREST, and LIABILITY
|
|
|
|
|
|
// as these activity types don't require asset profiles
|
|
|
|
|
|
const assetProfileInImport = assetProfilesWithMarketDataDto?.find( |
|
|
|
|
|
(profile) => { |
|
|
|
|
|
return ( |
|
|
|
|
|
profile.dataSource === dataSource && profile.symbol === symbol |
|
|
|
|
|
); |
|
|
|
|
|
} |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
assetProfiles[getAssetProfileIdentifier({ dataSource, symbol })] = { |
|
|
|
|
|
currency, |
|
|
|
|
|
dataSource, |
|
|
|
|
|
symbol, |
|
|
|
|
|
name: assetProfileInImport?.name |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
continue; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
let assetProfile: Partial<SymbolProfile> = { currency }; |
|
|
|
|
|
|
|
|
|
|
|
try { |
|
|
|
|
|
assetProfile = ( |
|
|
|
|
|
await this.dataProviderService.getAssetProfiles([ |
|
|
|
|
|
{ dataSource, symbol } |
|
|
|
|
|
]) |
|
|
|
|
|
)?.[symbol]; |
|
|
|
|
|
} catch {} |
|
|
|
|
|
|
|
|
|
|
|
if (!assetProfile?.name) { |
|
|
|
|
|
const assetProfileInImport = assetProfilesWithMarketDataDto?.find( |
|
|
|
|
|
(profile) => { |
|
|
|
|
|
return ( |
|
|
|
|
|
profile.dataSource === dataSource && profile.symbol === symbol |
|
|
|
|
|
); |
|
|
|
|
|
} |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
if (assetProfileInImport) { |
|
|
|
|
|
// Merge all fields of custom asset profiles into the validation object
|
|
|
|
|
|
Object.assign(assetProfile, { |
|
|
|
|
|
assetClass: assetProfileInImport.assetClass, |
|
|
|
|
|
assetSubClass: assetProfileInImport.assetSubClass, |
|
|
|
|
|
comment: assetProfileInImport.comment, |
|
|
|
|
|
countries: assetProfileInImport.countries, |
|
|
|
|
|
currency: assetProfileInImport.currency, |
|
|
|
|
|
cusip: assetProfileInImport.cusip, |
|
|
|
|
|
dataSource: assetProfileInImport.dataSource, |
|
|
|
|
|
figi: assetProfileInImport.figi, |
|
|
|
|
|
figiComposite: assetProfileInImport.figiComposite, |
|
|
|
|
|
figiShareClass: assetProfileInImport.figiShareClass, |
|
|
|
|
|
holdings: assetProfileInImport.holdings, |
|
|
|
|
|
isActive: assetProfileInImport.isActive, |
|
|
|
|
|
isin: assetProfileInImport.isin, |
|
|
|
|
|
name: assetProfileInImport.name, |
|
|
|
|
|
scraperConfiguration: assetProfileInImport.scraperConfiguration, |
|
|
|
|
|
sectors: assetProfileInImport.sectors, |
|
|
|
|
|
symbol: assetProfileInImport.symbol, |
|
|
|
|
|
symbolMapping: assetProfileInImport.symbolMapping, |
|
|
|
|
|
url: assetProfileInImport.url |
|
|
|
|
|
}); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (!['FEE', 'INTEREST', 'LIABILITY'].includes(type)) { |
|
|
|
|
|
if (!assetProfile?.name) { |
|
|
|
|
|
throw new Error( |
|
|
|
|
|
`activities.${index}.symbol ("${symbol}") is not valid for the specified data source ("${dataSource}")` |
|
|
|
|
|
); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
assetProfiles[getAssetProfileIdentifier({ dataSource, symbol })] = |
|
|
|
|
|
assetProfile; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return assetProfiles; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
} |
|
|
|