diff --git a/CHANGELOG.md b/CHANGELOG.md index f6ee7e60a9..9a792bb55e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Upgraded `stripe` from version `23.0.0` to `23.0.1` +### Fixed + +- Fixed the value of zero in the portfolio performance for date ranges starting after the end of an expired portfolio snapshot + ## 3.96.0 - 2026-10-31 ### Added diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index a04ca55fff..8d4e081d7f 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -575,7 +575,8 @@ export class PortfolioController { 'grossPerformanceWithCurrencyEffect', 'netPerformance', 'netPerformanceWithCurrencyEffect', - 'totalInvestment' + 'totalInvestment', + 'totalInvestmentValueWithCurrencyEffect' ] ); } diff --git a/apps/api/src/app/portfolio/portfolio.service.spec.ts b/apps/api/src/app/portfolio/portfolio.service.spec.ts index 30c274cb90..3994dc7c93 100644 --- a/apps/api/src/app/portfolio/portfolio.service.spec.ts +++ b/apps/api/src/app/portfolio/portfolio.service.spec.ts @@ -1202,6 +1202,81 @@ describe('PortfolioService', () => { { date: '2025-01-01', investmentValueWithCurrencyEffect: 200 } ]); }); + + describe('with an empty chart of the date range', () => { + const historicalData = [ + { date: '2023-03-01' }, + { + date: '2025-06-12', + dividendInBaseCurrency: 50, + netPerformance: 600, + netPerformanceInPercentage: 0.6, + netPerformanceInPercentageWithCurrencyEffect: 0.5, + netPerformanceWithCurrencyEffect: 500, + netWorth: 2000, + totalInvestment: 1000, + totalInvestmentValueWithCurrencyEffect: 1100, + valueWithCurrencyEffect: 1600 + } + ]; + + beforeEach(() => { + getPerformanceOfCalculator.mockResolvedValueOnce({ chart: [] }); + + jest + .spyOn(portfolioCalculatorFactory, 'createCalculator') + .mockReturnValue({ + getPerformance: getPerformanceOfCalculator, + getSnapshot: jest.fn().mockResolvedValue({ + historicalData, + errors: [], + hasErrors: false + }) + } as unknown as PortfolioCalculator); + }); + + it('should return the latest values of the portfolio snapshot without any performance when the portfolio snapshot ends before the date range', async () => { + const { chart, performance } = await portfolioService.getPerformance({ + dateRange: '1d', + userId: userDummyData.id + }); + + expect(chart).toEqual([]); + expect(performance).toEqual({ + currentNetWorth: 2000, + currentValueInBaseCurrency: 1600, + dividendInBaseCurrency: 0, + dividendPercentageWithCurrencyEffect: 0, + netPerformance: 0, + netPerformancePercentage: 0, + netPerformancePercentageWithCurrencyEffect: 0, + netPerformanceWithCurrencyEffect: 0, + totalInvestment: 1000, + totalInvestmentValueWithCurrencyEffect: 1100 + }); + }); + + it('should return no values when the portfolio snapshot ends after the date range', async () => { + const { chart, performance } = await portfolioService.getPerformance({ + dateRange: '2022', + userId: userDummyData.id + }); + + expect(chart).toEqual([]); + expect(performance).toEqual({ + currentNetWorth: 0, + currentValueInBaseCurrency: 0, + dividendInBaseCurrency: 0, + dividendPercentageWithCurrencyEffect: 0, + netPerformance: 0, + netPerformancePercentage: 0, + netPerformancePercentageWithCurrencyEffect: 0, + netPerformanceWithCurrencyEffect: 0, + totalInvestment: 0, + totalInvestmentValueWithCurrencyEffect: 0 + }); + }); + }); }); describe('getSummary', () => { diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 92faa9f712..8cb3ce9f1f 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -98,6 +98,7 @@ import { endOfDay, format, isAfter, + isBefore, isSameMonth, isSameYear, min, @@ -1234,6 +1235,17 @@ export class PortfolioService { start: startDate }); + // An expired portfolio snapshot can end before the date range until its + // computation in the background is complete. In this case, the performance + // in the date range is not known, but the latest values are available. + const latestHistoricalDataItem = historicalData.at(-1); + + const latestValues = + latestHistoricalDataItem && + isBefore(parseDate(latestHistoricalDataItem.date), startDate) + ? latestHistoricalDataItem + : undefined; + const { dividendInBaseCurrency, dividendInPercentageWithCurrencyEffect, @@ -1252,9 +1264,11 @@ export class PortfolioService { netPerformanceInPercentage: 0, netPerformanceInPercentageWithCurrencyEffect: 0, netPerformanceWithCurrencyEffect: 0, - netWorth: 0, - totalInvestment: 0, - valueWithCurrencyEffect: 0 + netWorth: latestValues?.netWorth ?? 0, + totalInvestment: latestValues?.totalInvestment ?? 0, + totalInvestmentValueWithCurrencyEffect: + latestValues?.totalInvestmentValueWithCurrencyEffect ?? 0, + valueWithCurrencyEffect: latestValues?.valueWithCurrencyEffect ?? 0 }; const chart =