|
|
@ -3,10 +3,15 @@ import { ActivitiesService } from '@ghostfolio/api/app/activities/activities.ser |
|
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.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 { DataGatheringService } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.service'; |
|
|
import { DataGatheringService } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.service'; |
|
|
|
|
|
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; |
|
|
import { TagService } from '@ghostfolio/api/services/tag/tag.service'; |
|
|
import { TagService } from '@ghostfolio/api/services/tag/tag.service'; |
|
|
import { NON_INVESTMENT_ACTIVITY_TYPES } from '@ghostfolio/common/config'; |
|
|
import { NON_INVESTMENT_ACTIVITY_TYPES } from '@ghostfolio/common/config'; |
|
|
import { CreateOrderDto } from '@ghostfolio/common/dtos'; |
|
|
import { |
|
|
|
|
|
CreateAssetProfileWithMarketDataDto, |
|
|
|
|
|
CreateOrderDto |
|
|
|
|
|
} from '@ghostfolio/common/dtos'; |
|
|
import { getAssetProfileIdentifier } from '@ghostfolio/common/helper'; |
|
|
import { getAssetProfileIdentifier } from '@ghostfolio/common/helper'; |
|
|
import { |
|
|
import { |
|
|
Activity, |
|
|
Activity, |
|
|
@ -15,6 +20,7 @@ import { |
|
|
import { UserWithSettings } from '@ghostfolio/common/types'; |
|
|
import { UserWithSettings } from '@ghostfolio/common/types'; |
|
|
|
|
|
|
|
|
import { DataSource, SymbolProfile } from '@prisma/client'; |
|
|
import { DataSource, SymbolProfile } from '@prisma/client'; |
|
|
|
|
|
import { isUUID } from 'class-validator'; |
|
|
import { parseISO } from 'date-fns'; |
|
|
import { parseISO } from 'date-fns'; |
|
|
|
|
|
|
|
|
import { ImportService } from './import.service'; |
|
|
import { ImportService } from './import.service'; |
|
|
@ -22,9 +28,11 @@ import { ImportService } from './import.service'; |
|
|
const CUSTOM_ASSET_PROFILE_SYMBOL = '1ad7d4a2-6b2d-4e0f-9b1f-2c0f8d3e5a7b'; |
|
|
const CUSTOM_ASSET_PROFILE_SYMBOL = '1ad7d4a2-6b2d-4e0f-9b1f-2c0f8d3e5a7b'; |
|
|
|
|
|
|
|
|
describe('ImportService', () => { |
|
|
describe('ImportService', () => { |
|
|
|
|
|
let addSymbolProfile: jest.Mock; |
|
|
let createActivity: jest.Mock; |
|
|
let createActivity: jest.Mock; |
|
|
let gatherSymbols: jest.Mock; |
|
|
let gatherSymbols: jest.Mock; |
|
|
let importService: ImportService; |
|
|
let importService: ImportService; |
|
|
|
|
|
let updateManyMarketData: jest.Mock; |
|
|
|
|
|
|
|
|
beforeEach(() => { |
|
|
beforeEach(() => { |
|
|
const configuration = { |
|
|
const configuration = { |
|
|
@ -33,8 +41,10 @@ describe('ImportService', () => { |
|
|
MAX_ACTIVITIES_TO_IMPORT: Number.MAX_SAFE_INTEGER |
|
|
MAX_ACTIVITIES_TO_IMPORT: Number.MAX_SAFE_INTEGER |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
addSymbolProfile = jest.fn(); |
|
|
createActivity = jest.fn(); |
|
|
createActivity = jest.fn(); |
|
|
gatherSymbols = jest.fn(); |
|
|
gatherSymbols = jest.fn(); |
|
|
|
|
|
updateManyMarketData = jest.fn(); |
|
|
|
|
|
|
|
|
const validateActivities = jest.fn( |
|
|
const validateActivities = jest.fn( |
|
|
({ activitiesDto }: { activitiesDto: CreateOrderDto[] }) => { |
|
|
({ activitiesDto }: { activitiesDto: CreateOrderDto[] }) => { |
|
|
@ -77,10 +87,14 @@ describe('ImportService', () => { |
|
|
{ |
|
|
{ |
|
|
toCurrencyAtDate: jest.fn().mockResolvedValue(0) |
|
|
toCurrencyAtDate: jest.fn().mockResolvedValue(0) |
|
|
} as unknown as ExchangeRateDataService, |
|
|
} as unknown as ExchangeRateDataService, |
|
|
|
|
|
{ updateMany: updateManyMarketData } as unknown as MarketDataService, |
|
|
null, |
|
|
null, |
|
|
null, |
|
|
null, |
|
|
null, |
|
|
{ |
|
|
null, |
|
|
add: addSymbolProfile, |
|
|
|
|
|
getCustomSymbolProfilesByNames: jest.fn().mockResolvedValue([]), |
|
|
|
|
|
getSymbolProfiles: jest.fn().mockResolvedValue([]) |
|
|
|
|
|
} as unknown as SymbolProfileService, |
|
|
{ |
|
|
{ |
|
|
getTagsForUser: jest.fn().mockResolvedValue([]) |
|
|
getTagsForUser: jest.fn().mockResolvedValue([]) |
|
|
} as unknown as TagService |
|
|
} as unknown as TagService |
|
|
@ -275,6 +289,146 @@ describe('ImportService', () => { |
|
|
]); |
|
|
]); |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
it('creates a custom asset profile with a UUID instead of a symbol with the prefix', async () => { |
|
|
|
|
|
mockCreatedAssetProfiles([ |
|
|
|
|
|
{ dataSource: DataSource.MANUAL, symbol: CUSTOM_ASSET_PROFILE_SYMBOL } |
|
|
|
|
|
]); |
|
|
|
|
|
|
|
|
|
|
|
await importActivities({ |
|
|
|
|
|
activitiesDto: [ |
|
|
|
|
|
createActivityDto({ |
|
|
|
|
|
dataSource: DataSource.MANUAL, |
|
|
|
|
|
date: '2024-01-01T00:00:00.000Z', |
|
|
|
|
|
symbol: 'GF_COPX', |
|
|
|
|
|
type: 'BUY' |
|
|
|
|
|
}) |
|
|
|
|
|
], |
|
|
|
|
|
assetProfilesWithMarketDataDto: [ |
|
|
|
|
|
{ |
|
|
|
|
|
currency: 'USD', |
|
|
|
|
|
dataSource: DataSource.MANUAL, |
|
|
|
|
|
marketData: [], |
|
|
|
|
|
name: 'Global X Copper Miners ETF', |
|
|
|
|
|
symbol: 'GF_COPX' |
|
|
|
|
|
} |
|
|
|
|
|
] |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
const { symbol } = addSymbolProfile.mock.calls[0][0]; |
|
|
|
|
|
|
|
|
|
|
|
expect(isUUID(symbol)).toBe(true); |
|
|
|
|
|
|
|
|
|
|
|
expect( |
|
|
|
|
|
createActivity.mock.calls[0][0].SymbolProfile.connectOrCreate.create |
|
|
|
|
|
).toMatchObject({ symbol, dataSource: DataSource.MANUAL }); |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
it('keeps the symbol with the prefix as the name of a custom asset profile without a name', async () => { |
|
|
|
|
|
mockCreatedAssetProfiles([ |
|
|
|
|
|
{ dataSource: DataSource.MANUAL, symbol: CUSTOM_ASSET_PROFILE_SYMBOL } |
|
|
|
|
|
]); |
|
|
|
|
|
|
|
|
|
|
|
await importActivities({ |
|
|
|
|
|
activitiesDto: [ |
|
|
|
|
|
createActivityDto({ |
|
|
|
|
|
dataSource: DataSource.MANUAL, |
|
|
|
|
|
date: '2024-01-01T00:00:00.000Z', |
|
|
|
|
|
symbol: 'GF_COPX', |
|
|
|
|
|
type: 'BUY' |
|
|
|
|
|
}) |
|
|
|
|
|
], |
|
|
|
|
|
assetProfilesWithMarketDataDto: [ |
|
|
|
|
|
{ |
|
|
|
|
|
currency: 'USD', |
|
|
|
|
|
dataSource: DataSource.MANUAL, |
|
|
|
|
|
marketData: [], |
|
|
|
|
|
symbol: 'GF_COPX' |
|
|
|
|
|
} |
|
|
|
|
|
] |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
expect(addSymbolProfile.mock.calls[0][0].name).toBe('GF_COPX'); |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
it('merges the market data of asset profiles with the same symbol with the prefix', async () => { |
|
|
|
|
|
mockCreatedAssetProfiles([ |
|
|
|
|
|
{ dataSource: DataSource.MANUAL, symbol: CUSTOM_ASSET_PROFILE_SYMBOL } |
|
|
|
|
|
]); |
|
|
|
|
|
|
|
|
|
|
|
await importActivities({ |
|
|
|
|
|
activitiesDto: [ |
|
|
|
|
|
createActivityDto({ |
|
|
|
|
|
dataSource: DataSource.MANUAL, |
|
|
|
|
|
date: '2024-01-01T00:00:00.000Z', |
|
|
|
|
|
symbol: 'GF_COPX', |
|
|
|
|
|
type: 'BUY' |
|
|
|
|
|
}) |
|
|
|
|
|
], |
|
|
|
|
|
assetProfilesWithMarketDataDto: [ |
|
|
|
|
|
{ |
|
|
|
|
|
currency: 'USD', |
|
|
|
|
|
dataSource: DataSource.MANUAL, |
|
|
|
|
|
marketData: [{ date: '2024-01-01', marketPrice: 100 }], |
|
|
|
|
|
name: 'Global X Copper Miners ETF', |
|
|
|
|
|
symbol: 'GF_COPX' |
|
|
|
|
|
}, |
|
|
|
|
|
{ |
|
|
|
|
|
currency: 'USD', |
|
|
|
|
|
dataSource: DataSource.MANUAL, |
|
|
|
|
|
marketData: [{ date: '2024-01-02', marketPrice: 101 }], |
|
|
|
|
|
name: 'Global X Copper Miners ETF', |
|
|
|
|
|
symbol: 'GF_COPX' |
|
|
|
|
|
} |
|
|
|
|
|
] |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
const { symbol } = addSymbolProfile.mock.calls[0][0]; |
|
|
|
|
|
|
|
|
|
|
|
expect(addSymbolProfile).toHaveBeenCalledTimes(1); |
|
|
|
|
|
|
|
|
|
|
|
expect(updateManyMarketData.mock.calls[0][0].data).toMatchObject([ |
|
|
|
|
|
{ symbol, date: '2024-01-01', marketPrice: 100 }, |
|
|
|
|
|
{ symbol, date: '2024-01-02', marketPrice: 101 } |
|
|
|
|
|
]); |
|
|
|
|
|
|
|
|
|
|
|
expect( |
|
|
|
|
|
createActivity.mock.calls[0][0].SymbolProfile.connectOrCreate.create |
|
|
|
|
|
).toMatchObject({ symbol, dataSource: DataSource.MANUAL }); |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
it('refers the next activities with a symbol with the prefix to the custom asset profile which createActivity() has created', async () => { |
|
|
|
|
|
mockCreatedAssetProfiles([ |
|
|
|
|
|
{ dataSource: DataSource.MANUAL, symbol: CUSTOM_ASSET_PROFILE_SYMBOL }, |
|
|
|
|
|
{ dataSource: DataSource.MANUAL, symbol: CUSTOM_ASSET_PROFILE_SYMBOL } |
|
|
|
|
|
]); |
|
|
|
|
|
|
|
|
|
|
|
await importActivities({ |
|
|
|
|
|
activitiesDto: [ |
|
|
|
|
|
createActivityDto({ |
|
|
|
|
|
dataSource: DataSource.MANUAL, |
|
|
|
|
|
date: '2024-01-01T00:00:00.000Z', |
|
|
|
|
|
symbol: 'GF_COPX', |
|
|
|
|
|
type: 'BUY' |
|
|
|
|
|
}), |
|
|
|
|
|
createActivityDto({ |
|
|
|
|
|
dataSource: DataSource.MANUAL, |
|
|
|
|
|
date: '2024-01-02T00:00:00.000Z', |
|
|
|
|
|
symbol: 'GF_COPX', |
|
|
|
|
|
type: 'BUY' |
|
|
|
|
|
}) |
|
|
|
|
|
] |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
expect( |
|
|
|
|
|
createActivity.mock.calls.map(([activity]) => { |
|
|
|
|
|
return activity.SymbolProfile.connectOrCreate.create; |
|
|
|
|
|
}) |
|
|
|
|
|
).toMatchObject([ |
|
|
|
|
|
{ dataSource: DataSource.MANUAL, symbol: 'GF_COPX' }, |
|
|
|
|
|
{ dataSource: DataSource.MANUAL, symbol: CUSTOM_ASSET_PROFILE_SYMBOL } |
|
|
|
|
|
]); |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
it('keeps the asset profiles of the activities in a dry run', async () => { |
|
|
it('keeps the asset profiles of the activities in a dry run', async () => { |
|
|
const activities = await importActivities({ |
|
|
const activities = await importActivities({ |
|
|
activitiesDto: [ |
|
|
activitiesDto: [ |
|
|
@ -306,16 +460,18 @@ describe('ImportService', () => { |
|
|
|
|
|
|
|
|
function importActivities({ |
|
|
function importActivities({ |
|
|
activitiesDto, |
|
|
activitiesDto, |
|
|
|
|
|
assetProfilesWithMarketDataDto = [], |
|
|
isDryRun |
|
|
isDryRun |
|
|
}: { |
|
|
}: { |
|
|
activitiesDto: CreateOrderDto[]; |
|
|
activitiesDto: CreateOrderDto[]; |
|
|
|
|
|
assetProfilesWithMarketDataDto?: CreateAssetProfileWithMarketDataDto[]; |
|
|
isDryRun?: boolean; |
|
|
isDryRun?: boolean; |
|
|
}) { |
|
|
}) { |
|
|
return importService.import({ |
|
|
return importService.import({ |
|
|
activitiesDto, |
|
|
activitiesDto, |
|
|
|
|
|
assetProfilesWithMarketDataDto, |
|
|
isDryRun, |
|
|
isDryRun, |
|
|
accountsWithBalancesDto: [], |
|
|
accountsWithBalancesDto: [], |
|
|
assetProfilesWithMarketDataDto: [], |
|
|
|
|
|
platformsDto: [], |
|
|
platformsDto: [], |
|
|
tagsDto: [], |
|
|
tagsDto: [], |
|
|
user: { |
|
|
user: { |
|
|
|