diff --git a/CHANGELOG.md b/CHANGELOG.md index 74ef71036..a65ce5f3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Changed + +- Improved the language localization for Türkçe (`tr`) + +## 2.78.0 - 2024-05-02 + ### Added - Added a form validation against the DTO in the create or update access dialog @@ -17,10 +23,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Improved the language localization for Türkçe (`tr`) +- Set the performance column of the holdings table to stick at the end +- Skipped the caching in the portfolio calculator if there are active filters (experimental) +- Improved the `INACTIVE` user role ### Fixed - Fixed an issue in the calculation of the portfolio summary caused by future liabilities +- Fixed a division by zero error in the dividend yield calculation (experimental) ## 2.77.1 - 2024-04-27 diff --git a/apps/api/src/app/auth/jwt.strategy.ts b/apps/api/src/app/auth/jwt.strategy.ts index c7ce38986..a8ad8fd08 100644 --- a/apps/api/src/app/auth/jwt.strategy.ts +++ b/apps/api/src/app/auth/jwt.strategy.ts @@ -2,10 +2,12 @@ import { UserService } from '@ghostfolio/api/app/user/user.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { HEADER_KEY_TIMEZONE } from '@ghostfolio/common/config'; +import { hasRole } from '@ghostfolio/common/permissions'; -import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { HttpException, Injectable } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import * as countriesAndTimezones from 'countries-and-timezones'; +import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { ExtractJwt, Strategy } from 'passport-jwt'; @Injectable() @@ -29,6 +31,13 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { if (user) { if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) { + if (hasRole(user, 'INACTIVE')) { + throw new HttpException( + getReasonPhrase(StatusCodes.TOO_MANY_REQUESTS), + StatusCodes.TOO_MANY_REQUESTS + ); + } + const country = countriesAndTimezones.getCountryForTimezone(timezone)?.id; @@ -45,10 +54,20 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { return user; } else { - throw ''; + throw new HttpException( + getReasonPhrase(StatusCodes.NOT_FOUND), + StatusCodes.NOT_FOUND + ); + } + } catch (error) { + if (error?.getStatus() === StatusCodes.TOO_MANY_REQUESTS) { + throw error; + } else { + throw new HttpException( + getReasonPhrase(StatusCodes.UNAUTHORIZED), + StatusCodes.UNAUTHORIZED + ); } - } catch (err) { - throw new UnauthorizedException('unauthorized', err.message); } } } diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.factory.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.factory.ts index 4937f1008..762415d1e 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.factory.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.factory.ts @@ -32,6 +32,7 @@ export class PortfolioCalculatorFactory { calculationType, currency, dateRange = 'max', + hasFilters, isExperimentalFeatures = false, userId }: { @@ -40,9 +41,12 @@ export class PortfolioCalculatorFactory { calculationType: PerformanceCalculationType; currency: string; dateRange?: DateRange; + hasFilters: boolean; isExperimentalFeatures?: boolean; userId: string; }): PortfolioCalculator { + const useCache = !hasFilters && isExperimentalFeatures; + switch (calculationType) { case PerformanceCalculationType.MWR: return new MWRPortfolioCalculator({ @@ -50,7 +54,7 @@ export class PortfolioCalculatorFactory { activities, currency, dateRange, - isExperimentalFeatures, + useCache, userId, configurationService: this.configurationService, currentRateService: this.currentRateService, @@ -64,7 +68,7 @@ export class PortfolioCalculatorFactory { currency, currentRateService: this.currentRateService, dateRange, - isExperimentalFeatures, + useCache, userId, configurationService: this.configurationService, exchangeRateDataService: this.exchangeRateDataService, diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index 568969603..e021eb2d4 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts @@ -56,14 +56,15 @@ export abstract class PortfolioCalculator { private currency: string; private currentRateService: CurrentRateService; private dataProviderInfos: DataProviderInfo[]; + private dateRange: DateRange; private endDate: Date; private exchangeRateDataService: ExchangeRateDataService; - private isExperimentalFeatures: boolean; private redisCacheService: RedisCacheService; private snapshot: PortfolioSnapshot; private snapshotPromise: Promise; private startDate: Date; private transactionPoints: TransactionPoint[]; + private useCache: boolean; private userId: string; public constructor({ @@ -74,8 +75,8 @@ export abstract class PortfolioCalculator { currentRateService, dateRange, exchangeRateDataService, - isExperimentalFeatures, redisCacheService, + useCache, userId }: { accountBalanceItems: HistoricalDataItem[]; @@ -85,16 +86,16 @@ export abstract class PortfolioCalculator { currentRateService: CurrentRateService; dateRange: DateRange; exchangeRateDataService: ExchangeRateDataService; - isExperimentalFeatures: boolean; redisCacheService: RedisCacheService; + useCache: boolean; userId: string; }) { this.accountBalanceItems = accountBalanceItems; this.configurationService = configurationService; this.currency = currency; this.currentRateService = currentRateService; + this.dateRange = dateRange; this.exchangeRateDataService = exchangeRateDataService; - this.isExperimentalFeatures = isExperimentalFeatures; this.activities = activities .map( @@ -129,6 +130,7 @@ export abstract class PortfolioCalculator { }); this.redisCacheService = redisCacheService; + this.useCache = useCache; this.userId = userId; const { endDate, startDate } = getInterval(dateRange); @@ -1047,11 +1049,13 @@ export abstract class PortfolioCalculator { } private async initialize() { - if (this.isExperimentalFeatures) { + if (this.useCache) { const startTimeTotal = performance.now(); const cachedSnapshot = await this.redisCacheService.get( - this.redisCacheService.getPortfolioSnapshotKey(this.userId) + this.redisCacheService.getPortfolioSnapshotKey({ + userId: this.userId + }) ); if (cachedSnapshot) { @@ -1074,7 +1078,9 @@ export abstract class PortfolioCalculator { ); this.redisCacheService.set( - this.redisCacheService.getPortfolioSnapshotKey(this.userId), + this.redisCacheService.getPortfolioSnapshotKey({ + userId: this.userId + }), JSON.stringify(this.snapshot), this.configurationService.get('CACHE_QUOTES_TTL') ); diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts index 422cf8bff..340f16b87 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts @@ -123,6 +123,7 @@ describe('PortfolioCalculator', () => { activities, calculationType: PerformanceCalculationType.TWR, currency: 'CHF', + hasFilters: false, userId: userDummyData.id }); diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell.spec.ts index dee8b2478..53ebdf19f 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell.spec.ts @@ -108,6 +108,7 @@ describe('PortfolioCalculator', () => { activities, calculationType: PerformanceCalculationType.TWR, currency: 'CHF', + hasFilters: false, userId: userDummyData.id }); diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy.spec.ts index db8ce01b3..bab265887 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy.spec.ts @@ -93,6 +93,7 @@ describe('PortfolioCalculator', () => { activities, calculationType: PerformanceCalculationType.TWR, currency: 'CHF', + hasFilters: false, userId: userDummyData.id }); diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts index 5a403eda1..eba5d4674 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts @@ -121,6 +121,7 @@ describe('PortfolioCalculator', () => { activities, calculationType: PerformanceCalculationType.TWR, currency: 'CHF', + hasFilters: false, userId: userDummyData.id }); diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-fee.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-fee.spec.ts index 97a77492b..88d7adb71 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-fee.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-fee.spec.ts @@ -93,6 +93,7 @@ describe('PortfolioCalculator', () => { activities, calculationType: PerformanceCalculationType.TWR, currency: 'USD', + hasFilters: false, userId: userDummyData.id }); diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-googl-buy.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-googl-buy.spec.ts index c916a381d..690f1eb51 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-googl-buy.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-googl-buy.spec.ts @@ -106,6 +106,7 @@ describe('PortfolioCalculator', () => { activities, calculationType: PerformanceCalculationType.TWR, currency: 'CHF', + hasFilters: false, userId: userDummyData.id }); diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-item.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-item.spec.ts index bf212f80b..422d119b2 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-item.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-item.spec.ts @@ -93,6 +93,7 @@ describe('PortfolioCalculator', () => { activities, calculationType: PerformanceCalculationType.TWR, currency: 'USD', + hasFilters: false, userId: userDummyData.id }); diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-liability.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-liability.spec.ts index 3ae63c72a..d468e8e00 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-liability.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-liability.spec.ts @@ -93,6 +93,7 @@ describe('PortfolioCalculator', () => { activities, calculationType: PerformanceCalculationType.TWR, currency: 'USD', + hasFilters: false, userId: userDummyData.id }); diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-msft-buy-with-dividend.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-msft-buy-with-dividend.spec.ts index 6948b4dbd..094c6cc2e 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-msft-buy-with-dividend.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-msft-buy-with-dividend.spec.ts @@ -121,6 +121,7 @@ describe('PortfolioCalculator', () => { activities, calculationType: PerformanceCalculationType.TWR, currency: 'USD', + hasFilters: false, userId: userDummyData.id }); diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-no-orders.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-no-orders.spec.ts index 1dd74d7e5..6bb432bfc 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-no-orders.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-no-orders.spec.ts @@ -71,6 +71,7 @@ describe('PortfolioCalculator', () => { activities: [], calculationType: PerformanceCalculationType.TWR, currency: 'CHF', + hasFilters: false, userId: userDummyData.id }); diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell-partially.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell-partially.spec.ts index d4451503a..f65d2ba61 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell-partially.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell-partially.spec.ts @@ -108,6 +108,7 @@ describe('PortfolioCalculator', () => { activities, calculationType: PerformanceCalculationType.TWR, currency: 'CHF', + hasFilters: false, userId: userDummyData.id }); 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 7850fb2bd..902f710ee 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 @@ -108,6 +108,7 @@ describe('PortfolioCalculator', () => { activities, calculationType: PerformanceCalculationType.TWR, currency: 'CHF', + hasFilters: false, userId: userDummyData.id }); diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 573c48a83..10c09a21f 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -277,9 +277,11 @@ export class PortfolioService { const portfolioCalculator = this.calculatorFactory.createCalculator({ activities, + dateRange, userId, calculationType: PerformanceCalculationType.TWR, currency: this.request.user.Settings.settings.baseCurrency, + hasFilters: filters?.length > 0, isExperimentalFeatures: this.request.user.Settings.settings.isExperimentalFeatures }); @@ -358,6 +360,7 @@ export class PortfolioService { userId, calculationType: PerformanceCalculationType.TWR, currency: userCurrency, + hasFilters: filters?.length > 0, isExperimentalFeatures: this.request.user?.Settings.settings.isExperimentalFeatures }); @@ -660,6 +663,7 @@ export class PortfolioService { }), calculationType: PerformanceCalculationType.TWR, currency: userCurrency, + hasFilters: true, isExperimentalFeatures: this.request.user.Settings.settings.isExperimentalFeatures }); @@ -700,17 +704,19 @@ export class PortfolioService { const dividendYieldPercent = this.getAnnualizedPerformancePercent({ daysInMarket: differenceInDays(new Date(), parseDate(firstBuyDate)), - netPerformancePercent: dividendInBaseCurrency.div( - timeWeightedInvestment - ) + netPerformancePercent: timeWeightedInvestment.eq(0) + ? new Big(0) + : dividendInBaseCurrency.div(timeWeightedInvestment) }); const dividendYieldPercentWithCurrencyEffect = this.getAnnualizedPerformancePercent({ daysInMarket: differenceInDays(new Date(), parseDate(firstBuyDate)), - netPerformancePercent: dividendInBaseCurrency.div( - timeWeightedInvestmentWithCurrencyEffect - ) + netPerformancePercent: timeWeightedInvestmentWithCurrencyEffect.eq(0) + ? new Big(0) + : dividendInBaseCurrency.div( + timeWeightedInvestmentWithCurrencyEffect + ) }); const historicalData = await this.dataProviderService.getHistorical( @@ -931,6 +937,7 @@ export class PortfolioService { userId, calculationType: PerformanceCalculationType.TWR, currency: this.request.user.Settings.settings.baseCurrency, + hasFilters: filters?.length > 0, isExperimentalFeatures: this.request.user.Settings.settings.isExperimentalFeatures }); @@ -1085,7 +1092,7 @@ export class PortfolioService { ) ); - const { endDate, startDate } = getInterval(dateRange); + const { endDate } = getInterval(dateRange); const { activities } = await this.orderService.getOrders({ endDate, @@ -1123,6 +1130,7 @@ export class PortfolioService { userId, calculationType: PerformanceCalculationType.TWR, currency: userCurrency, + hasFilters: filters?.length > 0, isExperimentalFeatures: this.request.user.Settings.settings.isExperimentalFeatures }); @@ -1220,6 +1228,7 @@ export class PortfolioService { userId, calculationType: PerformanceCalculationType.TWR, currency: this.request.user.Settings.settings.baseCurrency, + hasFilters: false, isExperimentalFeatures: this.request.user.Settings.settings.isExperimentalFeatures }); diff --git a/apps/api/src/app/redis-cache/redis-cache.service.ts b/apps/api/src/app/redis-cache/redis-cache.service.ts index a313eadf1..53b177b4f 100644 --- a/apps/api/src/app/redis-cache/redis-cache.service.ts +++ b/apps/api/src/app/redis-cache/redis-cache.service.ts @@ -24,7 +24,7 @@ export class RedisCacheService { return this.cache.get(key); } - public getPortfolioSnapshotKey(userId: string) { + public getPortfolioSnapshotKey({ userId }: { userId: string }) { return `portfolio-snapshot-${userId}`; } diff --git a/apps/api/src/app/user/user.controller.ts b/apps/api/src/app/user/user.controller.ts index 14c545192..39e78dcdc 100644 --- a/apps/api/src/app/user/user.controller.ts +++ b/apps/api/src/app/user/user.controller.ts @@ -2,11 +2,7 @@ import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorat import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { PropertyService } from '@ghostfolio/api/services/property/property.service'; import { User, UserSettings } from '@ghostfolio/common/interfaces'; -import { - hasPermission, - hasRole, - permissions -} from '@ghostfolio/common/permissions'; +import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import type { RequestWithUser } from '@ghostfolio/common/types'; import { @@ -63,13 +59,6 @@ export class UserController { public async getUser( @Headers('accept-language') acceptLanguage: string ): Promise { - if (hasRole(this.request.user, 'INACTIVE')) { - throw new HttpException( - getReasonPhrase(StatusCodes.TOO_MANY_REQUESTS), - StatusCodes.TOO_MANY_REQUESTS - ); - } - return this.userService.getUser( this.request.user, acceptLanguage?.split(',')?.[0] diff --git a/apps/api/src/events/portfolio-changed.listener.ts b/apps/api/src/events/portfolio-changed.listener.ts index 0f8877127..4a6e3d386 100644 --- a/apps/api/src/events/portfolio-changed.listener.ts +++ b/apps/api/src/events/portfolio-changed.listener.ts @@ -17,7 +17,9 @@ export class PortfolioChangedListener { ); this.redisCacheService.remove( - this.redisCacheService.getPortfolioSnapshotKey(event.getUserId()) + this.redisCacheService.getPortfolioSnapshotKey({ + userId: event.getUserId() + }) ); } } diff --git a/apps/client/src/app/core/auth.guard.ts b/apps/client/src/app/core/auth.guard.ts index 52d1e14ab..ee5ed77cd 100644 --- a/apps/client/src/app/core/auth.guard.ts +++ b/apps/client/src/app/core/auth.guard.ts @@ -54,9 +54,10 @@ export class AuthGuard { this.router.navigate(['/' + $localize`register`]); resolve(false); } else if ( - AuthGuard.PUBLIC_PAGE_ROUTES.filter((publicPageRoute) => - state.url.startsWith(publicPageRoute) - )?.length > 0 + AuthGuard.PUBLIC_PAGE_ROUTES.filter((publicPageRoute) => { + const [, url] = state.url.split('/'); + return `/${url}` === publicPageRoute; + })?.length > 0 ) { resolve(true); return EMPTY; diff --git a/libs/ui/src/lib/holdings-table/holdings-table.component.html b/libs/ui/src/lib/holdings-table/holdings-table.component.html index 8c0bca20f..181b120a8 100644 --- a/libs/ui/src/lib/holdings-table/holdings-table.component.html +++ b/libs/ui/src/lib/holdings-table/holdings-table.component.html @@ -109,7 +109,7 @@ - +