From 103c15ca31bea67fcd2d0108a69b5b15c4811700 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Thu, 16 Oct 2025 20:52:20 +0200 Subject: [PATCH] Feature/improve portfolio calculator unit tests by loading currency from user settings (#5765) * Use currency from user settings * Update changelog --- CHANGELOG.md | 1 + .../portfolio-calculator-test-utils.ts | 6 ++- .../roai/portfolio-calculator-btceur.spec.ts | 44 +++++++++---------- .../portfolio-calculator-btcusd-short.spec.ts | 44 +++++++++---------- .../roai/portfolio-calculator-btcusd.spec.ts | 44 +++++++++---------- ...ulator-novn-buy-and-sell-partially.spec.ts | 44 +++++++++---------- ...folio-calculator-novn-buy-and-sell.spec.ts | 44 +++++++++---------- .../ok/novn-buy-and-sell-partially.json | 7 ++- test/import/ok/novn-buy-and-sell.json | 7 ++- 9 files changed, 122 insertions(+), 119 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a024cc722..758898cee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Improved the currency validation in the search functionality of the data provider service - Optimized the get quotes functionality by utilizing the asset profile resolutions in the _Financial Modeling Prep_ service - Extracted the footer to a component +- Improved the portfolio calculator unit tests to load the user currency from the exported file ### Fixed 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 8850a6874..ccdbafac8 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 @@ -1,3 +1,5 @@ +import { Export } from '@ghostfolio/common/interfaces'; + import { readFileSync } from 'node:fs'; export const activityDummyData = { @@ -37,6 +39,6 @@ export const userDummyData = { id: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' }; -export function loadActivityExportFile(filePath: string) { - return JSON.parse(readFileSync(filePath, 'utf8')).activities; +export function loadExportFile(filePath: string): Export { + return JSON.parse(readFileSync(filePath, 'utf8')); } diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur.spec.ts index 1f6f9dc2a..1ac0dcd16 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur.spec.ts @@ -1,8 +1,7 @@ -import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto'; import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; import { activityDummyData, - loadActivityExportFile, + loadExportFile, symbolProfileDummyData, userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; @@ -16,9 +15,9 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate- import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service'; import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock'; import { parseDate } from '@ghostfolio/common/helper'; +import { Export } from '@ghostfolio/common/interfaces'; import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; -import { Tag } from '@prisma/client'; import { Big } from 'big.js'; import { join } from 'node:path'; @@ -53,7 +52,7 @@ jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { }); describe('PortfolioCalculator', () => { - let activityDtos: CreateOrderDto[]; + let exportResponse: Export; let configurationService: ConfigurationService; let currentRateService: CurrentRateService; @@ -63,7 +62,7 @@ describe('PortfolioCalculator', () => { let redisCacheService: RedisCacheService; beforeAll(() => { - activityDtos = loadActivityExportFile( + exportResponse = loadExportFile( join(__dirname, '../../../../../../../test/import/ok/btceur.json') ); }); @@ -97,28 +96,27 @@ describe('PortfolioCalculator', () => { it.only('with BTCUSD buy (in EUR)', async () => { jest.useFakeTimers().setSystemTime(parseDate('2022-01-14').getTime()); - const activities: Activity[] = activityDtos.map((activity) => ({ - ...activityDummyData, - ...activity, - date: parseDate(activity.date), - feeInAssetProfileCurrency: 4.46, - SymbolProfile: { - ...symbolProfileDummyData, - currency: 'USD', - dataSource: activity.dataSource, - name: 'Bitcoin', - symbol: activity.symbol - }, - tags: activity.tags?.map((id) => { - return { id } as Tag; - }), - unitPriceInAssetProfileCurrency: 44558.42 - })); + const activities: Activity[] = exportResponse.activities.map( + (activity) => ({ + ...activityDummyData, + ...activity, + date: parseDate(activity.date), + feeInAssetProfileCurrency: 4.46, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'USD', + dataSource: activity.dataSource, + name: 'Bitcoin', + symbol: activity.symbol + }, + unitPriceInAssetProfileCurrency: 44558.42 + }) + ); const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ activities, calculationType: PerformanceCalculationType.ROAI, - currency: 'USD', + currency: exportResponse.user.settings.currency, userId: userDummyData.id }); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-short.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-short.spec.ts index a2d7e60d3..29413c6ad 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-short.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-short.spec.ts @@ -1,8 +1,7 @@ -import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto'; import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; import { activityDummyData, - loadActivityExportFile, + loadExportFile, symbolProfileDummyData, userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; @@ -16,9 +15,9 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate- import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service'; import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock'; import { parseDate } from '@ghostfolio/common/helper'; +import { Export } from '@ghostfolio/common/interfaces'; import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; -import { Tag } from '@prisma/client'; import { Big } from 'big.js'; import { join } from 'node:path'; @@ -53,7 +52,7 @@ jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { }); describe('PortfolioCalculator', () => { - let activityDtos: CreateOrderDto[]; + let exportResponse: Export; let configurationService: ConfigurationService; let currentRateService: CurrentRateService; @@ -63,7 +62,7 @@ describe('PortfolioCalculator', () => { let redisCacheService: RedisCacheService; beforeAll(() => { - activityDtos = loadActivityExportFile( + exportResponse = loadExportFile( join(__dirname, '../../../../../../../test/import/ok/btcusd-short.json') ); }); @@ -97,28 +96,27 @@ describe('PortfolioCalculator', () => { it.only('with BTCUSD short sell (in USD)', async () => { jest.useFakeTimers().setSystemTime(parseDate('2022-01-14').getTime()); - const activities: Activity[] = activityDtos.map((activity) => ({ - ...activityDummyData, - ...activity, - date: parseDate(activity.date), - feeInAssetProfileCurrency: activity.fee, - SymbolProfile: { - ...symbolProfileDummyData, - currency: 'USD', - dataSource: activity.dataSource, - name: 'Bitcoin', - symbol: activity.symbol - }, - tags: activity.tags?.map((id) => { - return { id } as Tag; - }), - unitPriceInAssetProfileCurrency: activity.unitPrice - })); + const activities: Activity[] = exportResponse.activities.map( + (activity) => ({ + ...activityDummyData, + ...activity, + date: parseDate(activity.date), + feeInAssetProfileCurrency: activity.fee, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'USD', + dataSource: activity.dataSource, + name: 'Bitcoin', + symbol: activity.symbol + }, + unitPriceInAssetProfileCurrency: activity.unitPrice + }) + ); const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ activities, calculationType: PerformanceCalculationType.ROAI, - currency: 'USD', + currency: exportResponse.user.settings.currency, userId: userDummyData.id }); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd.spec.ts index bdccb23e0..26b3325c2 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd.spec.ts @@ -1,8 +1,7 @@ -import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto'; import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; import { activityDummyData, - loadActivityExportFile, + loadExportFile, symbolProfileDummyData, userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; @@ -16,9 +15,9 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate- import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service'; import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock'; import { parseDate } from '@ghostfolio/common/helper'; +import { Export } from '@ghostfolio/common/interfaces'; import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; -import { Tag } from '@prisma/client'; import { Big } from 'big.js'; import { join } from 'node:path'; @@ -53,7 +52,7 @@ jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { }); describe('PortfolioCalculator', () => { - let activityDtos: CreateOrderDto[]; + let exportResponse: Export; let configurationService: ConfigurationService; let currentRateService: CurrentRateService; @@ -63,7 +62,7 @@ describe('PortfolioCalculator', () => { let redisCacheService: RedisCacheService; beforeAll(() => { - activityDtos = loadActivityExportFile( + exportResponse = loadExportFile( join(__dirname, '../../../../../../../test/import/ok/btcusd.json') ); }); @@ -97,28 +96,27 @@ describe('PortfolioCalculator', () => { it.only('with BTCUSD buy (in USD)', async () => { jest.useFakeTimers().setSystemTime(parseDate('2022-01-14').getTime()); - const activities: Activity[] = activityDtos.map((activity) => ({ - ...activityDummyData, - ...activity, - date: parseDate(activity.date), - feeInAssetProfileCurrency: 4.46, - SymbolProfile: { - ...symbolProfileDummyData, - currency: 'USD', - dataSource: activity.dataSource, - name: 'Bitcoin', - symbol: activity.symbol - }, - tags: activity.tags?.map((id) => { - return { id } as Tag; - }), - unitPriceInAssetProfileCurrency: 44558.42 - })); + const activities: Activity[] = exportResponse.activities.map( + (activity) => ({ + ...activityDummyData, + ...activity, + date: parseDate(activity.date), + feeInAssetProfileCurrency: 4.46, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'USD', + dataSource: activity.dataSource, + name: 'Bitcoin', + symbol: activity.symbol + }, + unitPriceInAssetProfileCurrency: 44558.42 + }) + ); const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ activities, calculationType: PerformanceCalculationType.ROAI, - currency: 'USD', + currency: exportResponse.user.settings.currency, userId: userDummyData.id }); 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 4872a1004..0f1cdfff7 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 @@ -1,8 +1,7 @@ -import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto'; import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; import { activityDummyData, - loadActivityExportFile, + loadExportFile, symbolProfileDummyData, userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; @@ -16,9 +15,9 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate- import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service'; import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock'; import { parseDate } from '@ghostfolio/common/helper'; +import { Export } from '@ghostfolio/common/interfaces'; import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; -import { Tag } from '@prisma/client'; import { Big } from 'big.js'; import { join } from 'node:path'; @@ -53,7 +52,7 @@ jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { }); describe('PortfolioCalculator', () => { - let activityDtos: CreateOrderDto[]; + let exportResponse: Export; let configurationService: ConfigurationService; let currentRateService: CurrentRateService; @@ -63,7 +62,7 @@ describe('PortfolioCalculator', () => { let redisCacheService: RedisCacheService; beforeAll(() => { - activityDtos = loadActivityExportFile( + exportResponse = loadExportFile( join( __dirname, '../../../../../../../test/import/ok/novn-buy-and-sell-partially.json' @@ -100,28 +99,27 @@ describe('PortfolioCalculator', () => { it.only('with NOVN.SW buy and sell partially', async () => { jest.useFakeTimers().setSystemTime(parseDate('2022-04-11').getTime()); - const activities: Activity[] = activityDtos.map((activity) => ({ - ...activityDummyData, - ...activity, - date: parseDate(activity.date), - feeInAssetProfileCurrency: activity.fee, - SymbolProfile: { - ...symbolProfileDummyData, - currency: activity.currency, - dataSource: activity.dataSource, - name: 'Novartis AG', - symbol: activity.symbol - }, - tags: activity.tags?.map((id) => { - return { id } as Tag; - }), - unitPriceInAssetProfileCurrency: activity.unitPrice - })); + const activities: Activity[] = exportResponse.activities.map( + (activity) => ({ + ...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({ activities, calculationType: PerformanceCalculationType.ROAI, - currency: 'CHF', + currency: exportResponse.user.settings.currency, userId: userDummyData.id }); 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 e6c71230b..e426a68fa 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 @@ -1,8 +1,7 @@ -import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto'; import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; import { activityDummyData, - loadActivityExportFile, + loadExportFile, symbolProfileDummyData, userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; @@ -16,9 +15,9 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate- import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service'; import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock'; import { parseDate } from '@ghostfolio/common/helper'; +import { Export } from '@ghostfolio/common/interfaces'; import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; -import { Tag } from '@prisma/client'; import { Big } from 'big.js'; import { join } from 'node:path'; @@ -53,7 +52,7 @@ jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { }); describe('PortfolioCalculator', () => { - let activityDtos: CreateOrderDto[]; + let exportResponse: Export; let configurationService: ConfigurationService; let currentRateService: CurrentRateService; @@ -63,7 +62,7 @@ describe('PortfolioCalculator', () => { let redisCacheService: RedisCacheService; beforeAll(() => { - activityDtos = loadActivityExportFile( + exportResponse = loadExportFile( join( __dirname, '../../../../../../../test/import/ok/novn-buy-and-sell.json' @@ -100,28 +99,27 @@ describe('PortfolioCalculator', () => { it.only('with NOVN.SW buy and sell', async () => { jest.useFakeTimers().setSystemTime(parseDate('2022-04-11').getTime()); - const activities: Activity[] = activityDtos.map((activity) => ({ - ...activityDummyData, - ...activity, - date: parseDate(activity.date), - feeInAssetProfileCurrency: activity.fee, - SymbolProfile: { - ...symbolProfileDummyData, - currency: activity.currency, - dataSource: activity.dataSource, - name: 'Novartis AG', - symbol: activity.symbol - }, - tags: activity.tags?.map((id) => { - return { id } as Tag; - }), - unitPriceInAssetProfileCurrency: activity.unitPrice - })); + const activities: Activity[] = exportResponse.activities.map( + (activity) => ({ + ...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({ activities, calculationType: PerformanceCalculationType.ROAI, - currency: 'CHF', + currency: exportResponse.user.settings.currency, userId: userDummyData.id }); diff --git a/test/import/ok/novn-buy-and-sell-partially.json b/test/import/ok/novn-buy-and-sell-partially.json index 06cbc75ea..8c5778566 100644 --- a/test/import/ok/novn-buy-and-sell-partially.json +++ b/test/import/ok/novn-buy-and-sell-partially.json @@ -24,5 +24,10 @@ "date": "2022-03-07T00:00:00.000Z", "symbol": "NOVN.SW" } - ] + ], + "user": { + "settings": { + "currency": "CHF" + } + } } diff --git a/test/import/ok/novn-buy-and-sell.json b/test/import/ok/novn-buy-and-sell.json index b7ab6aee1..71ee9b7a9 100644 --- a/test/import/ok/novn-buy-and-sell.json +++ b/test/import/ok/novn-buy-and-sell.json @@ -24,5 +24,10 @@ "date": "2022-03-07T00:00:00.000Z", "symbol": "NOVN.SW" } - ] + ], + "user": { + "settings": { + "currency": "CHF" + } + } }