Browse Source

Reuse global asset profiles in activities import without changing their market data

pull/8246/head
Andrea Bugeja 5 days ago
parent
commit
061d21e759
Failed to extract signature
  1. 4
      CHANGELOG.md
  2. 82
      apps/api/src/app/import/import.service.spec.ts
  3. 7
      apps/api/src/app/import/import.service.ts

4
CHANGELOG.md

@ -15,6 +15,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Improved the performance of the historical market data table of the admin control panel by loading only the latest market price per asset profile
- Introduced a cooldown of 12 hours for the asset profile and exchange rate gathering on activity creation by retaining the completed jobs
### Fixed
- Fixed an issue in the activities import where global asset profiles were duplicated as custom asset profiles
## 3.88.0 - 2026-10-17
### Added

82
apps/api/src/app/import/import.service.spec.ts

@ -33,6 +33,7 @@ describe('ImportService', () => {
let addSymbolProfile: jest.Mock;
let createActivity: jest.Mock;
let gatherSymbols: jest.Mock;
let getSymbolProfiles: jest.Mock;
let importService: ImportService;
let updateManyMarketData: jest.Mock;
@ -46,6 +47,7 @@ describe('ImportService', () => {
addSymbolProfile = jest.fn();
createActivity = jest.fn();
gatherSymbols = jest.fn();
getSymbolProfiles = jest.fn().mockResolvedValue([]);
updateManyMarketData = jest.fn();
const validateActivities = jest.fn(
@ -94,8 +96,8 @@ describe('ImportService', () => {
null,
{
add: addSymbolProfile,
getCustomSymbolProfilesByNames: jest.fn().mockResolvedValue([]),
getSymbolProfiles: jest.fn().mockResolvedValue([])
getSymbolProfiles,
getCustomSymbolProfilesByNames: jest.fn().mockResolvedValue([])
} as unknown as SymbolProfileService,
{
getTagsForUser: jest.fn().mockResolvedValue([])
@ -481,6 +483,82 @@ describe('ImportService', () => {
]);
});
it('reuses a global asset profile without updating its market data', async () => {
getSymbolProfiles.mockResolvedValue([
{ dataSource: DataSource.YAHOO, symbol: 'AAPL', userId: null }
]);
mockCreatedAssetProfiles([
{ dataSource: DataSource.YAHOO, symbol: 'AAPL' }
]);
await importActivities({
activitiesDto: [
createActivityDto({
dataSource: DataSource.YAHOO,
date: '2024-01-01T00:00:00.000Z',
symbol: 'AAPL',
type: 'BUY'
})
],
assetProfilesWithMarketDataDto: [
{
currency: 'USD',
dataSource: DataSource.YAHOO,
marketData: [{ date: '2024-01-01', marketPrice: 1 }],
name: 'Apple Inc.',
symbol: 'AAPL'
}
]
});
expect(addSymbolProfile).not.toHaveBeenCalled();
expect(updateManyMarketData).not.toHaveBeenCalled();
expect(
createActivity.mock.calls[0][0].SymbolProfile.connectOrCreate.create
).toMatchObject({ dataSource: DataSource.YAHOO, symbol: 'AAPL' });
});
it('creates a custom asset profile with a UUID instead of the asset profile of another user', async () => {
getSymbolProfiles.mockResolvedValue([
{
dataSource: DataSource.MANUAL,
symbol: CUSTOM_ASSET_PROFILE_SYMBOL,
userId: 'other-user-id'
}
]);
mockCreatedAssetProfiles([
{ dataSource: DataSource.MANUAL, symbol: CUSTOM_ASSET_PROFILE_SYMBOL }
]);
await importActivities({
activitiesDto: [
createActivityDto({
dataSource: DataSource.MANUAL,
date: '2024-01-01T00:00:00.000Z',
symbol: CUSTOM_ASSET_PROFILE_SYMBOL,
type: 'BUY'
})
],
assetProfilesWithMarketDataDto: [
{
currency: 'USD',
dataSource: DataSource.MANUAL,
marketData: [{ date: '2024-01-01', marketPrice: 100 }],
name: 'Gold',
symbol: CUSTOM_ASSET_PROFILE_SYMBOL
}
]
});
const { symbol } = addSymbolProfile.mock.calls[0][0];
expect(isUUID(symbol)).toBe(true);
expect(symbol).not.toBe(CUSTOM_ASSET_PROFILE_SYMBOL);
});
it('keeps the asset profiles of the activities in a dry run', async () => {
const activities = await importActivities({
activitiesDto: [

7
apps/api/src/app/import/import.service.ts

@ -624,10 +624,12 @@ export class ImportService {
}
);
const isGlobalAssetProfile = existingAssetProfile?.userId === null;
// 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?.userId !== user.id) {
if (!isGlobalAssetProfile && 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.
@ -689,7 +691,8 @@ export class ImportService {
}
}
if (!isDryRun) {
// Never let an import change the market data of a global asset profile
if (!isDryRun && !isGlobalAssetProfile) {
const marketDataObjects = (
assetProfileWithMarketData.marketData ?? []
).map((marketData) => {

Loading…
Cancel
Save