Browse Source

Improve validation of manual symbols

pull/7409/head
Thomas Kaul 1 month ago
parent
commit
aa81ed29dc
  1. 4
      apps/api/src/app/import/import-data.dto.ts
  2. 27
      libs/common/src/lib/helper.spec.ts
  3. 11
      libs/common/src/lib/helper.ts
  4. 46
      libs/common/src/lib/validator-constraints/has-valid-manual-symbols.ts
  5. 52
      test/import/not-ok/invalid-symbol-with-manual-data-source.json

4
apps/api/src/app/import/import-data.dto.ts

@ -4,9 +4,10 @@ import {
CreateOrderDto, CreateOrderDto,
CreateTagDto CreateTagDto
} from '@ghostfolio/common/dtos'; } from '@ghostfolio/common/dtos';
import { HasValidManualSymbolsConstraint } from '@ghostfolio/common/validator-constraints/has-valid-manual-symbols';
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
import { IsArray, IsOptional, ValidateNested } from 'class-validator'; import { IsArray, IsOptional, Validate, ValidateNested } from 'class-validator';
export class ImportDataDto { export class ImportDataDto {
@IsArray() @IsArray()
@ -17,6 +18,7 @@ export class ImportDataDto {
@IsArray() @IsArray()
@Type(() => CreateOrderDto) @Type(() => CreateOrderDto)
@Validate(HasValidManualSymbolsConstraint)
@ValidateNested({ each: true }) @ValidateNested({ each: true })
activities: CreateOrderDto[]; activities: CreateOrderDto[];

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

@ -2,7 +2,8 @@ import {
extractNumberFromString, extractNumberFromString,
getNumberFormatGroup, getNumberFormatGroup,
isCurrency, isCurrency,
isCurrencySymbol isCurrencySymbol,
isValidManualSymbol
} from '@ghostfolio/common/helper'; } from '@ghostfolio/common/helper';
describe('Helper', () => { describe('Helper', () => {
@ -195,4 +196,28 @@ describe('Helper', () => {
expect(isCurrencySymbol('')).toEqual(false); expect(isCurrencySymbol('')).toEqual(false);
}); });
}); });
describe('Is valid manual symbol', () => {
it('UUID', () => {
expect(
isValidManualSymbol('7e91b7d4-1430-4212-8380-289a06c9bbc1')
).toEqual(true);
});
it('Ghostfolio-prefixed symbol', () => {
expect(isValidManualSymbol('GF_PENTHOUSE_APARTMENT')).toEqual(true);
});
it('Human-readable symbol', () => {
expect(isValidManualSymbol('PENTHOUSE_APARTMENT')).toEqual(false);
});
it('Human-readable name', () => {
expect(isValidManualSymbol('Penthouse Apartment')).toEqual(false);
});
it('Empty symbol', () => {
expect(isValidManualSymbol('')).toEqual(false);
});
});
}); });

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

@ -8,7 +8,7 @@ import {
SymbolProfile SymbolProfile
} from '@prisma/client'; } from '@prisma/client';
import { Big } from 'big.js'; import { Big } from 'big.js';
import { isISO4217CurrencyCode } from 'class-validator'; import { isISO4217CurrencyCode, isUUID } from 'class-validator';
import { import {
getDate, getDate,
getMonth, getMonth,
@ -41,6 +41,7 @@ import {
DERIVED_CURRENCIES, DERIVED_CURRENCIES,
ghostfolioFearAndGreedIndexSymbolCryptocurrencies, ghostfolioFearAndGreedIndexSymbolCryptocurrencies,
ghostfolioFearAndGreedIndexSymbolStocks, ghostfolioFearAndGreedIndexSymbolStocks,
ghostfolioPrefix,
TAG_ID_EXCLUDE_FROM_ANALYSIS TAG_ID_EXCLUDE_FROM_ANALYSIS
} from './config'; } from './config';
import { import {
@ -509,6 +510,14 @@ export function isRootCurrency(aCurrency: string) {
}); });
} }
export function isValidManualSymbol(aSymbol: string) {
if (!aSymbol) {
return false;
}
return isUUID(aSymbol) || aSymbol.startsWith(`${ghostfolioPrefix}_`);
}
export function parseDate(date: string): Date | undefined { export function parseDate(date: string): Date | undefined {
if (!date) { if (!date) {
return undefined; return undefined;

46
libs/common/src/lib/validator-constraints/has-valid-manual-symbols.ts

@ -0,0 +1,46 @@
import {
ValidationArguments,
ValidatorConstraint,
ValidatorConstraintInterface
} from 'class-validator';
import { ghostfolioPrefix } from '../config';
import { isValidManualSymbol } from '../helper';
interface ImportDataLike {
activities?: { dataSource?: string; symbol?: string; type?: string }[];
assetProfiles?: { dataSource?: string; symbol?: string }[];
}
@ValidatorConstraint({ name: 'hasValidManualSymbols' })
export class HasValidManualSymbolsConstraint implements ValidatorConstraintInterface {
public defaultMessage() {
return `manual symbols must be a UUID or start with "${ghostfolioPrefix}_"`;
}
public validate(_: unknown, args: ValidationArguments) {
const { activities = [], assetProfiles = [] } =
args.object as ImportDataLike;
const activitiesAreValid = activities.every(
({ dataSource, symbol, type }) => {
// FEE, INTEREST and LIABILITY default to the MANUAL data source
// (resolved in the backend), so treat them as manual when no data
// source is set
const isManual =
dataSource === 'MANUAL' ||
(!dataSource && ['FEE', 'INTEREST', 'LIABILITY'].includes(type));
return !isManual || isValidManualSymbol(symbol);
}
);
const assetProfilesAreValid = assetProfiles.every(
({ dataSource, symbol }) => {
return dataSource !== 'MANUAL' || isValidManualSymbol(symbol);
}
);
return activitiesAreValid && assetProfilesAreValid;
}
}

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

@ -0,0 +1,52 @@
{
"meta": {
"date": "2023-02-05T00:00:00.000Z",
"version": "dev"
},
"accounts": [],
"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": "PENTHOUSE_APARTMENT",
"url": null
}
],
"platforms": [],
"tags": [],
"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"
}
}
}
Loading…
Cancel
Save