diff --git a/CHANGELOG.md b/CHANGELOG.md index d03150bd43..d073e016bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Skipped opening the holding detail dialog for cash positions on the allocations page, the analysis page and the portfolio holdings page +- Resolved an exception in the `GET api/v1/portfolio/holding/:dataSource/:symbol` endpoint for cash positions +- Improved the error handling in the access endpoints (`POST` and `PUT`) to return `400 Bad Request` when granting access to a non-existent user ## 3.31.0 - 2026-07-20 diff --git a/apps/api/src/app/access/access.controller.ts b/apps/api/src/app/access/access.controller.ts index 35b1d485b0..d692f358df 100644 --- a/apps/api/src/app/access/access.controller.ts +++ b/apps/api/src/app/access/access.controller.ts @@ -87,7 +87,7 @@ export class AccessController { } try { - return this.accessService.createAccess({ + return await this.accessService.createAccess({ alias: data.alias || undefined, granteeUser: data.granteeUserId ? { connect: { id: data.granteeUserId } } @@ -155,7 +155,7 @@ export class AccessController { } try { - return this.accessService.updateAccess({ + return await this.accessService.updateAccess({ data: { alias: data.alias, granteeUser: data.granteeUserId diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index cee94f0208..c1e795c4af 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts @@ -51,6 +51,8 @@ import { format, isAfter, isBefore, + isFuture, + isPast, isWithinInterval, min, startOfDay, @@ -134,7 +136,7 @@ export abstract class PortfolioCalculator { dateOfFirstActivity = date; } - if (isAfter(date, new Date())) { + if (isFuture(date)) { // Adapt date to today if activity is in future (e.g. liability) // to include it in the interval date = endOfDay(new Date()); @@ -1113,7 +1115,7 @@ export abstract class PortfolioCalculator { portfolioSnapshot ); - if (isAfter(new Date(), new Date(expiration))) { + if (isPast(new Date(expiration))) { isCachedPortfolioSnapshotExpired = true; } } catch {} diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 6617b8f9b1..b3e86e0502 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -783,10 +783,24 @@ export class PortfolioService { return undefined; } - const [SymbolProfile] = await this.symbolProfileService.getSymbolProfiles([ + const [symbolProfile] = await this.symbolProfileService.getSymbolProfiles([ { dataSource, symbol } ]); + const assetProfile = + symbolProfile ?? + ({ + dataSource, + symbol, + assetClass: AssetClass.LIQUIDITY, + assetSubClass: AssetSubClass.CASH, + countries: [], + currency: symbol, + holdings: [], + name: symbol, + sectors: [] + } as EnhancedSymbolProfile); + const portfolioCalculator = this.calculatorFactory.createCalculator({ activities, userId, @@ -829,9 +843,10 @@ export class PortfolioService { timeWeightedInvestmentWithCurrencyEffect } = holding; - const activitiesOfHolding = activities.filter(({ assetProfile }) => { + const activitiesOfHolding = activities.filter((activity) => { return ( - assetProfile.dataSource === dataSource && assetProfile.symbol === symbol + activity.assetProfile.dataSource === dataSource && + activity.assetProfile.symbol === symbol ); }); @@ -863,19 +878,17 @@ export class PortfolioService { new Date() ); + const [firstActivity] = activitiesOfHolding; + const referenceUnitPrice = + firstActivity?.unitPriceInAssetProfileCurrency ?? marketPrice; + const historicalDataArray: HistoricalDataItem[] = []; - let marketPriceMax = Math.max( - activitiesOfHolding[0].unitPriceInAssetProfileCurrency, - marketPrice - ); + let marketPriceMax = Math.max(referenceUnitPrice, marketPrice); let marketPriceMaxDate = - marketPrice > activitiesOfHolding[0].unitPriceInAssetProfileCurrency + marketPrice > referenceUnitPrice ? new Date() - : activitiesOfHolding[0].date; - let marketPriceMin = Math.min( - activitiesOfHolding[0].unitPriceInAssetProfileCurrency, - marketPrice - ); + : (firstActivity?.date ?? new Date()); + let marketPriceMin = Math.min(referenceUnitPrice, marketPrice); const historicalDataItems = historicalData[getAssetProfileIdentifier({ dataSource, symbol })]; @@ -926,10 +939,10 @@ export class PortfolioService { } else { // Add historical entry for buy date, if no historical data available historicalDataArray.push({ - averagePrice: activitiesOfHolding[0].unitPriceInAssetProfileCurrency, + averagePrice: referenceUnitPrice, date: dateOfFirstActivity, - marketPrice: activitiesOfHolding[0].unitPriceInAssetProfileCurrency, - quantity: activitiesOfHolding[0].quantity + marketPrice: referenceUnitPrice, + quantity: firstActivity?.quantity ?? quantity.toNumber() }); } @@ -947,16 +960,16 @@ export class PortfolioService { marketPriceMin, tags, assetProfile: { - assetClass: SymbolProfile.assetClass, - assetSubClass: SymbolProfile.assetSubClass, - countries: SymbolProfile.countries, - currency: SymbolProfile.currency, - dataSource: SymbolProfile.dataSource, - isin: SymbolProfile.isin, - name: SymbolProfile.name, - sectors: SymbolProfile.sectors, - symbol: SymbolProfile.symbol, - userId: SymbolProfile.userId + assetClass: assetProfile.assetClass, + assetSubClass: assetProfile.assetSubClass, + countries: assetProfile.countries, + currency: assetProfile.currency, + dataSource: assetProfile.dataSource, + isin: assetProfile.isin, + name: assetProfile.name, + sectors: assetProfile.sectors, + symbol: assetProfile.symbol, + userId: assetProfile.userId }, averagePrice: averagePrice.toNumber(), dataProviderInfo: portfolioCalculator.getDataProviderInfos()?.[0], diff --git a/apps/api/src/services/benchmark/benchmark.service.ts b/apps/api/src/services/benchmark/benchmark.service.ts index 99ceaf21ef..affb0da08f 100644 --- a/apps/api/src/services/benchmark/benchmark.service.ts +++ b/apps/api/src/services/benchmark/benchmark.service.ts @@ -23,7 +23,7 @@ import { BenchmarkTrend } from '@ghostfolio/common/types'; import { Injectable, Logger } from '@nestjs/common'; import { SymbolProfile } from '@prisma/client'; import { Big } from 'big.js'; -import { addHours, isAfter, subDays } from 'date-fns'; +import { addHours, isPast, subDays } from 'date-fns'; import { round, uniqBy } from 'lodash'; import ms from 'ms'; @@ -94,7 +94,7 @@ export class BenchmarkService { this.logger.debug('Fetched benchmarks from cache'); - if (isAfter(new Date(), new Date(expiration))) { + if (isPast(new Date(expiration))) { this.calculateAndCacheBenchmarks({ enableSharing }); diff --git a/apps/client/src/app/components/investment-chart/investment-chart.component.ts b/apps/client/src/app/components/investment-chart/investment-chart.component.ts index dc3152f120..3aa65b9983 100644 --- a/apps/client/src/app/components/investment-chart/investment-chart.component.ts +++ b/apps/client/src/app/components/investment-chart/investment-chart.component.ts @@ -41,7 +41,7 @@ import { } from 'chart.js'; import 'chartjs-adapter-date-fns'; import { type AnnotationOptions } from 'chartjs-plugin-annotation'; -import { isAfter } from 'date-fns'; +import { isFuture } from 'date-fns'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; @Component({ @@ -311,6 +311,6 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy { return undefined; } - return isAfter(new Date(xValue), new Date()) ? aValue : undefined; + return isFuture(new Date(xValue)) ? aValue : undefined; } }