Browse Source

Improve validation of manual symbols

pull/7409/head
Thomas Kaul 1 month ago
parent
commit
6beccc2a39
  1. 16
      apps/api/src/app/activities/activities.service.ts
  2. 52
      apps/api/src/app/import/import.service.ts
  3. 17
      libs/common/src/lib/helper.spec.ts
  4. 10
      libs/common/src/lib/helper.ts

16
apps/api/src/app/activities/activities.service.ts

@ -23,6 +23,7 @@ import {
import { import {
canDeleteAssetProfile, canDeleteAssetProfile,
getAssetProfileIdentifier, getAssetProfileIdentifier,
isGhostfolioSymbol,
isValidManualSymbol isValidManualSymbol
} from '@ghostfolio/common/helper'; } from '@ghostfolio/common/helper';
import { import {
@ -205,6 +206,21 @@ export class ActivitiesService {
) { ) {
// Connect custom asset profile (clone) // Connect custom asset profile (clone)
symbol = data.SymbolProfile.connectOrCreate.create.symbol; symbol = data.SymbolProfile.connectOrCreate.create.symbol;
if (isGhostfolioSymbol(symbol)) {
// Asset profiles with the Ghostfolio prefix are created in the admin
// control only, so they must exist already
const existingAssetProfile =
await this.prismaService.symbolProfile.findUnique({
where: { dataSource_symbol: { dataSource, symbol } }
});
if (!existingAssetProfile) {
throw new Error(
`Asset profile not found for ${symbol} (${dataSource})`
);
}
}
} else { } else {
// Create custom asset profile // Create custom asset profile
name = name ?? data.SymbolProfile.connectOrCreate.create.symbol; name = name ?? data.SymbolProfile.connectOrCreate.create.symbol;

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

@ -11,11 +11,13 @@ import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/sy
import { TagService } from '@ghostfolio/api/services/tag/tag.service'; import { TagService } from '@ghostfolio/api/services/tag/tag.service';
import { import {
ACTIVITY_TYPES_WITH_GENERATED_UUID_SYMBOL, ACTIVITY_TYPES_WITH_GENERATED_UUID_SYMBOL,
DATA_GATHERING_QUEUE_PRIORITY_HIGH DATA_GATHERING_QUEUE_PRIORITY_HIGH,
ghostfolioPrefix
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { CreateAssetProfileDto, CreateOrderDto } from '@ghostfolio/common/dtos'; import { CreateAssetProfileDto, CreateOrderDto } from '@ghostfolio/common/dtos';
import { import {
getAssetProfileIdentifier, getAssetProfileIdentifier,
isGhostfolioSymbol,
parseDate parseDate
} from '@ghostfolio/common/helper'; } from '@ghostfolio/common/helper';
import { import {
@ -34,7 +36,7 @@ import { Injectable } from '@nestjs/common';
import { DataSource, Prisma } from '@prisma/client'; import { DataSource, Prisma } from '@prisma/client';
import { Big } from 'big.js'; import { Big } from 'big.js';
import { endOfToday, isAfter, isSameSecond, parseISO } from 'date-fns'; import { endOfToday, isAfter, isSameSecond, parseISO } from 'date-fns';
import { omit, uniqBy } from 'lodash'; import { omit, uniq, uniqBy } from 'lodash';
import { randomUUID } from 'node:crypto'; import { randomUUID } from 'node:crypto';
import { ImportDataDto } from './import-data.dto'; import { ImportDataDto } from './import-data.dto';
@ -193,6 +195,11 @@ export class ImportService {
const tagIdMapping: { [oldTagId: string]: string } = {}; const tagIdMapping: { [oldTagId: string]: string } = {};
const userCurrency = user.settings.settings.baseCurrency; const userCurrency = user.settings.settings.baseCurrency;
await this.validateGhostfolioSymbols({
activitiesDto,
assetProfilesWithMarketDataDto
});
const existingTagsOfUser = const existingTagsOfUser =
tagsDto?.length || (!isDryRun && accountsWithBalancesDto?.length) tagsDto?.length || (!isDryRun && accountsWithBalancesDto?.length)
? await this.tagService.getTagsForUser(user.id) ? await this.tagService.getTagsForUser(user.id)
@ -749,4 +756,45 @@ export class ImportService {
return uniqueAccountIds.size === 1; return uniqueAccountIds.size === 1;
} }
private async validateGhostfolioSymbols({
activitiesDto,
assetProfilesWithMarketDataDto
}: {
activitiesDto: ImportDataDto['activities'];
assetProfilesWithMarketDataDto: ImportDataDto['assetProfiles'];
}) {
const symbols = uniq(
[...(activitiesDto ?? []), ...(assetProfilesWithMarketDataDto ?? [])].map(
({ symbol }) => {
return symbol;
}
)
).filter((symbol) => {
return isGhostfolioSymbol(symbol);
});
if (symbols.length === 0) {
return;
}
const existingAssetProfiles =
await this.symbolProfileService.getSymbolProfiles(
symbols.map((symbol) => {
return { symbol, dataSource: DataSource.MANUAL };
})
);
const missingSymbols = symbols.filter((symbol) => {
return !existingAssetProfiles.some(({ symbol: existingSymbol }) => {
return existingSymbol === symbol;
});
});
if (missingSymbols.length > 0) {
throw new Error(
`Asset profiles with the prefix "${ghostfolioPrefix}_" can only be created in the admin control (${missingSymbols.join(', ')})`
);
}
}
} }

17
libs/common/src/lib/helper.spec.ts

@ -3,6 +3,7 @@ import {
getNumberFormatGroup, getNumberFormatGroup,
isCurrency, isCurrency,
isCurrencySymbol, isCurrencySymbol,
isGhostfolioSymbol,
isValidManualSymbol isValidManualSymbol
} from '@ghostfolio/common/helper'; } from '@ghostfolio/common/helper';
@ -197,6 +198,22 @@ describe('Helper', () => {
}); });
}); });
describe('Is Ghostfolio symbol', () => {
it('Ghostfolio-prefixed symbol', () => {
expect(isGhostfolioSymbol('GF_PENTHOUSE_APARTMENT')).toEqual(true);
});
it('UUID', () => {
expect(
isGhostfolioSymbol('7e91b7d4-1430-4212-8380-289a06c9bbc1')
).toEqual(false);
});
it('Human-readable symbol', () => {
expect(isGhostfolioSymbol('PENTHOUSE_APARTMENT')).toEqual(false);
});
});
describe('Is valid manual symbol', () => { describe('Is valid manual symbol', () => {
it('UUID', () => { it('UUID', () => {
expect( expect(

10
libs/common/src/lib/helper.ts

@ -500,6 +500,14 @@ export function isDerivedCurrency(aCurrency: string) {
}); });
} }
export function isGhostfolioSymbol(aSymbol?: string) {
if (!aSymbol) {
return false;
}
return aSymbol.startsWith(`${ghostfolioPrefix}_`);
}
export function isRootCurrency(aCurrency: string) { export function isRootCurrency(aCurrency: string) {
if (aCurrency === 'USD') { if (aCurrency === 'USD') {
return true; return true;
@ -515,7 +523,7 @@ export function isValidManualSymbol(aSymbol?: string) {
return false; return false;
} }
return isUUID(aSymbol) || aSymbol.startsWith(`${ghostfolioPrefix}_`); return isGhostfolioSymbol(aSymbol) || isUUID(aSymbol);
} }
export function parseDate(date: string): Date | undefined { export function parseDate(date: string): Date | undefined {

Loading…
Cancel
Save