From 3361666f63f348a23ad24cdb06d3de6d958997da Mon Sep 17 00:00:00 2001 From: csehatt741 <77381875+csehatt741@users.noreply.github.com> Date: Fri, 18 Apr 2025 11:11:49 +0200 Subject: [PATCH] Feature/activity in custom currency (#4486) * Activity in custom currency * Update changelog --------- Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> --- CHANGELOG.md | 6 ++ apps/api/src/app/export/export.service.ts | 3 +- apps/api/src/app/import/import.controller.ts | 5 +- apps/api/src/app/import/import.service.ts | 88 ++++--------------- .../order/interfaces/activities.interface.ts | 4 +- apps/api/src/app/order/order.service.ts | 13 ++- .../portfolio-calculator-test-utils.ts | 3 +- .../calculator/portfolio-calculator.ts | 8 +- ...aln-buy-and-sell-in-two-activities.spec.ts | 12 +-- ...folio-calculator-baln-buy-and-sell.spec.ts | 8 +- .../portfolio-calculator-baln-buy.spec.ts | 4 +- ...ator-btcusd-buy-and-sell-partially.spec.ts | 8 +- .../roai/portfolio-calculator-fee.spec.ts | 4 +- .../portfolio-calculator-googl-buy.spec.ts | 4 +- .../roai/portfolio-calculator-item.spec.ts | 4 +- .../portfolio-calculator-liability.spec.ts | 4 +- ...-calculator-msft-buy-with-dividend.spec.ts | 8 +- ...ulator-novn-buy-and-sell-partially.spec.ts | 4 +- ...folio-calculator-novn-buy-and-sell.spec.ts | 4 +- .../src/app/portfolio/portfolio.service.ts | 8 +- ...ate-or-update-activity-dialog.component.ts | 86 +++--------------- .../create-or-update-activity-dialog.html | 74 +--------------- .../app/services/import-activities.service.ts | 3 +- .../activities-table.component.html | 2 +- .../migration.sql | 2 + test/import/ok-btceur.json | 29 ++++++ test/import/ok-btcusd.json | 29 ++++++ 27 files changed, 164 insertions(+), 263 deletions(-) create mode 100644 prisma/migrations/20250401084916_set_value_of_currency_to_null_in_order/migration.sql create mode 100644 test/import/ok-btceur.json create mode 100644 test/import/ok-btcusd.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 0850d7831..043f56390 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Changed + +- Added support for activities in a custom currency + ## 2.152.1 - 2025-04-17 ### Changed diff --git a/apps/api/src/app/export/export.service.ts b/apps/api/src/app/export/export.service.ts index f0449dc14..5efa429c7 100644 --- a/apps/api/src/app/export/export.service.ts +++ b/apps/api/src/app/export/export.service.ts @@ -120,6 +120,7 @@ export class ExportService { ({ accountId, comment, + currency, date, fee, id, @@ -137,7 +138,7 @@ export class ExportService { quantity, type, unitPrice, - currency: SymbolProfile.currency, + currency: currency ?? SymbolProfile.currency, dataSource: SymbolProfile.dataSource, date: date.toISOString(), symbol: ['FEE', 'INTEREST', 'ITEM', 'LIABILITY'].includes(type) diff --git a/apps/api/src/app/import/import.controller.ts b/apps/api/src/app/import/import.controller.ts index b276a3c3d..15631a3e8 100644 --- a/apps/api/src/app/import/import.controller.ts +++ b/apps/api/src/app/import/import.controller.ts @@ -98,12 +98,9 @@ export class ImportController { @Param('dataSource') dataSource: DataSource, @Param('symbol') symbol: string ): Promise { - const userCurrency = this.request.user.Settings.settings.baseCurrency; - const activities = await this.importService.getDividends({ dataSource, - symbol, - userCurrency + symbol }); return { activities }; diff --git a/apps/api/src/app/import/import.service.ts b/apps/api/src/app/import/import.service.ts index c72420417..babe7c3e3 100644 --- a/apps/api/src/app/import/import.service.ts +++ b/apps/api/src/app/import/import.service.ts @@ -10,12 +10,10 @@ import { PlatformService } from '@ghostfolio/api/app/platform/platform.service'; import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; -import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { DataGatheringService } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; import { DATA_GATHERING_QUEUE_PRIORITY_HIGH } from '@ghostfolio/common/config'; import { - DATE_FORMAT, getAssetProfileIdentifier, parseDate } from '@ghostfolio/common/helper'; @@ -29,8 +27,8 @@ import { import { Injectable } from '@nestjs/common'; import { DataSource, Prisma, SymbolProfile } from '@prisma/client'; import { Big } from 'big.js'; -import { endOfToday, format, isAfter, isSameSecond, parseISO } from 'date-fns'; -import { isNumber, uniqBy } from 'lodash'; +import { endOfToday, isAfter, isSameSecond, parseISO } from 'date-fns'; +import { uniqBy } from 'lodash'; import { v4 as uuidv4 } from 'uuid'; @Injectable() @@ -40,7 +38,6 @@ export class ImportService { private readonly configurationService: ConfigurationService, private readonly dataGatheringService: DataGatheringService, private readonly dataProviderService: DataProviderService, - private readonly exchangeRateDataService: ExchangeRateDataService, private readonly orderService: OrderService, private readonly platformService: PlatformService, private readonly portfolioService: PortfolioService, @@ -49,9 +46,8 @@ export class ImportService { public async getDividends({ dataSource, - symbol, - userCurrency - }: AssetProfileIdentifier & { userCurrency: string }): Promise { + symbol + }: AssetProfileIdentifier): Promise { try { const { firstBuyDate, historicalData, orders } = await this.portfolioService.getPosition(dataSource, undefined, symbol); @@ -121,22 +117,16 @@ export class ImportService { currency: undefined, createdAt: undefined, fee: 0, - feeInBaseCurrency: 0, + feeInAssetProfileCurrency: 0, id: assetProfile.id, isDraft: false, SymbolProfile: assetProfile, symbolProfileId: assetProfile.id, type: 'DIVIDEND', unitPrice: marketPrice, + unitPriceInAssetProfileCurrency: marketPrice, updatedAt: undefined, - userId: Account?.userId, - valueInBaseCurrency: - await this.exchangeRateDataService.toCurrencyAtDate( - value, - assetProfile.currency, - userCurrency, - date - ) + userId: Account?.userId }; }) ); @@ -266,17 +256,17 @@ export class ImportService { const activities: Activity[] = []; - for (const [index, activity] of activitiesExtendedWithErrors.entries()) { + for (const activity of activitiesExtendedWithErrors) { const accountId = activity.accountId; const comment = activity.comment; const currency = activity.currency; const date = activity.date; const error = activity.error; - let fee = activity.fee; + const fee = activity.fee; const quantity = activity.quantity; const SymbolProfile = activity.SymbolProfile; const type = activity.type; - let unitPrice = activity.unitPrice; + const unitPrice = activity.unitPrice; const assetProfile = assetProfiles[ getAssetProfileIdentifier({ @@ -284,7 +274,6 @@ export class ImportService { symbol: SymbolProfile.symbol }) ] ?? { - currency: SymbolProfile.currency, dataSource: SymbolProfile.dataSource, symbol: SymbolProfile.symbol }; @@ -320,35 +309,6 @@ export class ImportService { Account?: { id: string; name: string }; }); - if (SymbolProfile.currency !== assetProfile.currency) { - // Convert the unit price and fee to the asset currency if the imported - // activity is in a different currency - unitPrice = await this.exchangeRateDataService.toCurrencyAtDate( - unitPrice, - SymbolProfile.currency, - assetProfile.currency, - date - ); - - if (!isNumber(unitPrice)) { - throw new Error( - `activities.${index} historical exchange rate at ${format( - date, - DATE_FORMAT - )} is not available from "${SymbolProfile.currency}" to "${ - assetProfile.currency - }"` - ); - } - - fee = await this.exchangeRateDataService.toCurrencyAtDate( - fee, - SymbolProfile.currency, - assetProfile.currency, - date - ); - } - if (isDryRun) { order = { comment, @@ -400,6 +360,7 @@ export class ImportService { order = await this.orderService.createOrder({ comment, + currency, date, fee, quantity, @@ -439,21 +400,8 @@ export class ImportService { ...order, error, value, - feeInBaseCurrency: await this.exchangeRateDataService.toCurrencyAtDate( - fee, - assetProfile.currency, - userCurrency, - date - ), // @ts-ignore - SymbolProfile: assetProfile, - valueInBaseCurrency: - await this.exchangeRateDataService.toCurrencyAtDate( - value, - assetProfile.currency, - userCurrency, - date - ) + SymbolProfile: assetProfile }); } @@ -520,7 +468,8 @@ export class ImportService { return ( activity.accountId === accountId && activity.comment === comment && - activity.SymbolProfile.currency === currency && + (activity.currency === currency || + activity.SymbolProfile.currency === currency) && activity.SymbolProfile.dataSource === dataSource && isSameSecond(activity.date, date) && activity.fee === fee && @@ -538,6 +487,7 @@ export class ImportService { return { accountId, comment, + currency, date, error, fee, @@ -545,7 +495,6 @@ export class ImportService { type, unitPrice, SymbolProfile: { - currency, dataSource, symbol, activitiesCount: undefined, @@ -553,6 +502,7 @@ export class ImportService { assetSubClass: undefined, countries: undefined, createdAt: undefined, + currency: undefined, holdings: undefined, id: undefined, isActive: true, @@ -633,12 +583,6 @@ export class ImportService { `activities.${index}.symbol ("${symbol}") is not valid for the specified data source ("${dataSource}")` ); } - - if (assetProfile.currency !== currency) { - throw new Error( - `activities.${index}.currency ("${currency}") does not match with currency of ${assetProfile.symbol} ("${assetProfile.currency}")` - ); - } } assetProfiles[getAssetProfileIdentifier({ dataSource, symbol })] = diff --git a/apps/api/src/app/order/interfaces/activities.interface.ts b/apps/api/src/app/order/interfaces/activities.interface.ts index b16d10b7d..0c25c8ef8 100644 --- a/apps/api/src/app/order/interfaces/activities.interface.ts +++ b/apps/api/src/app/order/interfaces/activities.interface.ts @@ -11,12 +11,12 @@ export interface Activities { export interface Activity extends Order { Account?: AccountWithPlatform; error?: ActivityError; - feeInBaseCurrency: number; + feeInAssetProfileCurrency: number; SymbolProfile?: EnhancedSymbolProfile; tags?: Tag[]; + unitPriceInAssetProfileCurrency: number; updateAccountBalance?: boolean; value: number; - valueInBaseCurrency: number; } export interface ActivityError { diff --git a/apps/api/src/app/order/order.service.ts b/apps/api/src/app/order/order.service.ts index aa5ac4630..c2b822ac9 100644 --- a/apps/api/src/app/order/order.service.ts +++ b/apps/api/src/app/order/order.service.ts @@ -534,18 +534,25 @@ export class OrderService { return { ...order, value, - feeInBaseCurrency: + feeInAssetProfileCurrency: await this.exchangeRateDataService.toCurrencyAtDate( order.fee, + order.currency ?? order.SymbolProfile.currency, order.SymbolProfile.currency, - userCurrency, order.date ), SymbolProfile: assetProfile, + unitPriceInAssetProfileCurrency: + await this.exchangeRateDataService.toCurrencyAtDate( + order.unitPrice, + order.currency ?? order.SymbolProfile.currency, + order.SymbolProfile.currency, + order.date + ), valueInBaseCurrency: await this.exchangeRateDataService.toCurrencyAtDate( value, - order.SymbolProfile.currency, + order.currency ?? order.SymbolProfile.currency, userCurrency, order.date ) diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator-test-utils.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator-test-utils.ts index c5a902c29..2c9f7b6f3 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator-test-utils.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator-test-utils.ts @@ -6,10 +6,11 @@ export const activityDummyData = { comment: undefined, createdAt: new Date(), currency: undefined, - feeInBaseCurrency: undefined, + fee: undefined, id: undefined, isDraft: false, symbolProfileId: undefined, + unitPrice: undefined, updatedAt: new Date(), userId: undefined, value: undefined, diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index 52d57230b..9698cb315 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts @@ -112,12 +112,12 @@ export abstract class PortfolioCalculator { .map( ({ date, - fee, + feeInAssetProfileCurrency, quantity, SymbolProfile, tags = [], type, - unitPrice + unitPriceInAssetProfileCurrency }) => { if (isBefore(date, dateOfFirstActivity)) { dateOfFirstActivity = date; @@ -134,9 +134,9 @@ export abstract class PortfolioCalculator { tags, type, date: format(date, DATE_FORMAT), - fee: new Big(fee), + fee: new Big(feeInAssetProfileCurrency), quantity: new Big(quantity), - unitPrice: new Big(unitPrice) + unitPrice: new Big(unitPriceInAssetProfileCurrency) }; } ) diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts index e157e2d26..eed5a1e80 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts @@ -91,7 +91,7 @@ describe('PortfolioCalculator', () => { { ...activityDummyData, date: new Date('2021-11-22'), - fee: 1.55, + feeInAssetProfileCurrency: 1.55, quantity: 2, SymbolProfile: { ...symbolProfileDummyData, @@ -101,12 +101,12 @@ describe('PortfolioCalculator', () => { symbol: 'BALN.SW' }, type: 'BUY', - unitPrice: 142.9 + unitPriceInAssetProfileCurrency: 142.9 }, { ...activityDummyData, date: new Date('2021-11-30'), - fee: 1.65, + feeInAssetProfileCurrency: 1.65, quantity: 1, SymbolProfile: { ...symbolProfileDummyData, @@ -116,12 +116,12 @@ describe('PortfolioCalculator', () => { symbol: 'BALN.SW' }, type: 'SELL', - unitPrice: 136.6 + unitPriceInAssetProfileCurrency: 136.6 }, { ...activityDummyData, date: new Date('2021-11-30'), - fee: 0, + feeInAssetProfileCurrency: 0, quantity: 1, SymbolProfile: { ...symbolProfileDummyData, @@ -131,7 +131,7 @@ describe('PortfolioCalculator', () => { symbol: 'BALN.SW' }, type: 'SELL', - unitPrice: 136.6 + unitPriceInAssetProfileCurrency: 136.6 } ]; diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell.spec.ts index a1650ea82..15f3983fe 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell.spec.ts @@ -91,7 +91,7 @@ describe('PortfolioCalculator', () => { { ...activityDummyData, date: new Date('2021-11-22'), - fee: 1.55, + feeInAssetProfileCurrency: 1.55, quantity: 2, SymbolProfile: { ...symbolProfileDummyData, @@ -101,12 +101,12 @@ describe('PortfolioCalculator', () => { symbol: 'BALN.SW' }, type: 'BUY', - unitPrice: 142.9 + unitPriceInAssetProfileCurrency: 142.9 }, { ...activityDummyData, date: new Date('2021-11-30'), - fee: 1.65, + feeInAssetProfileCurrency: 1.65, quantity: 2, SymbolProfile: { ...symbolProfileDummyData, @@ -116,7 +116,7 @@ describe('PortfolioCalculator', () => { symbol: 'BALN.SW' }, type: 'SELL', - unitPrice: 136.6 + unitPriceInAssetProfileCurrency: 136.6 } ]; diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy.spec.ts index 63a4d77b4..7a34bd114 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy.spec.ts @@ -91,7 +91,7 @@ describe('PortfolioCalculator', () => { { ...activityDummyData, date: new Date('2021-11-30'), - fee: 1.55, + feeInAssetProfileCurrency: 1.55, quantity: 2, SymbolProfile: { ...symbolProfileDummyData, @@ -101,7 +101,7 @@ describe('PortfolioCalculator', () => { symbol: 'BALN.SW' }, type: 'BUY', - unitPrice: 136.6 + unitPriceInAssetProfileCurrency: 136.6 } ]; diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts index 2853e3d87..3158076cb 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts @@ -105,7 +105,7 @@ describe('PortfolioCalculator', () => { { ...activityDummyData, date: new Date('2015-01-01'), - fee: 0, + feeInAssetProfileCurrency: 0, quantity: 2, SymbolProfile: { ...symbolProfileDummyData, @@ -115,12 +115,12 @@ describe('PortfolioCalculator', () => { symbol: 'BTCUSD' }, type: 'BUY', - unitPrice: 320.43 + unitPriceInAssetProfileCurrency: 320.43 }, { ...activityDummyData, date: new Date('2017-12-31'), - fee: 0, + feeInAssetProfileCurrency: 0, quantity: 1, SymbolProfile: { ...symbolProfileDummyData, @@ -130,7 +130,7 @@ describe('PortfolioCalculator', () => { symbol: 'BTCUSD' }, type: 'SELL', - unitPrice: 14156.4 + unitPriceInAssetProfileCurrency: 14156.4 } ]; diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-fee.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-fee.spec.ts index b96e4f540..22a34af24 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-fee.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-fee.spec.ts @@ -91,7 +91,7 @@ describe('PortfolioCalculator', () => { { ...activityDummyData, date: new Date('2021-09-01'), - fee: 49, + feeInAssetProfileCurrency: 49, quantity: 0, SymbolProfile: { ...symbolProfileDummyData, @@ -101,7 +101,7 @@ describe('PortfolioCalculator', () => { symbol: '2c463fb3-af07-486e-adb0-8301b3d72141' }, type: 'FEE', - unitPrice: 0 + unitPriceInAssetProfileCurrency: 0 } ]; diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-googl-buy.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-googl-buy.spec.ts index b3793a5b4..41299eb40 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-googl-buy.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-googl-buy.spec.ts @@ -104,7 +104,7 @@ describe('PortfolioCalculator', () => { { ...activityDummyData, date: new Date('2023-01-03'), - fee: 1, + feeInAssetProfileCurrency: 1, quantity: 1, SymbolProfile: { ...symbolProfileDummyData, @@ -114,7 +114,7 @@ describe('PortfolioCalculator', () => { symbol: 'GOOGL' }, type: 'BUY', - unitPrice: 89.12 + unitPriceInAssetProfileCurrency: 89.12 } ]; diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-item.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-item.spec.ts index d226fe6f8..721c9ae96 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-item.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-item.spec.ts @@ -91,7 +91,7 @@ describe('PortfolioCalculator', () => { { ...activityDummyData, date: new Date('2022-01-01'), - fee: 0, + feeInAssetProfileCurrency: 0, quantity: 1, SymbolProfile: { ...symbolProfileDummyData, @@ -101,7 +101,7 @@ describe('PortfolioCalculator', () => { symbol: 'dac95060-d4f2-4653-a253-2c45e6fb5cde' }, type: 'ITEM', - unitPrice: 500000 + unitPriceInAssetProfileCurrency: 500000 } ]; diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-liability.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-liability.spec.ts index 569212b9a..a82e605d4 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-liability.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-liability.spec.ts @@ -91,7 +91,7 @@ describe('PortfolioCalculator', () => { { ...activityDummyData, date: new Date('2023-01-01'), // Date in future - fee: 0, + feeInAssetProfileCurrency: 0, quantity: 1, SymbolProfile: { ...symbolProfileDummyData, @@ -101,7 +101,7 @@ describe('PortfolioCalculator', () => { symbol: '55196015-1365-4560-aa60-8751ae6d18f8' }, type: 'LIABILITY', - unitPrice: 3000 + unitPriceInAssetProfileCurrency: 3000 } ]; diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-with-dividend.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-with-dividend.spec.ts index 4c54ba7aa..d8e774639 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-with-dividend.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-with-dividend.spec.ts @@ -104,7 +104,7 @@ describe('PortfolioCalculator', () => { { ...activityDummyData, date: new Date('2021-09-16'), - fee: 19, + feeInAssetProfileCurrency: 19, quantity: 1, SymbolProfile: { ...symbolProfileDummyData, @@ -114,12 +114,12 @@ describe('PortfolioCalculator', () => { symbol: 'MSFT' }, type: 'BUY', - unitPrice: 298.58 + unitPriceInAssetProfileCurrency: 298.58 }, { ...activityDummyData, date: new Date('2021-11-16'), - fee: 0, + feeInAssetProfileCurrency: 0, quantity: 1, SymbolProfile: { ...symbolProfileDummyData, @@ -129,7 +129,7 @@ describe('PortfolioCalculator', () => { symbol: 'MSFT' }, type: 'DIVIDEND', - unitPrice: 0.62 + unitPriceInAssetProfileCurrency: 0.62 } ]; diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell-partially.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell-partially.spec.ts index 84bcc5bc1..ffbfe7345 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell-partially.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell-partially.spec.ts @@ -105,13 +105,15 @@ describe('PortfolioCalculator', () => { ...activityDummyData, ...activity, date: parseDate(activity.date), + feeInAssetProfileCurrency: activity.fee, SymbolProfile: { ...symbolProfileDummyData, currency: activity.currency, dataSource: activity.dataSource, name: 'Novartis AG', symbol: activity.symbol - } + }, + unitPriceInAssetProfileCurrency: activity.unitPrice })); const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell.spec.ts index 937fd8b48..0256a6a1e 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell.spec.ts @@ -105,13 +105,15 @@ describe('PortfolioCalculator', () => { ...activityDummyData, ...activity, date: parseDate(activity.date), + feeInAssetProfileCurrency: activity.fee, SymbolProfile: { ...symbolProfileDummyData, currency: activity.currency, dataSource: activity.dataSource, name: 'Novartis AG', symbol: activity.symbol - } + }, + unitPriceInAssetProfileCurrency: activity.unitPrice })); const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 7287c103b..d3bbc1e06 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -246,10 +246,14 @@ export class PortfolioService { activities: Activity[]; groupBy?: GroupBy; }): Promise { - let dividends = activities.map(({ date, valueInBaseCurrency }) => { + let dividends = activities.map(({ currency, date, value }) => { return { date: format(date, DATE_FORMAT), - investment: valueInBaseCurrency + investment: this.exchangeRateDataService.toCurrency( + value, + currency, + this.getUserCurrency() + ) }; }); diff --git a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts index 555fbc7aa..f6ce2a81d 100644 --- a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts +++ b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts @@ -15,7 +15,7 @@ import { DateAdapter, MAT_DATE_LOCALE } from '@angular/material/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { AssetClass, AssetSubClass, Tag, Type } from '@prisma/client'; import { isAfter, isToday } from 'date-fns'; -import { EMPTY, Subject, lastValueFrom } from 'rxjs'; +import { EMPTY, Subject } from 'rxjs'; import { catchError, delay, takeUntil } from 'rxjs/operators'; import { DataService } from '../../../../services/data.service'; @@ -102,7 +102,8 @@ export class CreateOrUpdateActivityDialog implements OnDestroy { Validators.required ], currencyOfUnitPrice: [ - this.data.activity?.SymbolProfile?.currency, + this.data.activity?.currency ?? + this.data.activity?.SymbolProfile?.currency, Validators.required ], dataSource: [ @@ -111,7 +112,6 @@ export class CreateOrUpdateActivityDialog implements OnDestroy { ], date: [this.data.activity?.date, Validators.required], fee: [this.data.activity?.fee, Validators.required], - feeInCustomCurrency: [this.data.activity?.fee, Validators.required], name: [this.data.activity?.SymbolProfile?.name, Validators.required], quantity: [this.data.activity?.quantity, Validators.required], searchSymbol: [ @@ -133,10 +133,6 @@ export class CreateOrUpdateActivityDialog implements OnDestroy { ], type: [undefined, Validators.required], // Set after value changes subscription unitPrice: [this.data.activity?.unitPrice, Validators.required], - unitPriceInCustomCurrency: [ - this.data.activity?.unitPrice, - Validators.required - ], updateAccountBalance: [false] }); @@ -148,57 +144,6 @@ export class CreateOrUpdateActivityDialog implements OnDestroy { takeUntil(this.unsubscribeSubject) ) .subscribe(async () => { - let exchangeRateOfUnitPrice = 1; - - this.activityForm.get('feeInCustomCurrency').setErrors(null); - this.activityForm.get('unitPriceInCustomCurrency').setErrors(null); - - const currency = this.activityForm.get('currency').value; - const currencyOfUnitPrice = this.activityForm.get( - 'currencyOfUnitPrice' - ).value; - const date = this.activityForm.get('date').value; - - if ( - currency && - currencyOfUnitPrice && - currency !== currencyOfUnitPrice && - date - ) { - try { - const { marketPrice } = await lastValueFrom( - this.dataService - .fetchExchangeRateForDate({ - date, - symbol: `${currencyOfUnitPrice}-${currency}` - }) - .pipe(takeUntil(this.unsubscribeSubject)) - ); - - exchangeRateOfUnitPrice = marketPrice; - } catch { - this.activityForm.get('unitPriceInCustomCurrency').setErrors({ - invalid: true - }); - } - } - - const feeInCustomCurrency = - this.activityForm.get('feeInCustomCurrency').value * - exchangeRateOfUnitPrice; - - const unitPriceInCustomCurrency = - this.activityForm.get('unitPriceInCustomCurrency').value * - exchangeRateOfUnitPrice; - - this.activityForm.get('fee').setValue(feeInCustomCurrency, { - emitEvent: false - }); - - this.activityForm.get('unitPrice').setValue(unitPriceInCustomCurrency, { - emitEvent: false - }); - if ( this.activityForm.get('type').value === 'BUY' || this.activityForm.get('type').value === 'FEE' || @@ -265,10 +210,6 @@ export class CreateOrUpdateActivityDialog implements OnDestroy { this.activityForm.get('type').value ) ) { - this.activityForm - .get('dataSource') - .setValue(this.activityForm.get('searchSymbol').value.dataSource); - this.updateSymbol(); } @@ -297,7 +238,7 @@ export class CreateOrUpdateActivityDialog implements OnDestroy { .get('dataSource') .removeValidators(Validators.required); this.activityForm.get('dataSource').updateValueAndValidity(); - this.activityForm.get('feeInCustomCurrency').reset(); + this.activityForm.get('fee').reset(); this.activityForm.get('name').setValidators(Validators.required); this.activityForm.get('name').updateValueAndValidity(); this.activityForm.get('quantity').setValue(1); @@ -331,12 +272,11 @@ export class CreateOrUpdateActivityDialog implements OnDestroy { this.activityForm.get('dataSource').updateValueAndValidity(); if ( - (type === 'FEE' && - this.activityForm.get('feeInCustomCurrency').value === 0) || + (type === 'FEE' && this.activityForm.get('fee').value === 0) || type === 'INTEREST' || type === 'LIABILITY' ) { - this.activityForm.get('feeInCustomCurrency').reset(); + this.activityForm.get('fee').reset(); } this.activityForm.get('name').setValidators(Validators.required); @@ -354,7 +294,7 @@ export class CreateOrUpdateActivityDialog implements OnDestroy { this.activityForm.get('searchSymbol').updateValueAndValidity(); if (type === 'FEE') { - this.activityForm.get('unitPriceInCustomCurrency').setValue(0); + this.activityForm.get('unitPrice').setValue(0); } if ( @@ -410,7 +350,7 @@ export class CreateOrUpdateActivityDialog implements OnDestroy { public applyCurrentMarketPrice() { this.activityForm.patchValue({ currencyOfUnitPrice: this.activityForm.get('currency').value, - unitPriceInCustomCurrency: this.currentMarketPrice + unitPrice: this.currentMarketPrice }); } @@ -496,7 +436,7 @@ export class CreateOrUpdateActivityDialog implements OnDestroy { this.dataService .fetchSymbolItem({ - dataSource: this.activityForm.get('dataSource').value, + dataSource: this.activityForm.get('searchSymbol').value.dataSource, symbol: this.activityForm.get('searchSymbol').value.symbol }) .pipe( @@ -512,9 +452,11 @@ export class CreateOrUpdateActivityDialog implements OnDestroy { takeUntil(this.unsubscribeSubject) ) .subscribe(({ currency, dataSource, marketPrice }) => { - this.activityForm.get('currency').setValue(currency); - this.activityForm.get('currencyOfUnitPrice').setValue(currency); - this.activityForm.get('dataSource').setValue(dataSource); + if (this.mode === 'create') { + this.activityForm.get('currency').setValue(currency); + this.activityForm.get('currencyOfUnitPrice').setValue(currency); + this.activityForm.get('dataSource').setValue(dataSource); + } this.currentMarketPrice = marketPrice; diff --git a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html index 85fcf5a94..b0521530f 100644 --- a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html +++ b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html @@ -214,11 +214,7 @@ } } - +
- @if ( - activityForm.get('unitPriceInCustomCurrency').hasError('invalid') - ) { - Oops! Could not get the historical exchange rate - from - {{ - activityForm.get('date')?.value | date: defaultDateFormat - }} - } @if ( currentMarketPrice && @@ -263,36 +246,6 @@ } -
- - - @switch (activityForm.get('type')?.value) { - @case ('DIVIDEND') { - Dividend - } - @case ('FEE') { - Value - } - @case ('INTEREST') { - Value - } - @case ('ITEM') { - Value - } - @case ('LIABILITY') { - Value - } - @default { - Unit Price - } - } - - - {{ - activityForm.get('currency').value - }} - -
Fee - +
{{ activityForm.get('currencyOfUnitPrice').value }}
- @if (activityForm.get('feeInCustomCurrency').hasError('invalid')) { - Oops! Could not get the historical exchange rate - from - {{ - activityForm.get('date')?.value | date: defaultDateFormat - }} - } - -
-
- - Fee - - {{ - activityForm.get('currency').value - }}
@@ -392,7 +325,8 @@ [isCurrency]="true" [locale]="data.user?.settings?.locale" [unit]=" - activityForm.get('currency')?.value ?? data.user?.settings?.baseCurrency + activityForm.get('currencyOfUnitPrice')?.value ?? + data.user?.settings?.baseCurrency " [value]="total" /> diff --git a/apps/client/src/app/services/import-activities.service.ts b/apps/client/src/app/services/import-activities.service.ts index c1b2209b3..2164bd248 100644 --- a/apps/client/src/app/services/import-activities.service.ts +++ b/apps/client/src/app/services/import-activities.service.ts @@ -126,6 +126,7 @@ export class ImportActivitiesService { private convertToCreateOrderDto({ accountId, comment, + currency, date, fee, quantity, @@ -142,7 +143,7 @@ export class ImportActivitiesService { type, unitPrice, updateAccountBalance, - currency: SymbolProfile.currency, + currency: currency ?? SymbolProfile.currency, dataSource: SymbolProfile.dataSource, date: date.toString(), symbol: SymbolProfile.symbol diff --git a/libs/ui/src/lib/activities-table/activities-table.component.html b/libs/ui/src/lib/activities-table/activities-table.component.html index e5b33efd2..79a7d3417 100644 --- a/libs/ui/src/lib/activities-table/activities-table.component.html +++ b/libs/ui/src/lib/activities-table/activities-table.component.html @@ -280,7 +280,7 @@ class="d-none d-lg-table-cell px-1" mat-cell > - {{ element.SymbolProfile?.currency }} + {{ element.currency ?? element.SymbolProfile?.currency }} diff --git a/prisma/migrations/20250401084916_set_value_of_currency_to_null_in_order/migration.sql b/prisma/migrations/20250401084916_set_value_of_currency_to_null_in_order/migration.sql new file mode 100644 index 000000000..03a4e7178 --- /dev/null +++ b/prisma/migrations/20250401084916_set_value_of_currency_to_null_in_order/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +UPDATE "Order" SET "currency" = NULL; diff --git a/test/import/ok-btceur.json b/test/import/ok-btceur.json new file mode 100644 index 000000000..b370682f9 --- /dev/null +++ b/test/import/ok-btceur.json @@ -0,0 +1,29 @@ +{ + "meta": { + "date": "2021-12-12T00:00:00.000Z", + "version": "dev" + }, + "accounts": [], + "platforms": [], + "tags": [], + "activities": [ + { + "accountId": null, + "comment": null, + "fee": 3.94, + "quantity": 1, + "type": "BUY", + "unitPrice": 39378.5, + "currency": "EUR", + "dataSource": "YAHOO", + "date": "2021-12-12T00:00:00.000Z", + "symbol": "BTCUSD", + "tags": [] + } + ], + "user": { + "settings": { + "currency": "USD" + } + } +} diff --git a/test/import/ok-btcusd.json b/test/import/ok-btcusd.json new file mode 100644 index 000000000..fc2e1f66e --- /dev/null +++ b/test/import/ok-btcusd.json @@ -0,0 +1,29 @@ +{ + "meta": { + "date": "2021-12-12T00:00:00.000Z", + "version": "dev" + }, + "accounts": [], + "platforms": [], + "tags": [], + "activities": [ + { + "accountId": null, + "comment": null, + "fee": 4.46, + "quantity": 1, + "type": "BUY", + "unitPrice": 44558.42, + "currency": "USD", + "dataSource": "YAHOO", + "date": "2021-12-12T00:00:00.000Z", + "symbol": "BTCUSD", + "tags": [] + } + ], + "user": { + "settings": { + "currency": "USD" + } + } +}