diff --git a/CHANGELOG.md b/CHANGELOG.md index f8822c7b6..cdb3ee041 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Extended the export functionality by custom asset profiles - Improved the platform icon in the create or update platform dialog of the admin control - Localized the durations of the coupon system - Refactored the resources pages to standalone diff --git a/apps/api/src/app/export/export.module.ts b/apps/api/src/app/export/export.module.ts index 424e83617..4f40cc417 100644 --- a/apps/api/src/app/export/export.module.ts +++ b/apps/api/src/app/export/export.module.ts @@ -2,6 +2,7 @@ import { AccountModule } from '@ghostfolio/api/app/account/account.module'; import { OrderModule } from '@ghostfolio/api/app/order/order.module'; import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module'; import { ApiModule } from '@ghostfolio/api/services/api/api.module'; +import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module'; import { TagModule } from '@ghostfolio/api/services/tag/tag.module'; import { Module } from '@nestjs/common'; @@ -14,6 +15,7 @@ import { ExportService } from './export.service'; imports: [ AccountModule, ApiModule, + MarketDataModule, OrderModule, TagModule, TransformDataSourceInRequestModule diff --git a/apps/api/src/app/export/export.service.ts b/apps/api/src/app/export/export.service.ts index f0f52bfe2..9c9d20682 100644 --- a/apps/api/src/app/export/export.service.ts +++ b/apps/api/src/app/export/export.service.ts @@ -1,17 +1,19 @@ import { AccountService } from '@ghostfolio/api/app/account/account.service'; import { OrderService } from '@ghostfolio/api/app/order/order.service'; import { environment } from '@ghostfolio/api/environments/environment'; +import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; import { TagService } from '@ghostfolio/api/services/tag/tag.service'; import { Filter, Export } from '@ghostfolio/common/interfaces'; import { Injectable } from '@nestjs/common'; import { Platform, Prisma } from '@prisma/client'; -import { groupBy } from 'lodash'; +import { groupBy, uniqBy } from 'lodash'; @Injectable() export class ExportService { public constructor( private readonly accountService: AccountService, + private readonly marketDataService: MarketDataService, private readonly orderService: OrderService, private readonly tagService: TagService ) {} @@ -108,6 +110,36 @@ export class ExportService { } ); + const customAssetProfiles = uniqBy( + activities + .map(({ SymbolProfile }) => { + return SymbolProfile; + }) + .filter(({ userId: assetProfileUserId }) => { + return assetProfileUserId === userId; + }), + ({ id }) => { + return id; + } + ); + + const marketDataByAssetProfile = Object.fromEntries( + await Promise.all( + customAssetProfiles.map(async ({ dataSource, id, symbol }) => { + const marketData = ( + await this.marketDataService.marketDataItems({ + where: { dataSource, symbol } + }) + ).map(({ date, marketPrice }) => ({ + date: date.toISOString(), + marketPrice + })); + + return [id, marketData] as const; + }) + ) + ); + const tags = (await this.tagService.getTagsForUser(userId)) .filter( ({ id, isUsed }) => @@ -128,6 +160,55 @@ export class ExportService { return { meta: { date: new Date().toISOString(), version: environment.version }, accounts, + assetProfiles: customAssetProfiles.map( + ({ + assetClass, + assetSubClass, + comment, + countries, + currency, + cusip, + dataSource, + figi, + figiComposite, + figiShareClass, + holdings, + id, + isActive, + isin, + name, + scraperConfiguration, + sectors, + symbol, + symbolMapping, + url + }) => { + return { + assetClass, + assetSubClass, + comment, + countries: countries as unknown as Prisma.JsonArray, + currency, + cusip, + dataSource, + figi, + figiComposite, + figiShareClass, + holdings: holdings as unknown as Prisma.JsonArray, + id, + isActive, + isin, + marketData: marketDataByAssetProfile[id], + name, + scraperConfiguration: + scraperConfiguration as unknown as Prisma.JsonArray, + sectors: sectors as unknown as Prisma.JsonArray, + symbol, + symbolMapping, + url + }; + } + ), platforms: Object.values(platformsMap), tags, activities: activities.map( @@ -155,11 +236,7 @@ export class ExportService { currency: currency ?? SymbolProfile.currency, dataSource: SymbolProfile.dataSource, date: date.toISOString(), - symbol: - ['FEE', 'INTEREST', 'LIABILITY'].includes(type) || - (SymbolProfile.dataSource === 'MANUAL' && type === 'BUY') - ? SymbolProfile.name - : SymbolProfile.symbol, + symbol: SymbolProfile.symbol, tags: currentTags.map(({ id: tagId }) => { return tagId; }) diff --git a/libs/common/src/lib/interfaces/enhanced-symbol-profile.interface.ts b/libs/common/src/lib/interfaces/enhanced-symbol-profile.interface.ts index b810728d6..4cc6ba8aa 100644 --- a/libs/common/src/lib/interfaces/enhanced-symbol-profile.interface.ts +++ b/libs/common/src/lib/interfaces/enhanced-symbol-profile.interface.ts @@ -14,6 +14,7 @@ export interface EnhancedSymbolProfile { countries: Country[]; createdAt: Date; currency?: string; + cusip?: string; dataProviderInfo?: DataProviderInfo; dataSource: DataSource; dateOfFirstActivity?: Date; diff --git a/libs/common/src/lib/interfaces/export.interface.ts b/libs/common/src/lib/interfaces/export.interface.ts index 24e478779..905e33b06 100644 --- a/libs/common/src/lib/interfaces/export.interface.ts +++ b/libs/common/src/lib/interfaces/export.interface.ts @@ -1,4 +1,11 @@ -import { Account, Order, Platform, Tag } from '@prisma/client'; +import { + Account, + DataSource, + Order, + Platform, + SymbolProfile, + Tag +} from '@prisma/client'; import { AccountBalance } from './account-balance.interface'; @@ -15,7 +22,10 @@ export interface Export { | 'symbolProfileId' | 'updatedAt' | 'userId' - > & { date: string; symbol: string })[]; + > & { dataSource: DataSource; date: string; symbol: string })[]; + assetProfiles: (Omit & { + marketData: { date: string; marketPrice: number }[]; + })[]; meta: { date: string; version: string;