Browse Source

Merge b75e8b8dd3 into e753453613

pull/7507/merge
CoderVJain 3 weeks ago
committed by GitHub
parent
commit
16be8a1818
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 135
      apps/api/src/app/import/import.service.spec.ts
  3. 31
      apps/api/src/app/import/import.service.ts

1
CHANGELOG.md

@ -133,6 +133,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Fixed the duplicate detection during the activity import so it treats an omitted comment the same as null and matches holdings imported by ISIN against holdings imported by ticker symbol
- Fixed the calendar year date range in time zones with a negative _UTC_ offset
- Fixed the deletion of activities to respect the activity type filter on the activities page (experimental)
- Fixed the deletion of activities to respect the date range filter on the activities page

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

@ -0,0 +1,135 @@
import { CreateOrderDto } from '@ghostfolio/common/dtos';
import { DataSource, Type } from '@prisma/client';
import { ImportService } from './import.service';
jest.mock('@ghostfolio/api/app/activities/activities.service', () => {
return {
ActivitiesService: jest.fn().mockImplementation(() => {
return {
getActivities: () => {
return Promise.resolve({
activities: [
{
accountId: 'df1c6156-9e17-4434-93c8-6ee4e15c8c1d',
comment: null,
currency: 'USD',
date: new Date('2025-05-09T13:00:28.000Z'),
fee: 0.35074925,
quantity: 2,
type: Type.BUY,
unitPrice: 102.548,
assetProfile: {
currency: 'USD',
dataSource: DataSource.YAHOO,
isin: 'US0079031078',
symbol: 'US0079031078'
}
}
]
});
}
};
})
};
});
jest.mock(
'@ghostfolio/api/services/symbol-profile/symbol-profile.service',
() => {
return {
SymbolProfileService: jest.fn().mockImplementation(() => {
return {
getSymbolProfiles: (
assetProfileIdentifiers: {
dataSource: DataSource;
symbol: string;
}[]
) => {
return Promise.resolve(
assetProfileIdentifiers
.filter(({ symbol }) => {
return symbol === 'AMD';
})
.map(({ dataSource, symbol }) => {
return { dataSource, symbol, isin: 'US0079031078' };
})
);
}
};
})
};
}
);
describe('ImportService', () => {
let importService: ImportService;
beforeAll(() => {
importService = new ImportService(
null,
new (jest.requireMock(
'@ghostfolio/api/app/activities/activities.service'
).ActivitiesService)(),
null,
null,
null,
null,
null,
null,
null,
new (jest.requireMock(
'@ghostfolio/api/services/symbol-profile/symbol-profile.service'
).SymbolProfileService)(),
null
);
});
function buildActivityDto(
overrides: Partial<CreateOrderDto> = {}
): Partial<CreateOrderDto> {
return {
accountId: 'df1c6156-9e17-4434-93c8-6ee4e15c8c1d',
currency: 'USD',
dataSource: DataSource.YAHOO,
date: '2025-05-09T13:00:28.000Z',
fee: 0.35074925,
quantity: 2,
symbol: 'US0079031078',
type: Type.BUY,
unitPrice: 102.548,
...overrides
};
}
it('flags a duplicate when the comment is omitted instead of null', async () => {
const [activity] = await (importService as any).extendActivitiesWithErrors({
activitiesDto: [buildActivityDto()],
userCurrency: 'USD',
userId: 'da09d1fa-b8e2-40a1-9e5a-decd1cbb63b1'
});
expect(activity.error).toEqual({ code: 'IS_DUPLICATE' });
});
it('flags a duplicate when the same holding is imported by ticker symbol instead of ISIN', async () => {
const [activity] = await (importService as any).extendActivitiesWithErrors({
activitiesDto: [buildActivityDto({ symbol: 'AMD' })],
userCurrency: 'USD',
userId: 'da09d1fa-b8e2-40a1-9e5a-decd1cbb63b1'
});
expect(activity.error).toEqual({ code: 'IS_DUPLICATE' });
});
it('does not flag a different holding as a duplicate', async () => {
const [activity] = await (importService as any).extendActivitiesWithErrors({
activitiesDto: [buildActivityDto({ symbol: 'MSFT' })],
userCurrency: 'USD',
userId: 'da09d1fa-b8e2-40a1-9e5a-decd1cbb63b1'
});
expect(activity.error).toBeUndefined();
});
});

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

@ -811,6 +811,18 @@ export class ImportService {
withExcludedAccountsAndActivities: true
});
const incomingAssetProfiles =
await this.symbolProfileService.getSymbolProfiles(
uniqBy(
activitiesDto.map(({ dataSource, symbol }) => {
return { dataSource, symbol };
}),
({ dataSource, symbol }) => {
return getAssetProfileIdentifier({ dataSource, symbol });
}
)
);
return activitiesDto.map(
({
accountId,
@ -826,17 +838,30 @@ export class ImportService {
unitPrice
}) => {
const date = parseISO(dateString);
const incomingIsin = incomingAssetProfiles.find((assetProfile) => {
return (
assetProfile.dataSource === dataSource &&
assetProfile.symbol === symbol
);
})?.isin;
const isDuplicate = existingActivities.some((activity) => {
const isSameAssetProfile =
(activity.assetProfile.dataSource === dataSource &&
activity.assetProfile.symbol === symbol) ||
(!!activity.assetProfile.isin &&
activity.assetProfile.isin === incomingIsin);
return (
activity.accountId === accountId &&
activity.comment === comment &&
(activity.comment ?? null) === (comment ?? null) &&
(activity.currency === currency ||
activity.assetProfile.currency === currency) &&
activity.assetProfile.dataSource === dataSource &&
isSameAssetProfile &&
isSameSecond(activity.date, date) &&
activity.fee === fee &&
activity.quantity === quantity &&
activity.assetProfile.symbol === symbol &&
activity.type === type &&
activity.unitPrice === unitPrice
);

Loading…
Cancel
Save