|
@ -8,10 +8,14 @@ import { |
|
|
import { OrderService } from '@ghostfolio/api/app/order/order.service'; |
|
|
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 { DataGatheringService } from '@ghostfolio/api/services/data-gathering/data-gathering.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 { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; |
|
|
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; |
|
|
import { parseDate } from '@ghostfolio/common/helper'; |
|
|
import { |
|
|
|
|
|
getAssetProfileIdentifier, |
|
|
|
|
|
parseDate |
|
|
|
|
|
} from '@ghostfolio/common/helper'; |
|
|
import { UniqueAsset } from '@ghostfolio/common/interfaces'; |
|
|
import { UniqueAsset } from '@ghostfolio/common/interfaces'; |
|
|
import { |
|
|
import { |
|
|
AccountWithPlatform, |
|
|
AccountWithPlatform, |
|
@ -21,12 +25,14 @@ import { Injectable } from '@nestjs/common'; |
|
|
import { DataSource, Prisma, SymbolProfile } from '@prisma/client'; |
|
|
import { DataSource, Prisma, SymbolProfile } from '@prisma/client'; |
|
|
import Big from 'big.js'; |
|
|
import Big from 'big.js'; |
|
|
import { endOfToday, isAfter, isSameDay, parseISO } from 'date-fns'; |
|
|
import { endOfToday, isAfter, isSameDay, parseISO } from 'date-fns'; |
|
|
|
|
|
import { uniqBy } from 'lodash'; |
|
|
import { v4 as uuidv4 } from 'uuid'; |
|
|
import { v4 as uuidv4 } from 'uuid'; |
|
|
|
|
|
|
|
|
@Injectable() |
|
|
@Injectable() |
|
|
export class ImportService { |
|
|
export class ImportService { |
|
|
public constructor( |
|
|
public constructor( |
|
|
private readonly accountService: AccountService, |
|
|
private readonly accountService: AccountService, |
|
|
|
|
|
private readonly dataGatheringService: DataGatheringService, |
|
|
private readonly dataProviderService: DataProviderService, |
|
|
private readonly dataProviderService: DataProviderService, |
|
|
private readonly exchangeRateDataService: ExchangeRateDataService, |
|
|
private readonly exchangeRateDataService: ExchangeRateDataService, |
|
|
private readonly orderService: OrderService, |
|
|
private readonly orderService: OrderService, |
|
@ -220,8 +226,7 @@ export class ImportService { |
|
|
|
|
|
|
|
|
const assetProfiles = await this.validateActivities({ |
|
|
const assetProfiles = await this.validateActivities({ |
|
|
activitiesDto, |
|
|
activitiesDto, |
|
|
maxActivitiesToImport, |
|
|
maxActivitiesToImport |
|
|
userId |
|
|
|
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
const activitiesExtendedWithErrors = await this.extendActivitiesWithErrors({ |
|
|
const activitiesExtendedWithErrors = await this.extendActivitiesWithErrors({ |
|
@ -250,10 +255,37 @@ export class ImportService { |
|
|
error, |
|
|
error, |
|
|
fee, |
|
|
fee, |
|
|
quantity, |
|
|
quantity, |
|
|
SymbolProfile: assetProfile, |
|
|
SymbolProfile, |
|
|
type, |
|
|
type, |
|
|
unitPrice |
|
|
unitPrice |
|
|
} of activitiesExtendedWithErrors) { |
|
|
} of activitiesExtendedWithErrors) { |
|
|
|
|
|
const assetProfile = assetProfiles[ |
|
|
|
|
|
getAssetProfileIdentifier({ |
|
|
|
|
|
dataSource: SymbolProfile.dataSource, |
|
|
|
|
|
symbol: SymbolProfile.symbol |
|
|
|
|
|
}) |
|
|
|
|
|
] ?? { |
|
|
|
|
|
currency: SymbolProfile.currency, |
|
|
|
|
|
dataSource: SymbolProfile.dataSource, |
|
|
|
|
|
symbol: SymbolProfile.symbol |
|
|
|
|
|
}; |
|
|
|
|
|
const { |
|
|
|
|
|
assetClass, |
|
|
|
|
|
assetSubClass, |
|
|
|
|
|
countries, |
|
|
|
|
|
createdAt, |
|
|
|
|
|
currency, |
|
|
|
|
|
dataSource, |
|
|
|
|
|
id, |
|
|
|
|
|
isin, |
|
|
|
|
|
name, |
|
|
|
|
|
scraperConfiguration, |
|
|
|
|
|
sectors, |
|
|
|
|
|
symbol, |
|
|
|
|
|
symbolMapping, |
|
|
|
|
|
url, |
|
|
|
|
|
updatedAt |
|
|
|
|
|
} = assetProfile; |
|
|
const validatedAccount = accounts.find(({ id }) => { |
|
|
const validatedAccount = accounts.find(({ id }) => { |
|
|
return id === accountId; |
|
|
return id === accountId; |
|
|
}); |
|
|
}); |
|
@ -279,23 +311,22 @@ export class ImportService { |
|
|
id: uuidv4(), |
|
|
id: uuidv4(), |
|
|
isDraft: isAfter(date, endOfToday()), |
|
|
isDraft: isAfter(date, endOfToday()), |
|
|
SymbolProfile: { |
|
|
SymbolProfile: { |
|
|
assetClass: assetProfile.assetClass, |
|
|
assetClass, |
|
|
assetSubClass: assetProfile.assetSubClass, |
|
|
assetSubClass, |
|
|
comment: assetProfile.comment, |
|
|
countries, |
|
|
countries: assetProfile.countries, |
|
|
createdAt, |
|
|
createdAt: assetProfile.createdAt, |
|
|
currency, |
|
|
currency: assetProfile.currency, |
|
|
dataSource, |
|
|
dataSource: assetProfile.dataSource, |
|
|
id, |
|
|
id: assetProfile.id, |
|
|
isin, |
|
|
isin: assetProfile.isin, |
|
|
name, |
|
|
name: assetProfile.name, |
|
|
scraperConfiguration, |
|
|
scraperConfiguration: assetProfile.scraperConfiguration, |
|
|
sectors, |
|
|
sectors: assetProfile.sectors, |
|
|
symbol, |
|
|
symbol: assetProfile.currency, |
|
|
symbolMapping, |
|
|
symbolMapping: assetProfile.symbolMapping, |
|
|
updatedAt, |
|
|
updatedAt: assetProfile.updatedAt, |
|
|
url, |
|
|
url: assetProfile.url, |
|
|
comment: assetProfile.comment |
|
|
...assetProfiles[assetProfile.symbol] |
|
|
|
|
|
}, |
|
|
}, |
|
|
Account: validatedAccount, |
|
|
Account: validatedAccount, |
|
|
symbolProfileId: undefined, |
|
|
symbolProfileId: undefined, |
|
@ -318,14 +349,14 @@ export class ImportService { |
|
|
SymbolProfile: { |
|
|
SymbolProfile: { |
|
|
connectOrCreate: { |
|
|
connectOrCreate: { |
|
|
create: { |
|
|
create: { |
|
|
currency: assetProfile.currency, |
|
|
currency, |
|
|
dataSource: assetProfile.dataSource, |
|
|
dataSource, |
|
|
symbol: assetProfile.symbol |
|
|
symbol |
|
|
}, |
|
|
}, |
|
|
where: { |
|
|
where: { |
|
|
dataSource_symbol: { |
|
|
dataSource_symbol: { |
|
|
dataSource: assetProfile.dataSource, |
|
|
dataSource, |
|
|
symbol: assetProfile.symbol |
|
|
symbol |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
@ -337,24 +368,49 @@ export class ImportService { |
|
|
|
|
|
|
|
|
const value = new Big(quantity).mul(unitPrice).toNumber(); |
|
|
const value = new Big(quantity).mul(unitPrice).toNumber(); |
|
|
|
|
|
|
|
|
//@ts-ignore
|
|
|
|
|
|
activities.push({ |
|
|
activities.push({ |
|
|
...order, |
|
|
...order, |
|
|
error, |
|
|
error, |
|
|
value, |
|
|
value, |
|
|
feeInBaseCurrency: this.exchangeRateDataService.toCurrency( |
|
|
feeInBaseCurrency: this.exchangeRateDataService.toCurrency( |
|
|
fee, |
|
|
fee, |
|
|
assetProfile.currency, |
|
|
currency, |
|
|
userCurrency |
|
|
userCurrency |
|
|
), |
|
|
), |
|
|
|
|
|
//@ts-ignore
|
|
|
|
|
|
SymbolProfile: assetProfile, |
|
|
valueInBaseCurrency: this.exchangeRateDataService.toCurrency( |
|
|
valueInBaseCurrency: this.exchangeRateDataService.toCurrency( |
|
|
value, |
|
|
value, |
|
|
assetProfile.currency, |
|
|
currency, |
|
|
userCurrency |
|
|
userCurrency |
|
|
) |
|
|
) |
|
|
}); |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
activities.sort((activity1, activity2) => { |
|
|
|
|
|
return Number(activity1.date) - Number(activity2.date); |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
if (!isDryRun) { |
|
|
|
|
|
// Gather symbol data in the background, if not dry run
|
|
|
|
|
|
const uniqueActivities = uniqBy(activities, ({ SymbolProfile }) => { |
|
|
|
|
|
return getAssetProfileIdentifier({ |
|
|
|
|
|
dataSource: SymbolProfile.dataSource, |
|
|
|
|
|
symbol: SymbolProfile.symbol |
|
|
|
|
|
}); |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
this.dataGatheringService.gatherSymbols( |
|
|
|
|
|
uniqueActivities.map(({ date, SymbolProfile }) => { |
|
|
|
|
|
return { |
|
|
|
|
|
date, |
|
|
|
|
|
dataSource: SymbolProfile.dataSource, |
|
|
|
|
|
symbol: SymbolProfile.symbol |
|
|
|
|
|
}; |
|
|
|
|
|
}) |
|
|
|
|
|
); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
return activities; |
|
|
return activities; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
@ -446,25 +502,30 @@ export class ImportService { |
|
|
|
|
|
|
|
|
private async validateActivities({ |
|
|
private async validateActivities({ |
|
|
activitiesDto, |
|
|
activitiesDto, |
|
|
maxActivitiesToImport, |
|
|
maxActivitiesToImport |
|
|
userId |
|
|
|
|
|
}: { |
|
|
}: { |
|
|
activitiesDto: Partial<CreateOrderDto>[]; |
|
|
activitiesDto: Partial<CreateOrderDto>[]; |
|
|
maxActivitiesToImport: number; |
|
|
maxActivitiesToImport: number; |
|
|
userId: string; |
|
|
|
|
|
}) { |
|
|
}) { |
|
|
if (activitiesDto?.length > maxActivitiesToImport) { |
|
|
if (activitiesDto?.length > maxActivitiesToImport) { |
|
|
throw new Error(`Too many activities (${maxActivitiesToImport} at most)`); |
|
|
throw new Error(`Too many activities (${maxActivitiesToImport} at most)`); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const assetProfiles: { |
|
|
const assetProfiles: { |
|
|
[symbol: string]: Partial<SymbolProfile>; |
|
|
[assetProfileIdentifier: string]: Partial<SymbolProfile>; |
|
|
} = {}; |
|
|
} = {}; |
|
|
|
|
|
|
|
|
|
|
|
const uniqueActivitiesDto = uniqBy( |
|
|
|
|
|
activitiesDto, |
|
|
|
|
|
({ dataSource, symbol }) => { |
|
|
|
|
|
return getAssetProfileIdentifier({ dataSource, symbol }); |
|
|
|
|
|
} |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
for (const [ |
|
|
for (const [ |
|
|
index, |
|
|
index, |
|
|
{ currency, dataSource, symbol } |
|
|
{ currency, dataSource, symbol } |
|
|
] of activitiesDto.entries()) { |
|
|
] of uniqueActivitiesDto.entries()) { |
|
|
if (dataSource !== 'MANUAL') { |
|
|
if (dataSource !== 'MANUAL') { |
|
|
const assetProfile = ( |
|
|
const assetProfile = ( |
|
|
await this.dataProviderService.getAssetProfiles([ |
|
|
await this.dataProviderService.getAssetProfiles([ |
|
@ -484,7 +545,8 @@ export class ImportService { |
|
|
); |
|
|
); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
assetProfiles[symbol] = assetProfile; |
|
|
assetProfiles[getAssetProfileIdentifier({ dataSource, symbol })] = |
|
|
|
|
|
assetProfile; |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|