Browse Source

Bugfix/unused custom asset profiles created by activities import (#7673)

* Fix unused custom asset profiles created by activities import

* Update changelog
pull/7693/head
Thomas Kaul 6 days ago
committed by GitHub
parent
commit
96d56dfb2d
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 90
      apps/api/src/app/import/import.service.ts
  3. 6
      apps/api/src/app/import/interfaces/asset-profile-to-create.interface.ts
  4. 40
      apps/api/src/services/data-provider/data-provider.service.ts

1
CHANGELOG.md

@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Fixed the _Storybook_ setup by loading the `@angular/localize` polyfill centrally
- Fixed an issue in the activities import where an unused custom asset profile was created if the related activities were not imported
## 3.57.0 - 2026-08-21

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

@ -48,6 +48,7 @@ import { omit, uniqBy } from 'lodash';
import { randomUUID } from 'node:crypto';
import { ImportDataDto } from './import-data.dto';
import { AssetProfileToCreate } from './interfaces/asset-profile-to-create.interface';
@Injectable()
export class ImportService {
@ -534,6 +535,8 @@ export class ImportService {
}
}
const assetProfilesToCreate: AssetProfileToCreate[] = [];
if (assetProfilesWithMarketDataDto?.length) {
const customAssetProfileNames = assetProfilesWithMarketDataDto
.filter(({ dataSource, name }) => {
@ -557,6 +560,7 @@ export class ImportService {
]);
for (const assetProfileWithMarketData of assetProfilesWithMarketDataDto) {
let assetProfileToCreate: Prisma.SymbolProfileCreateInput;
let symbol = assetProfileWithMarketData.symbol;
// Check if there is any existing asset profile
@ -605,13 +609,10 @@ export class ImportService {
assetProfile.symbol = symbol;
if (!isDryRun) {
// Create a new asset profile
const assetProfileObject: Prisma.SymbolProfileCreateInput = {
assetProfileToCreate = {
...assetProfile,
user: { connect: { id: user.id } }
};
await this.symbolProfileService.add(assetProfileObject);
}
}
@ -625,7 +626,6 @@ export class ImportService {
}
if (!isDryRun) {
// Insert or update market data
const marketDataObjects = (
assetProfileWithMarketData.marketData ?? []
).map((marketData) => {
@ -636,7 +636,40 @@ export class ImportService {
} as Prisma.MarketDataUpdateInput;
});
await this.marketDataService.updateMany({ data: marketDataObjects });
if (assetProfileToCreate) {
const assetProfileToCreateIdentifier =
getAssetProfileIdentifier(assetProfileToCreate);
const duplicateAssetProfileToCreate = assetProfilesToCreate.find(
({ assetProfile }) => {
return (
getAssetProfileIdentifier(assetProfile) ===
assetProfileToCreateIdentifier
);
}
);
if (duplicateAssetProfileToCreate) {
// The import contains the same asset profile more than once,
// which would fail with a unique constraint violation. Keep the
// first asset profile and merge the market data into it.
duplicateAssetProfileToCreate.marketDataObjects.push(
...marketDataObjects
);
} else {
// Create the new asset profile and its market data later, once it
// is known which activities are imported
assetProfilesToCreate.push({
marketDataObjects,
assetProfile: assetProfileToCreate
});
}
} else {
// Insert or update market data
await this.marketDataService.updateMany({
data: marketDataObjects
});
}
}
}
}
@ -719,6 +752,25 @@ export class ImportService {
return id === TAG_ID_DRAFT;
}) ?? { id: TAG_ID_DRAFT, name: 'DRAFT' };
// Create the new asset profiles of the activities to import only, so that
// no unused asset profile remains, for example if no activity refers to
// the asset profile. An asset profile which is created before the
// validation of the activities would stay behind, because the import is
// not rolled back on an error.
if (!isDryRun) {
for (const {
assetProfile,
marketDataObjects
} of this.getAssetProfilesToCreate({
activities: activitiesExtendedWithErrors,
assetProfiles: assetProfilesToCreate
})) {
await this.symbolProfileService.add(assetProfile);
await this.marketDataService.updateMany({ data: marketDataObjects });
}
}
const activities: Activity[] = [];
for (const activity of activitiesExtendedWithErrors) {
@ -934,7 +986,7 @@ export class ImportService {
activitiesDto: Partial<CreateOrderDto>[];
userCurrency: string;
userId: string;
}): Promise<Partial<Activity>[]> {
}): Promise<(Partial<Activity> & Pick<Activity, 'assetProfile'>)[]> {
const { activities: existingActivities } =
await this.activitiesService.getActivities({
userCurrency,
@ -1055,6 +1107,30 @@ export class ImportService {
return matchingAccountsOfUser[0];
}
private getAssetProfilesToCreate({
activities,
assetProfiles
}: {
activities: Pick<Activity, 'assetProfile' | 'error'>[];
assetProfiles: AssetProfileToCreate[];
}) {
const assetProfileIdentifiersToImport = new Set(
activities
.filter(({ error }) => {
return !error;
})
.map(({ assetProfile }) => {
return getAssetProfileIdentifier(assetProfile);
})
);
return assetProfiles.filter(({ assetProfile }) => {
return assetProfileIdentifiersToImport.has(
getAssetProfileIdentifier(assetProfile)
);
});
}
private isUniqueAccount(accounts: AccountWithValue[]) {
const uniqueAccountIds = new Set<string>();

6
apps/api/src/app/import/interfaces/asset-profile-to-create.interface.ts

@ -0,0 +1,6 @@
import { Prisma } from '@prisma/client';
export interface AssetProfileToCreate {
assetProfile: Prisma.SymbolProfileCreateInput;
marketDataObjects: Prisma.MarketDataUpdateInput[];
}

40
apps/api/src/services/data-provider/data-provider.service.ts

@ -39,7 +39,7 @@ import { Inject, Injectable, Logger, OnModuleInit } from '@nestjs/common';
import { DataSource, MarketData, Prisma, SymbolProfile } from '@prisma/client';
import { Big } from 'big.js';
import { eachDayOfInterval, format, isValid } from 'date-fns';
import { groupBy, isEmpty, isNumber, uniqWith } from 'lodash';
import { groupBy, isEmpty, isNumber, omit, uniqWith } from 'lodash';
import ms from 'ms';
import { AssetProfileInvalidError } from './errors/asset-profile-invalid.error';
@ -272,23 +272,31 @@ export class DataProviderService implements OnModuleInit {
});
if (!assetProfiles[assetProfileIdentifier]) {
if (
(dataSource === DataSource.MANUAL && type === 'BUY') ||
NON_INVESTMENT_ACTIVITY_TYPES.includes(type)
) {
const assetProfileInImport = assetProfilesWithMarketDataDto?.find(
(assetProfile) => {
(assetProfileWithMarketData) => {
return (
assetProfile.dataSource === dataSource &&
assetProfile.symbol === symbol
assetProfileWithMarketData.dataSource === dataSource &&
assetProfileWithMarketData.symbol === symbol
);
}
);
// A custom asset profile of the import is created after the
// validation, thus the data provider cannot resolve it yet
if (
(dataSource === DataSource.MANUAL && type === 'BUY') ||
assetProfileInImport?.dataSource === DataSource.MANUAL ||
NON_INVESTMENT_ACTIVITY_TYPES.includes(type)
) {
assetProfiles[assetProfileIdentifier] = {
currency,
...omit(assetProfileInImport ?? {}, [
'dataSource',
'marketData',
'symbol'
]),
dataSource,
symbol,
currency: assetProfileInImport?.currency ?? currency,
name: assetProfileInImport?.name ?? symbol
};
@ -308,20 +316,6 @@ export class DataProviderService implements OnModuleInit {
)?.[assetProfileIdentifier];
} catch {}
if (!assetProfile?.name) {
const assetProfileInImport = assetProfilesWithMarketDataDto?.find(
(profile) => {
return (
profile.dataSource === dataSource && profile.symbol === symbol
);
}
);
if (assetProfileInImport) {
Object.assign(assetProfile, assetProfileInImport);
}
}
if (!assetProfile?.name) {
throw new Error(
`${activityPath}.symbol ("${symbol}") is not valid for the specified data source ("${maskedDataSource}")`

Loading…
Cancel
Save