From 61cac99d54808bd703884801240397e93fd7602a Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Tue, 4 Aug 2026 20:31:57 +0200 Subject: [PATCH] Task/improve symbol validation for assets with manual data source (#7467) * Improve symbol validation for assets with manual data source * Update changelog --- CHANGELOG.md | 2 + .../src/app/activities/activities.service.ts | 12 +- apps/api/src/app/admin/admin.service.ts | 10 +- apps/api/src/app/import/import.service.ts | 166 +++++++++++++----- .../data-provider/data-provider.service.ts | 2 +- .../symbol-profile/symbol-profile.service.ts | 21 +++ .../app/services/import-activities.service.ts | 79 ++++++--- libs/common/src/lib/helper.spec.ts | 31 +++- libs/common/src/lib/helper.ts | 15 +- package-lock.json | 34 +++- package.json | 1 + ...nvalid-symbol-with-manual-data-source.json | 27 +++ test/import/ok/without-accounts.json | 24 ++- 13 files changed, 337 insertions(+), 87 deletions(-) create mode 100644 test/import/not-ok/invalid-symbol-with-manual-data-source.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 1de1e82b9..538815f1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Extended the support of the _Exclude from Analysis_ tag from accounts to activities - Optimized the performance of the search in the assistant by reusing the cached portfolio snapshot +- Improved the validation of the import functionality when referencing an asset profile with the data source `MANUAL` +- Improved the validation of the endpoint to add a custom asset profile in the admin control panel ### Fixed diff --git a/apps/api/src/app/activities/activities.service.ts b/apps/api/src/app/activities/activities.service.ts index da3e99312..ef3839340 100644 --- a/apps/api/src/app/activities/activities.service.ts +++ b/apps/api/src/app/activities/activities.service.ts @@ -20,12 +20,12 @@ import { DATA_GATHERING_QUEUE_PRIORITY_HIGH, GATHER_ASSET_PROFILE_PROCESS_JOB_NAME, GATHER_ASSET_PROFILE_PROCESS_JOB_OPTIONS, - ghostfolioPrefix, TAG_ID_EXCLUDE_FROM_ANALYSIS } from '@ghostfolio/common/config'; import { canDeleteAssetProfile, - getAssetProfileIdentifier + getAssetProfileIdentifier, + isValidCustomAssetProfileSymbol } from '@ghostfolio/common/helper'; import { ActivitiesResponse, @@ -48,7 +48,6 @@ import { Type as ActivityType } from '@prisma/client'; import { Big } from 'big.js'; -import { isUUID } from 'class-validator'; import { endOfToday, isAfter } from 'date-fns'; import { groupBy, uniqBy } from 'lodash'; import { randomUUID } from 'node:crypto'; @@ -204,10 +203,9 @@ export class ActivitiesService { let symbol: string; if ( - data.SymbolProfile.connectOrCreate.create.symbol.startsWith( - `${ghostfolioPrefix}_` - ) || - isUUID(data.SymbolProfile.connectOrCreate.create.symbol) + isValidCustomAssetProfileSymbol( + data.SymbolProfile.connectOrCreate.create.symbol + ) ) { // Connect custom asset profile (clone) symbol = data.SymbolProfile.connectOrCreate.create.symbol; diff --git a/apps/api/src/app/admin/admin.service.ts b/apps/api/src/app/admin/admin.service.ts index 4c608e0fd..d384e0d55 100644 --- a/apps/api/src/app/admin/admin.service.ts +++ b/apps/api/src/app/admin/admin.service.ts @@ -7,6 +7,7 @@ import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; import { + ghostfolioPrefix, PROPERTY_CURRENCIES, PROPERTY_IS_READ_ONLY_MODE, PROPERTY_IS_USER_SIGNUP_ENABLED @@ -14,7 +15,8 @@ import { import { applyAssetProfileOverrides, getAssetProfileIdentifier, - getCurrencyFromSymbol + getCurrencyFromSymbol, + hasGhostfolioPrefix } from '@ghostfolio/common/helper'; import { AdminData, @@ -63,6 +65,12 @@ export class AdminService { > { try { if (dataSource === 'MANUAL') { + if (!hasGhostfolioPrefix(symbol)) { + throw new BadRequestException( + `symbol ("${symbol}") must start with the prefix "${ghostfolioPrefix}_" for the data source ("${dataSource}")` + ); + } + return this.symbolProfileService.add({ currency, dataSource, diff --git a/apps/api/src/app/import/import.service.ts b/apps/api/src/app/import/import.service.ts index 52b0662d6..be511df44 100644 --- a/apps/api/src/app/import/import.service.ts +++ b/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 { @@ -196,6 +198,41 @@ export class ImportService { const tagIdMapping: { [oldTagId: string]: string } = {}; const userCurrency = user.settings.settings.baseCurrency; + // Validate the symbols before any data is persisted + for (const [index, assetProfileWithMarketData] of ( + assetProfilesWithMarketDataDto ?? [] + ).entries()) { + if ( + assetProfileWithMarketData.dataSource === DataSource.MANUAL && + !isValidCustomAssetProfileSymbol(assetProfileWithMarketData.symbol) + ) { + throw new Error( + `assetProfiles.${index}.symbol ("${assetProfileWithMarketData.symbol}") must be a UUID or start with the prefix "${ghostfolioPrefix}_" for the data source ("${DataSource.MANUAL}")` + ); + } + } + + // Validate the symbols before any data is persisted. Activities without a + // data source are excluded, since a symbol is generated in + // createActivity() if needed. + for (const [index, activity] of activitiesDto.entries()) { + if (!activity.dataSource) { + if (['FEE', 'INTEREST', 'LIABILITY'].includes(activity.type)) { + activity.dataSource = DataSource.MANUAL; + } else { + activity.dataSource = + this.dataProviderService.getDataSourceForImport(); + } + } else 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 (platformsDto?.length) { const canCreatePlatform = hasPermission( user.permissions, @@ -384,71 +421,117 @@ export class ImportService { } } - if (!isDryRun && assetProfilesWithMarketDataDto?.length) { - const existingAssetProfiles = - await this.symbolProfileService.getSymbolProfiles( - assetProfilesWithMarketDataDto.map(({ dataSource, symbol }) => { - return { dataSource, symbol }; + if (assetProfilesWithMarketDataDto?.length) { + const customAssetProfileNames = assetProfilesWithMarketDataDto + .filter(({ dataSource, name }) => { + return dataSource === DataSource.MANUAL && Boolean(name); + }) + .map(({ name }) => { + return name; + }); + + const [existingAssetProfiles, existingCustomAssetProfilesOfUser] = + await Promise.all([ + this.symbolProfileService.getSymbolProfiles( + assetProfilesWithMarketDataDto.map(({ dataSource, symbol }) => { + return { dataSource, symbol }; + }) + ), + this.symbolProfileService.getCustomSymbolProfilesByNames({ + names: customAssetProfileNames, + userId: user.id }) - ); + ]); for (const assetProfileWithMarketData of assetProfilesWithMarketDataDto) { + let symbol = assetProfileWithMarketData.symbol; + // Check if there is any existing asset profile const existingAssetProfile = existingAssetProfiles.find( - ({ dataSource, symbol }) => { + (assetProfile) => { return ( - dataSource === assetProfileWithMarketData.dataSource && - symbol === assetProfileWithMarketData.symbol + assetProfile.dataSource === + assetProfileWithMarketData.dataSource && + assetProfile.symbol === assetProfileWithMarketData.symbol ); } ); - // If there is no asset profile or if the asset profile belongs to a different user, then create a new asset profile + // If there is no asset profile or if the asset profile belongs to a + // different user, then reuse the custom asset profile of the user or + // create a new asset profile if (!existingAssetProfile || existingAssetProfile.userId !== user.id) { - const assetProfile: CreateAssetProfileDto = omit( - assetProfileWithMarketData, - 'marketData' - ); + // Check if the user has a custom asset profile with the same name. + // Skip asset profiles with a legacy free-text symbol as they would + // fail the symbol validation on a future import. + const existingCustomAssetProfileOfUser = + assetProfileWithMarketData.dataSource === DataSource.MANUAL + ? existingCustomAssetProfilesOfUser.find((customAssetProfile) => { + return ( + customAssetProfile.name === + assetProfileWithMarketData.name && + isValidCustomAssetProfileSymbol(customAssetProfile.symbol) + ); + }) + : undefined; + + if (existingCustomAssetProfileOfUser) { + // Reuse the custom asset profile of the user instead of creating a duplicate + symbol = existingCustomAssetProfileOfUser.symbol; + } else { + const assetProfile: CreateAssetProfileDto = omit( + assetProfileWithMarketData, + 'marketData' + ); + + // Asset profile belongs to a different user, generate a new symbol + if (existingAssetProfile && !isDryRun) { + symbol = randomUUID(); + } - // Asset profile belongs to a different user - if (existingAssetProfile) { - const symbol = randomUUID(); - assetProfileSymbolMapping[assetProfile.symbol] = symbol; assetProfile.symbol = symbol; + + if (!isDryRun) { + // Create a new asset profile + const assetProfileObject: Prisma.SymbolProfileCreateInput = { + ...assetProfile, + user: { connect: { id: user.id } } + }; + + await this.symbolProfileService.add(assetProfileObject); + } } - // Create a new asset profile - const assetProfileObject: Prisma.SymbolProfileCreateInput = { - ...assetProfile, - user: { connect: { id: user.id } } - }; + if (symbol !== assetProfileWithMarketData.symbol) { + assetProfileSymbolMapping[assetProfileWithMarketData.symbol] = + symbol; - await this.symbolProfileService.add(assetProfileObject); + // Keep the asset profile in sync with the activities to validate + assetProfileWithMarketData.symbol = symbol; + } } - // Insert or update market data - const marketDataObjects = assetProfileWithMarketData.marketData.map( - (marketData) => { + if (!isDryRun) { + // Insert or update market data + const marketDataObjects = ( + assetProfileWithMarketData.marketData ?? [] + ).map((marketData) => { return { ...marketData, - dataSource: assetProfileWithMarketData.dataSource, - symbol: assetProfileWithMarketData.symbol + symbol, + dataSource: assetProfileWithMarketData.dataSource } as Prisma.MarketDataUpdateInput; - } - ); + }); - await this.marketDataService.updateMany({ data: marketDataObjects }); + await this.marketDataService.updateMany({ data: marketDataObjects }); + } } } for (const activity of activitiesDto) { - if (!activity.dataSource) { - if (['FEE', 'INTEREST', 'LIABILITY'].includes(activity.type)) { - activity.dataSource = DataSource.MANUAL; - } else { - activity.dataSource = - this.dataProviderService.getDataSourceForImport(); - } + // If an asset profile is created or reused, then update the symbol in all activities + if (assetProfileSymbolMapping[activity.symbol]) { + activity.symbol = assetProfileSymbolMapping[activity.symbol]; } if (!isDryRun) { @@ -457,11 +540,6 @@ export class ImportService { activity.accountId = accountIdMapping[activity.accountId]; } - // If a new asset profile is created, then update the symbol in all activities - if (assetProfileSymbolMapping[activity.symbol]) { - activity.symbol = assetProfileSymbolMapping[activity.symbol]; - } - // If a new tag is created, then update the tag ID in all activities activity.tags = (activity.tags ?? []).map((tagId) => { return tagIdMapping[tagId] ?? tagId; diff --git a/apps/api/src/services/data-provider/data-provider.service.ts b/apps/api/src/services/data-provider/data-provider.service.ts index 4c273f2da..6031a8e25 100644 --- a/apps/api/src/services/data-provider/data-provider.service.ts +++ b/apps/api/src/services/data-provider/data-provider.service.ts @@ -318,7 +318,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}")` ); } diff --git a/apps/api/src/services/symbol-profile/symbol-profile.service.ts b/apps/api/src/services/symbol-profile/symbol-profile.service.ts index 1854a9f93..ebc8a94c7 100644 --- a/apps/api/src/services/symbol-profile/symbol-profile.service.ts +++ b/apps/api/src/services/symbol-profile/symbol-profile.service.ts @@ -105,6 +105,27 @@ export class SymbolProfileService { }; } + public async getCustomSymbolProfilesByNames({ + names, + userId + }: { + names: string[]; + userId: string; + }): Promise[]> { + if (names.length === 0) { + return []; + } + + return this.prismaService.symbolProfile.findMany({ + select: { name: true, symbol: true }, + where: { + userId, + dataSource: DataSource.MANUAL, + name: { in: names } + } + }); + } + public async getSymbolProfiles( aAssetProfileIdentifiers: AssetProfileIdentifier[] ): Promise { diff --git a/apps/client/src/app/services/import-activities.service.ts b/apps/client/src/app/services/import-activities.service.ts index 6a8f99bda..503149a03 100644 --- a/apps/client/src/app/services/import-activities.service.ts +++ b/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'; @@ -14,6 +17,7 @@ import { Account, DataSource, Type as ActivityType } from '@prisma/client'; import { isFinite, isNumber, isString } from 'lodash'; import { parse as csvToJson } from 'papaparse'; import { firstValueFrom } from 'rxjs'; +import { v4 as uuidv4 } from 'uuid'; @Injectable({ providedIn: 'root' @@ -57,13 +61,58 @@ export class ImportActivitiesService { const activities: CreateOrderDto[] = []; const assetProfiles: CreateAssetProfileWithMarketDataDto[] = []; + const assetProfileSymbolMapping = new Map(); 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 dataSource = this.parseDataSource({ item }); + let symbol = this.parseSymbol({ content, index, item }); + + if (!dataSource && ['FEE', 'INTEREST', 'LIABILITY'].includes(type)) { + // Apply the same data source as the import service + dataSource = DataSource.MANUAL; + } + + if (dataSource === DataSource.MANUAL) { + const name = symbol; + + if (!isValidCustomAssetProfileSymbol(symbol)) { + // Generate a symbol and keep the free text as the name + symbol = assetProfileSymbolMapping.get(name) ?? uuidv4(); + assetProfileSymbolMapping.set(name, symbol); + } + + 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 +126,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({ diff --git a/libs/common/src/lib/helper.spec.ts b/libs/common/src/lib/helper.spec.ts index 42441a619..669c42e32 100644 --- a/libs/common/src/lib/helper.spec.ts +++ b/libs/common/src/lib/helper.spec.ts @@ -10,7 +10,8 @@ import { isAccountExcluded, isCurrency, isCurrencySymbol, - isSplitRatio + isSplitRatio, + isValidCustomAssetProfileSymbol } from '@ghostfolio/common/helper'; describe('Helper', () => { @@ -326,4 +327,32 @@ describe('Helper', () => { ); }); }); + + describe('Is valid custom asset profile symbol', () => { + it('Empty symbol', () => { + expect(isValidCustomAssetProfileSymbol('')).toEqual(false); + }); + + it('Free-text symbol', () => { + expect(isValidCustomAssetProfileSymbol('Penthouse Apartment')).toEqual( + false + ); + }); + + it('Stock symbol', () => { + expect(isValidCustomAssetProfileSymbol('AAPL')).toEqual(false); + }); + + it('Symbol with Ghostfolio prefix', () => { + expect(isValidCustomAssetProfileSymbol('GF_PENTHOUSE_APARTMENT')).toEqual( + true + ); + }); + + it('UUID', () => { + expect( + isValidCustomAssetProfileSymbol('7e91b7d4-1430-4212-8380-289a06c9bbc1') + ).toEqual(true); + }); + }); }); diff --git a/libs/common/src/lib/helper.ts b/libs/common/src/lib/helper.ts index 22f969d02..6c0d6ea45 100644 --- a/libs/common/src/lib/helper.ts +++ b/libs/common/src/lib/helper.ts @@ -8,7 +8,7 @@ import { SymbolProfile } from '@prisma/client'; import { Big } from 'big.js'; -import { isISO4217CurrencyCode } from 'class-validator'; +import { isISO4217CurrencyCode, isUUID } from 'class-validator'; import { getDate, getMonth, @@ -41,6 +41,7 @@ import { DERIVED_CURRENCIES, ghostfolioFearAndGreedIndexSymbolCryptocurrencies, ghostfolioFearAndGreedIndexSymbolStocks, + ghostfolioPrefix, SEARCH_QUERY_MINIMUM_LENGTH, TAG_ID_EXCLUDE_FROM_ANALYSIS } from './config'; @@ -466,6 +467,14 @@ export function getYesterday() { return subDays(new Date(Date.UTC(year, month, day)), 1); } +export function hasGhostfolioPrefix(aSymbol: string) { + if (!aSymbol) { + return false; + } + + return aSymbol.startsWith(`${ghostfolioPrefix}_`); +} + export function interpolate(template: string, context: any) { return template?.replace(/[$]{([^}]+)}/g, (_, objectPath) => { const properties = objectPath.split('.'); @@ -548,6 +557,10 @@ export function isSplitRatio({ ); } +export function isValidCustomAssetProfileSymbol(aSymbol: string) { + return hasGhostfolioPrefix(aSymbol) || isUUID(aSymbol); +} + export function isValidSearchQuery(aQuery: string) { return aQuery?.trim().length >= SEARCH_QUERY_MINIMUM_LENGTH; } diff --git a/package-lock.json b/package-lock.json index 87cad3a36..51ee52ea1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -97,6 +97,7 @@ "tablemark": "4.1.0", "twitter-api-v2": "1.29.0", "undici": "8.5.0", + "uuid": "14.0.1", "yahoo-finance2": "4.0.0", "zod": "4.4.3", "zone.js": "0.16.1" @@ -15985,6 +15986,16 @@ "node": ">=12" } }, + "node_modules/bull/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/bundle-name": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", @@ -32154,6 +32165,17 @@ "websocket-driver": "^0.7.4" } }, + "node_modules/sockjs/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/socks": { "version": "2.8.7", "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", @@ -34664,12 +34686,16 @@ } }, "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.1.tgz", + "integrity": "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], "license": "MIT", "bin": { - "uuid": "dist/bin/uuid" + "uuid": "dist-node/bin/uuid" } }, "node_modules/v8-compile-cache-lib": { diff --git a/package.json b/package.json index 9ace2cbe8..61bf1c0d4 100644 --- a/package.json +++ b/package.json @@ -141,6 +141,7 @@ "tablemark": "4.1.0", "twitter-api-v2": "1.29.0", "undici": "8.5.0", + "uuid": "14.0.1", "yahoo-finance2": "4.0.0", "zod": "4.4.3", "zone.js": "0.16.1" diff --git a/test/import/not-ok/invalid-symbol-with-manual-data-source.json b/test/import/not-ok/invalid-symbol-with-manual-data-source.json new file mode 100644 index 000000000..08d070e61 --- /dev/null +++ b/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" + } + } +} diff --git a/test/import/ok/without-accounts.json b/test/import/ok/without-accounts.json index 2283dd889..cacbe153e 100644 --- a/test/import/ok/without-accounts.json +++ b/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,