Browse Source

Fix unused custom asset profiles created by activities import

pull/7673/head
Thomas Kaul 1 week ago
parent
commit
724eef1987
  1. 84
      apps/api/src/app/import/import.service.ts
  2. 41
      apps/api/src/services/data-provider/data-provider.service.ts

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

@ -536,7 +536,6 @@ export class ImportService {
const assetProfilesToCreate: { const assetProfilesToCreate: {
assetProfile: Prisma.SymbolProfileCreateInput; assetProfile: Prisma.SymbolProfileCreateInput;
assetProfileIdentifier: string;
marketDataObjects: Prisma.MarketDataUpdateInput[]; marketDataObjects: Prisma.MarketDataUpdateInput[];
}[] = []; }[] = [];
@ -644,11 +643,7 @@ export class ImportService {
// is known which activities are imported // is known which activities are imported
assetProfilesToCreate.push({ assetProfilesToCreate.push({
marketDataObjects, marketDataObjects,
assetProfile: assetProfileToCreate, assetProfile: assetProfileToCreate
assetProfileIdentifier: getAssetProfileIdentifier({
symbol,
dataSource: assetProfileWithMarketData.dataSource
})
}); });
} else { } else {
// Insert or update market data // Insert or update market data
@ -738,35 +733,21 @@ export class ImportService {
return id === TAG_ID_DRAFT; return id === TAG_ID_DRAFT;
}) ?? { id: TAG_ID_DRAFT, name: 'DRAFT' }; }) ?? { id: TAG_ID_DRAFT, name: 'DRAFT' };
if (assetProfilesToCreate.length) { // Create the new asset profiles of the activities to import only, so that
// Create the new asset profiles of the activities to import only, so // no unused asset profile remains, for example if an activity is a
// that no unused asset profile remains, for example if an activity is a // duplicate. An asset profile which is created before the validation of
// duplicate. An asset profile which is created before the validation of // the activities would stay behind, because the import is not rolled back
// the activities would stay behind, because the import is not rolled // on an error.
// back on an error. for (const {
const assetProfileIdentifiersToImport = new Set( assetProfile,
activitiesExtendedWithErrors marketDataObjects
.filter(({ error }) => { } of this.getAssetProfilesToCreate({
return !error; activities: activitiesExtendedWithErrors,
}) assetProfiles: assetProfilesToCreate
.map(({ assetProfile }) => { })) {
return getAssetProfileIdentifier(assetProfile); await this.symbolProfileService.add(assetProfile);
})
); await this.marketDataService.updateMany({ data: marketDataObjects });
for (const {
assetProfile,
assetProfileIdentifier,
marketDataObjects
} of assetProfilesToCreate) {
if (!assetProfileIdentifiersToImport.has(assetProfileIdentifier)) {
continue;
}
await this.symbolProfileService.add(assetProfile);
await this.marketDataService.updateMany({ data: marketDataObjects });
}
} }
const activities: Activity[] = []; const activities: Activity[] = [];
@ -1105,6 +1086,39 @@ export class ImportService {
return matchingAccountsOfUser[0]; return matchingAccountsOfUser[0];
} }
/**
* Returns the asset profiles which at least one activity of the import uses.
* The asset profiles are created after the validation of the activities,
* thus an asset profile of an activity which is not imported must not be
* created.
*/
private getAssetProfilesToCreate({
activities,
assetProfiles
}: {
activities: Partial<Activity>[];
assetProfiles: {
assetProfile: Prisma.SymbolProfileCreateInput;
marketDataObjects: Prisma.MarketDataUpdateInput[];
}[];
}) {
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[]) { private isUniqueAccount(accounts: AccountWithValue[]) {
const uniqueAccountIds = new Set<string>(); const uniqueAccountIds = new Set<string>();

41
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 { DataSource, MarketData, Prisma, SymbolProfile } from '@prisma/client';
import { Big } from 'big.js'; import { Big } from 'big.js';
import { eachDayOfInterval, format, isValid } from 'date-fns'; 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 ms from 'ms';
import { AssetProfileInvalidError } from './errors/asset-profile-invalid.error'; import { AssetProfileInvalidError } from './errors/asset-profile-invalid.error';
@ -272,19 +272,22 @@ export class DataProviderService implements OnModuleInit {
}); });
if (!assetProfiles[assetProfileIdentifier]) { if (!assetProfiles[assetProfileIdentifier]) {
const assetProfileInImport = assetProfilesWithMarketDataDto?.find(
(assetProfile) => {
return (
assetProfile.dataSource === dataSource &&
assetProfile.symbol === symbol
);
}
);
// A custom asset profile of the import is created after the
// validation, thus the data provider cannot resolve it yet
if ( if (
(dataSource === DataSource.MANUAL && type === 'BUY') || (dataSource === DataSource.MANUAL &&
(type === 'BUY' || Boolean(assetProfileInImport))) ||
NON_INVESTMENT_ACTIVITY_TYPES.includes(type) NON_INVESTMENT_ACTIVITY_TYPES.includes(type)
) { ) {
const assetProfileInImport = assetProfilesWithMarketDataDto?.find(
(assetProfile) => {
return (
assetProfile.dataSource === dataSource &&
assetProfile.symbol === symbol
);
}
);
assetProfiles[assetProfileIdentifier] = { assetProfiles[assetProfileIdentifier] = {
currency, currency,
dataSource, dataSource,
@ -309,18 +312,10 @@ export class DataProviderService implements OnModuleInit {
)?.[assetProfileIdentifier] ?? assetProfile; )?.[assetProfileIdentifier] ?? assetProfile;
} catch {} } catch {}
if (!assetProfile?.name) { if (!assetProfile?.name && assetProfileInImport) {
const assetProfileInImport = assetProfilesWithMarketDataDto?.find( // Omit the market data, since it must not become part of the
(profile) => { // asset profile of the response
return ( Object.assign(assetProfile, omit(assetProfileInImport, 'marketData'));
profile.dataSource === dataSource && profile.symbol === symbol
);
}
);
if (assetProfileInImport) {
Object.assign(assetProfile, assetProfileInImport);
}
} }
if (!assetProfile?.name) { if (!assetProfile?.name) {

Loading…
Cancel
Save