@ -11,11 +11,13 @@ import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/sy
import { TagService } from '@ghostfolio/api/services/tag/tag.service' ;
import { TagService } from '@ghostfolio/api/services/tag/tag.service' ;
import {
import {
DATA_GATHERING_QUEUE_PRIORITY_HIGH ,
DATA_GATHERING_QUEUE_PRIORITY_HIGH ,
ghostfolioPrefix ,
TAG_ID_EXCLUDE_FROM_ANALYSIS
TAG_ID_EXCLUDE_FROM_ANALYSIS
} from '@ghostfolio/common/config' ;
} from '@ghostfolio/common/config' ;
import { CreateAssetProfileDto , CreateOrderDto } from '@ghostfolio/common/dtos' ;
import { CreateAssetProfileDto , CreateOrderDto } from '@ghostfolio/common/dtos' ;
import {
import {
getAssetProfileIdentifier ,
getAssetProfileIdentifier ,
isValidCustomAssetProfileSymbol ,
parseDate
parseDate
} from '@ghostfolio/common/helper' ;
} from '@ghostfolio/common/helper' ;
import {
import {
@ -196,6 +198,41 @@ export class ImportService {
const tagIdMapping : { [ oldTagId : string ] : string } = { } ;
const tagIdMapping : { [ oldTagId : string ] : string } = { } ;
const userCurrency = user . settings . settings . baseCurrency ;
const userCurrency = user . settings . settings . baseCurrency ;
// Validate the symbols before any data is persisted
for ( const [ index , assetProfileWithMarketData ] of (
assetProfilesWithMarketDataDto ? ? [ ]
) . entries ( ) ) {
if (
assetProfileWithMarketData . dataSource === DataSource . MANUAL &&
! isValidCustomAssetProfileSymbol ( assetProfileWithMarketData . symbol )
) {
throw new Error (
` assetProfiles. ${ index } .symbol (" ${ assetProfileWithMarketData . symbol } ") must be a UUID or start with the prefix " ${ ghostfolioPrefix } _" for the data source (" ${ DataSource . MANUAL } ") `
) ;
}
}
// Validate the symbols before any data is persisted. Activities without a
// data source are excluded, since a symbol is generated in
// createActivity() if needed.
for ( const [ index , activity ] of activitiesDto . entries ( ) ) {
if ( ! activity . dataSource ) {
if ( [ 'FEE' , 'INTEREST' , 'LIABILITY' ] . includes ( activity . type ) ) {
activity . dataSource = DataSource . MANUAL ;
} else {
activity . dataSource =
this . dataProviderService . getDataSourceForImport ( ) ;
}
} else if (
activity . dataSource === DataSource . MANUAL &&
! isValidCustomAssetProfileSymbol ( activity . symbol )
) {
throw new Error (
` activities. ${ index } .symbol (" ${ activity . symbol } ") must be a UUID or start with the prefix " ${ ghostfolioPrefix } _" for the data source (" ${ DataSource . MANUAL } ") `
) ;
}
}
if ( platformsDto ? . length ) {
if ( platformsDto ? . length ) {
const canCreatePlatform = hasPermission (
const canCreatePlatform = hasPermission (
user . permissions ,
user . permissions ,
@ -384,39 +421,77 @@ export class ImportService {
}
}
}
}
if ( ! isDryRun && assetProfilesWithMarketDataDto ? . length ) {
if ( assetProfilesWithMarketDataDto ? . length ) {
const existingAssetProfiles =
const customAssetProfileNames = assetProfilesWithMarketDataDto
await this . symbolProfileService . getSymbolProfiles (
. filter ( ( { dataSource , name } ) = > {
return dataSource === DataSource . MANUAL && Boolean ( name ) ;
} )
. map ( ( { name } ) = > {
return name ;
} ) ;
const [ existingAssetProfiles , existingCustomAssetProfilesOfUser ] =
await Promise . all ( [
this . symbolProfileService . getSymbolProfiles (
assetProfilesWithMarketDataDto . map ( ( { dataSource , symbol } ) = > {
assetProfilesWithMarketDataDto . map ( ( { dataSource , symbol } ) = > {
return { dataSource , symbol } ;
return { dataSource , symbol } ;
} )
} )
) ;
) ,
this . symbolProfileService . getCustomSymbolProfilesByNames ( {
names : customAssetProfileNames ,
userId : user.id
} )
] ) ;
for ( const assetProfileWithMarketData of assetProfilesWithMarketDataDto ) {
for ( const assetProfileWithMarketData of assetProfilesWithMarketDataDto ) {
let symbol = assetProfileWithMarketData . symbol ;
// Check if there is any existing asset profile
// Check if there is any existing asset profile
const existingAssetProfile = existingAssetProfiles . find (
const existingAssetProfile = existingAssetProfiles . find (
( { dataSource , symbol } ) = > {
( assetProfile ) = > {
return (
return (
dataSource === assetProfileWithMarketData . dataSource &&
assetProfile . dataSource ===
symbol === assetProfileWithMarketData . symbol
assetProfileWithMarketData . dataSource &&
assetProfile . symbol === assetProfileWithMarketData . symbol
) ;
) ;
}
}
) ;
) ;
// If there is no asset profile or if the asset profile belongs to a different user, then create a new asset profile
// If there is no asset profile or if the asset profile belongs to a
// different user, then reuse the custom asset profile of the user or
// create a new asset profile
if ( ! existingAssetProfile || existingAssetProfile . userId !== user . id ) {
if ( ! existingAssetProfile || existingAssetProfile . userId !== user . id ) {
// Check if the user has a custom asset profile with the same name.
// Skip asset profiles with a legacy free-text symbol as they would
// fail the symbol validation on a future import.
const existingCustomAssetProfileOfUser =
assetProfileWithMarketData . dataSource === DataSource . MANUAL
? existingCustomAssetProfilesOfUser . find ( ( customAssetProfile ) = > {
return (
customAssetProfile . name ===
assetProfileWithMarketData . name &&
isValidCustomAssetProfileSymbol ( customAssetProfile . symbol )
) ;
} )
: undefined ;
if ( existingCustomAssetProfileOfUser ) {
// Reuse the custom asset profile of the user instead of creating a duplicate
symbol = existingCustomAssetProfileOfUser . symbol ;
} else {
const assetProfile : CreateAssetProfileDto = omit (
const assetProfile : CreateAssetProfileDto = omit (
assetProfileWithMarketData ,
assetProfileWithMarketData ,
'marketData'
'marketData'
) ;
) ;
// Asset profile belongs to a different user
// Asset profile belongs to a different user, generate a new symbol
if ( existingAssetProfile ) {
if ( existingAssetProfile && ! isDryRun ) {
const symbol = randomUUID ( ) ;
symbol = randomUUID ( ) ;
assetProfileSymbolMapping [ assetProfile . symbol ] = symbol ;
assetProfile . symbol = symbol ;
}
}
assetProfile . symbol = symbol ;
if ( ! isDryRun ) {
// Create a new asset profile
// Create a new asset profile
const assetProfileObject : Prisma.SymbolProfileCreateInput = {
const assetProfileObject : Prisma.SymbolProfileCreateInput = {
. . . assetProfile ,
. . . assetProfile ,
@ -425,30 +500,38 @@ export class ImportService {
await this . symbolProfileService . add ( assetProfileObject ) ;
await this . symbolProfileService . add ( assetProfileObject ) ;
}
}
}
if ( symbol !== assetProfileWithMarketData . symbol ) {
assetProfileSymbolMapping [ assetProfileWithMarketData . symbol ] =
symbol ;
// Keep the asset profile in sync with the activities to validate
assetProfileWithMarketData . symbol = symbol ;
}
}
if ( ! isDryRun ) {
// Insert or update market data
// Insert or update market data
const marketDataObjects = assetProfileWithMarketData . marketData . map (
const marketDataObjects = (
( marketData ) = > {
assetProfileWithMarketData . marketData ? ? [ ]
) . map ( ( marketData ) = > {
return {
return {
. . . marketData ,
. . . marketData ,
dataSource : assetProfileWithMarketData.dataSource ,
symbol ,
symbol : assetProfileWithMarketData . symbol
dataSource : assetProfileWithMarketData.dataSource
} as Prisma . MarketDataUpdateInput ;
} as Prisma . MarketDataUpdateInput ;
}
} ) ;
) ;
await this . marketDataService . updateMany ( { data : marketDataObjects } ) ;
await this . marketDataService . updateMany ( { data : marketDataObjects } ) ;
}
}
}
}
}
for ( const activity of activitiesDto ) {
for ( const activity of activitiesDto ) {
if ( ! activity . dataSource ) {
// If an asset profile is created or reused, then update the symbol in all activities
if ( [ 'FEE' , 'INTEREST' , 'LIABILITY' ] . includes ( activity . type ) ) {
if ( assetProfileSymbolMapping [ activity . symbol ] ) {
activity . dataSource = DataSource . MANUAL ;
activity . symbol = assetProfileSymbolMapping [ activity . symbol ] ;
} else {
activity . dataSource =
this . dataProviderService . getDataSourceForImport ( ) ;
}
}
}
if ( ! isDryRun ) {
if ( ! isDryRun ) {
@ -457,11 +540,6 @@ export class ImportService {
activity . accountId = accountIdMapping [ activity . accountId ] ;
activity . accountId = accountIdMapping [ activity . accountId ] ;
}
}
// If a new asset profile is created, then update the symbol in all activities
if ( assetProfileSymbolMapping [ activity . symbol ] ) {
activity . symbol = assetProfileSymbolMapping [ activity . symbol ] ;
}
// If a new tag is created, then update the tag ID in all activities
// If a new tag is created, then update the tag ID in all activities
activity . tags = ( activity . tags ? ? [ ] ) . map ( ( tagId ) = > {
activity . tags = ( activity . tags ? ? [ ] ) . map ( ( tagId ) = > {
return tagIdMapping [ tagId ] ? ? tagId ;
return tagIdMapping [ tagId ] ? ? tagId ;