diff --git a/CHANGELOG.md b/CHANGELOG.md index ab2f94cfc..8cbac09ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,21 @@ 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 + +### Added + +- Added the name to the tooltip of the chart of the holdings tab on the home page (experimental) + +### Changed + +- Exposed the timeout of the portfolio snapshot computation as an environment variable (`PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_TIMEOUT`) +- Improved the portfolio unit tests to work with exported activity files + +### Fixed + +- Considered the language of the user settings on login with _Security Token_ + ## 2.114.0 - 2024-10-10 ### Added 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 d458be708..217ec499b 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 { readFileSync } from 'fs'; + export const activityDummyData = { accountId: undefined, accountUserId: undefined, @@ -29,3 +31,7 @@ export const symbolProfileDummyData = { export const userDummyData = { id: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' }; + +export function loadActivityExportFile(filePath: string) { + return JSON.parse(readFileSync(filePath, 'utf8')).activities; +} diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell.spec.ts index 30b17dcd4..575d01dc6 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell.spec.ts @@ -1,6 +1,8 @@ +import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto'; import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; import { activityDummyData, + loadActivityExportFile, symbolProfileDummyData, userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; @@ -20,6 +22,7 @@ import { parseDate } from '@ghostfolio/common/helper'; import { Big } from 'big.js'; import { last } from 'lodash'; +import { join } from 'path'; jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { return { @@ -52,6 +55,8 @@ jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { }); describe('PortfolioCalculator', () => { + let activityDtos: CreateOrderDto[]; + let configurationService: ConfigurationService; let currentRateService: CurrentRateService; let exchangeRateDataService: ExchangeRateDataService; @@ -59,6 +64,15 @@ describe('PortfolioCalculator', () => { let portfolioSnapshotService: PortfolioSnapshotService; let redisCacheService: RedisCacheService; + beforeAll(() => { + activityDtos = loadActivityExportFile( + join( + __dirname, + '../../../../../../../test/import/ok-novn-buy-and-sell.json' + ) + ); + }); + beforeEach(() => { configurationService = new ConfigurationService(); @@ -89,38 +103,18 @@ describe('PortfolioCalculator', () => { it.only('with NOVN.SW buy and sell', async () => { jest.useFakeTimers().setSystemTime(parseDate('2022-04-11').getTime()); - const activities: Activity[] = [ - { - ...activityDummyData, - date: new Date('2022-03-07'), - fee: 0, - quantity: 2, - SymbolProfile: { - ...symbolProfileDummyData, - currency: 'CHF', - dataSource: 'YAHOO', - name: 'Novartis AG', - symbol: 'NOVN.SW' - }, - type: 'BUY', - unitPrice: 75.8 - }, - { - ...activityDummyData, - date: new Date('2022-04-08'), - fee: 0, - quantity: 2, - SymbolProfile: { - ...symbolProfileDummyData, - currency: 'CHF', - dataSource: 'YAHOO', - name: 'Novartis AG', - symbol: 'NOVN.SW' - }, - type: 'SELL', - unitPrice: 85.73 + const activities: Activity[] = activityDtos.map((activity) => ({ + ...activityDummyData, + ...activity, + date: parseDate(activity.date), + SymbolProfile: { + ...symbolProfileDummyData, + currency: activity.currency, + dataSource: activity.dataSource, + name: 'Novartis AG', + symbol: activity.symbol } - ]; + })); const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ activities, diff --git a/apps/api/src/services/configuration/configuration.service.ts b/apps/api/src/services/configuration/configuration.service.ts index cca393a2a..dafd4803c 100644 --- a/apps/api/src/services/configuration/configuration.service.ts +++ b/apps/api/src/services/configuration/configuration.service.ts @@ -4,6 +4,7 @@ import { DEFAULT_PROCESSOR_CONCURRENCY_GATHER_ASSET_PROFILE, DEFAULT_PROCESSOR_CONCURRENCY_GATHER_HISTORICAL_MARKET_DATA, DEFAULT_PROCESSOR_CONCURRENCY_PORTFOLIO_SNAPSHOT, + DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_TIMEOUT, DEFAULT_ROOT_URL } from '@ghostfolio/common/config'; @@ -59,6 +60,9 @@ export class ConfigurationService { PROCESSOR_CONCURRENCY_PORTFOLIO_SNAPSHOT: num({ default: DEFAULT_PROCESSOR_CONCURRENCY_PORTFOLIO_SNAPSHOT }), + PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_TIMEOUT: num({ + default: DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_TIMEOUT + }), REDIS_DB: num({ default: 0 }), REDIS_HOST: str({ default: 'localhost' }), REDIS_PASSWORD: str({ default: '' }), diff --git a/apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.module.ts b/apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.module.ts index 620feda53..058d971d8 100644 --- a/apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.module.ts +++ b/apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.module.ts @@ -8,7 +8,10 @@ import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data- import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module'; import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module'; import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service'; -import { PORTFOLIO_SNAPSHOT_QUEUE } from '@ghostfolio/common/config'; +import { + DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_TIMEOUT, + PORTFOLIO_SNAPSHOT_QUEUE +} from '@ghostfolio/common/config'; import { BullModule } from '@nestjs/bull'; import { Module } from '@nestjs/common'; @@ -20,7 +23,14 @@ import { PortfolioSnapshotProcessor } from './portfolio-snapshot.processor'; imports: [ AccountBalanceModule, BullModule.registerQueue({ - name: PORTFOLIO_SNAPSHOT_QUEUE + name: PORTFOLIO_SNAPSHOT_QUEUE, + settings: { + lockDuration: parseInt( + process.env.PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_TIMEOUT ?? + DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_TIMEOUT.toString(), + 10 + ) + } }), ConfigurationModule, DataProviderModule, diff --git a/apps/client/src/app/components/header/header.component.ts b/apps/client/src/app/components/header/header.component.ts index 972b6036f..af69f7b39 100644 --- a/apps/client/src/app/components/header/header.component.ts +++ b/apps/client/src/app/components/header/header.component.ts @@ -267,7 +267,18 @@ export class HeaderComponent implements OnChanges { this.settingsStorageService.getSetting(KEY_STAY_SIGNED_IN) === 'true' ); - this.router.navigate(['/']); + this.userService + .get() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((user) => { + const userLanguage = user?.settings?.language; + + if (userLanguage && document.documentElement.lang !== userLanguage) { + window.location.href = `../${userLanguage}`; + } else { + this.router.navigate(['/']); + } + }); } public ngOnDestroy() { diff --git a/libs/common/src/lib/config.ts b/libs/common/src/lib/config.ts index e6d68f49b..0b812f7cd 100644 --- a/libs/common/src/lib/config.ts +++ b/libs/common/src/lib/config.ts @@ -51,6 +51,7 @@ export const DEFAULT_PAGE_SIZE = 50; export const DEFAULT_PROCESSOR_CONCURRENCY_GATHER_ASSET_PROFILE = 1; export const DEFAULT_PROCESSOR_CONCURRENCY_GATHER_HISTORICAL_MARKET_DATA = 1; export const DEFAULT_PROCESSOR_CONCURRENCY_PORTFOLIO_SNAPSHOT = 1; +export const DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_TIMEOUT = 30000; export const DEFAULT_ROOT_URL = 'https://localhost:4200'; // USX is handled separately diff --git a/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts b/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts index b278180ea..0e694f6dc 100644 --- a/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts +++ b/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts @@ -227,16 +227,24 @@ export class GfTreemapChartComponent }), callbacks: { label: (context) => { + const name = context.raw._data.name; + const symbol = context.raw._data.symbol; + if (context.raw._data.valueInBaseCurrency !== null) { const value = context.raw._data.valueInBaseCurrency; - return `${value.toLocaleString(this.locale, { - maximumFractionDigits: 2, - minimumFractionDigits: 2 - })} ${this.baseCurrency}`; + + return [ + `${name ?? symbol}`, + `${value.toLocaleString(this.locale, { + maximumFractionDigits: 2, + minimumFractionDigits: 2 + })} ${this.baseCurrency}` + ]; } else { const percentage = context.raw._data.allocationInPercentage * 100; - return `${percentage.toFixed(2)}%`; + + return [`${name ?? symbol}`, `${percentage.toFixed(2)}%`]; } }, title: () => { diff --git a/test/import/ok-novn-buy-and-sell.json b/test/import/ok-novn-buy-and-sell.json index b8a62279d..b7ab6aee1 100644 --- a/test/import/ok-novn-buy-and-sell.json +++ b/test/import/ok-novn-buy-and-sell.json @@ -11,7 +11,7 @@ "unitPrice": 85.73, "currency": "CHF", "dataSource": "YAHOO", - "date": "2022-04-07T22:00:00.000Z", + "date": "2022-04-08T00:00:00.000Z", "symbol": "NOVN.SW" }, { @@ -21,7 +21,7 @@ "unitPrice": 75.8, "currency": "CHF", "dataSource": "YAHOO", - "date": "2022-03-06T23:00:00.000Z", + "date": "2022-03-07T00:00:00.000Z", "symbol": "NOVN.SW" } ]