Browse Source

Improve symbol validation for assets with manual data source

pull/7467/head
Thomas Kaul 1 month ago
parent
commit
4d4a209e3d
  1. 2
      apps/api/src/app/admin/admin.service.ts
  2. 22
      apps/api/src/app/import/import.service.ts
  3. 4
      apps/api/src/services/data-provider/data-provider.service.ts
  4. 72
      apps/client/src/app/services/import-activities.service.ts
  5. 6
      libs/common/src/lib/dtos/create-asset-profile-with-market-data.dto.ts
  6. 18
      libs/common/src/lib/validator-constraints/is-custom-asset-profile-symbol.ts
  7. 27
      test/import/not-ok/invalid-symbol-with-manual-data-source.json
  8. 24
      test/import/ok/without-accounts.json

2
apps/api/src/app/admin/admin.service.ts

@ -67,7 +67,7 @@ export class AdminService {
if (dataSource === 'MANUAL') {
if (!hasGhostfolioPrefix(symbol)) {
throw new BadRequestException(
`symbol "${symbol}" must start with the prefix "${ghostfolioPrefix}_" for the data source "${dataSource}"`
`symbol ("${symbol}") must start with the prefix "${ghostfolioPrefix}_" for the data source ("${dataSource}")`
);
}

22
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 {
DATA_GATHERING_QUEUE_PRIORITY_HIGH,
ghostfolioPrefix,
TAG_ID_EXCLUDE_FROM_ANALYSIS
} from '@ghostfolio/common/config';
import { CreateAssetProfileDto, CreateOrderDto } from '@ghostfolio/common/dtos';
import {
getAssetProfileIdentifier,
isValidCustomAssetProfileSymbol,
parseDate
} from '@ghostfolio/common/helper';
import {
@ -393,6 +395,15 @@ export class ImportService {
);
for (const assetProfileWithMarketData of assetProfilesWithMarketDataDto) {
if (
!isValidCustomAssetProfileSymbol(assetProfileWithMarketData.symbol)
) {
// Skip synthetic asset profiles (e.g. of the csv import), where the
// symbol is used as the name of the asset profile created in
// createActivity()
continue;
}
// Check if there is any existing asset profile
const existingAssetProfile = existingAssetProfiles.find(
({ dataSource, symbol }) => {
@ -441,7 +452,7 @@ export class ImportService {
}
}
for (const activity of activitiesDto) {
for (const [index, activity] of activitiesDto.entries()) {
if (!activity.dataSource) {
if (['FEE', 'INTEREST', 'LIABILITY'].includes(activity.type)) {
activity.dataSource = DataSource.MANUAL;
@ -451,6 +462,15 @@ export class ImportService {
}
}
if (
activity.dataSource === DataSource.MANUAL &&
!isValidCustomAssetProfileSymbol(activity.symbol)
) {
throw new Error(
`activities.${index}.symbol ("${activity.symbol}") must be a UUID or start with the prefix "${ghostfolioPrefix}_" for the data source ("${DataSource.MANUAL}")`
);
}
if (!isDryRun) {
// If a new account is created, then update the accountId in all activities
if (accountIdMapping[activity.accountId]) {

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

@ -296,7 +296,7 @@ export class DataProviderService implements OnModuleInit {
!isValidCustomAssetProfileSymbol(symbol)
) {
throw new Error(
`${activityPath}.symbol "${symbol}" must be a UUID or start with the prefix "${ghostfolioPrefix}_" for the data source "${DataSource.MANUAL}"`
`${activityPath}.symbol ("${symbol}") must be a UUID or start with the prefix "${ghostfolioPrefix}_" for the data source ("${DataSource.MANUAL}")`
);
}
@ -329,7 +329,7 @@ export class DataProviderService implements OnModuleInit {
if (!assetProfile?.name) {
throw new Error(
`activities.${index}.symbol ("${symbol}") is not valid for the specified data source ("${maskedDataSource}")`
`${activityPath}.symbol ("${symbol}") is not valid for the specified data source ("${maskedDataSource}")`
);
}

72
apps/client/src/app/services/import-activities.service.ts

@ -5,7 +5,10 @@ import {
CreatePlatformDto,
CreateTagDto
} from '@ghostfolio/common/dtos';
import { parseDate as parseDateHelper } from '@ghostfolio/common/helper';
import {
isValidCustomAssetProfileSymbol,
parseDate as parseDateHelper
} from '@ghostfolio/common/helper';
import { Activity } from '@ghostfolio/common/interfaces';
import { HttpClient } from '@angular/common/http';
@ -57,13 +60,54 @@ export class ImportActivitiesService {
const activities: CreateOrderDto[] = [];
const assetProfiles: CreateAssetProfileWithMarketDataDto[] = [];
const assetProfileSymbolMapping: { [name: string]: string } = {};
for (const [index, item] of content.entries()) {
const currency = this.parseCurrency({ content, index, item });
const dataSource = this.parseDataSource({ item });
const symbol = this.parseSymbol({ content, index, item });
const type = this.parseType({ content, index, item });
let symbol = this.parseSymbol({ content, index, item });
if (dataSource === DataSource.MANUAL) {
const name = symbol;
if (!isValidCustomAssetProfileSymbol(symbol)) {
// Generate a symbol and keep the free text as the name
assetProfileSymbolMapping[name] =
assetProfileSymbolMapping[name] ?? crypto.randomUUID();
symbol = assetProfileSymbolMapping[name];
}
const isExistingAssetProfile = assetProfiles.some((assetProfile) => {
return assetProfile.symbol === symbol;
});
if (!isExistingAssetProfile) {
// Create synthetic asset profile for MANUAL data source
assetProfiles.push({
currency,
name,
symbol,
assetClass: undefined,
assetSubClass: undefined,
comment: undefined,
countries: [],
cusip: undefined,
dataSource: DataSource.MANUAL,
figi: undefined,
figiComposite: undefined,
figiShareClass: undefined,
holdings: [],
isActive: true,
isin: undefined,
marketData: [],
sectors: [],
url: undefined
});
}
}
activities.push({
currency,
dataSource,
@ -77,30 +121,6 @@ export class ImportActivitiesService {
unitPrice: this.parseUnitPrice({ content, index, item }),
updateAccountBalance: false
});
if (dataSource === DataSource.MANUAL) {
// Create synthetic asset profile for MANUAL data source
assetProfiles.push({
currency,
symbol,
assetClass: undefined,
assetSubClass: undefined,
comment: undefined,
countries: [],
cusip: undefined,
dataSource: DataSource.MANUAL,
figi: undefined,
figiComposite: undefined,
figiShareClass: undefined,
holdings: [],
isActive: true,
isin: undefined,
marketData: [],
name: symbol,
sectors: [],
url: undefined
});
}
}
const result = await this.importJson({

6
libs/common/src/lib/dtos/create-asset-profile-with-market-data.dto.ts

@ -1,8 +1,7 @@
import { MarketData } from '@ghostfolio/common/interfaces';
import { IsCustomAssetProfileSymbolConstraint } from '@ghostfolio/common/validator-constraints/is-custom-asset-profile-symbol';
import { DataSource } from '@prisma/client';
import { IsArray, IsIn, IsOptional, Validate } from 'class-validator';
import { IsArray, IsIn, IsOptional } from 'class-validator';
import { CreateAssetProfileDto } from './create-asset-profile.dto';
@ -15,7 +14,4 @@ export class CreateAssetProfileWithMarketDataDto extends CreateAssetProfileDto {
@IsArray()
@IsOptional()
marketData?: MarketData[];
@Validate(IsCustomAssetProfileSymbolConstraint)
override symbol: string;
}

18
libs/common/src/lib/validator-constraints/is-custom-asset-profile-symbol.ts

@ -1,18 +0,0 @@
import {
ValidatorConstraint,
ValidatorConstraintInterface
} from 'class-validator';
import { ghostfolioPrefix } from '../config';
import { isValidCustomAssetProfileSymbol } from '../helper';
@ValidatorConstraint({ name: 'isCustomAssetProfileSymbol' })
export class IsCustomAssetProfileSymbolConstraint implements ValidatorConstraintInterface {
public defaultMessage() {
return `$property must be a UUID or start with the prefix "${ghostfolioPrefix}_"`;
}
public validate(aSymbol: string) {
return isValidCustomAssetProfileSymbol(aSymbol);
}
}

27
test/import/not-ok/invalid-symbol-with-manual-data-source.json

@ -0,0 +1,27 @@
{
"meta": {
"date": "2023-02-05T00:00:00.000Z",
"version": "dev"
},
"activities": [
{
"accountId": null,
"comment": null,
"currency": "USD",
"dataSource": "MANUAL",
"date": "2022-01-01T00:00:00.000Z",
"fee": 0,
"quantity": 1,
"symbol": "Penthouse Apartment",
"tags": [],
"type": "BUY",
"unitPrice": 500000
}
],
"user": {
"settings": {
"currency": "USD",
"performanceCalculationType": "ROAI"
}
}
}

24
test/import/ok/without-accounts.json

@ -3,6 +3,28 @@
"date": "2022-04-01T00:00:00.000Z",
"version": "dev"
},
"assetProfiles": [
{
"assetClass": null,
"assetSubClass": null,
"comment": null,
"countries": [],
"currency": "USD",
"cusip": null,
"dataSource": "MANUAL",
"figi": null,
"figiComposite": null,
"figiShareClass": null,
"holdings": [],
"isActive": true,
"isin": null,
"marketData": [],
"name": "Penthouse Apartment",
"sectors": [],
"symbol": "7e91b7d4-1430-4212-8380-289a06c9bbc1",
"url": null
}
],
"activities": [
{
"fee": 0,
@ -22,7 +44,7 @@
"currency": "USD",
"dataSource": "MANUAL",
"date": "2022-01-01T00:00:00.000Z",
"symbol": "Penthouse Apartment"
"symbol": "7e91b7d4-1430-4212-8380-289a06c9bbc1"
},
{
"fee": 0,

Loading…
Cancel
Save