Browse Source

Merge branch 'main' into feature/security-headers

pull/8041/head
Thomas Kaul 1 week ago
committed by GitHub
parent
commit
03a528f16e
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 8
      CHANGELOG.md
  2. 44
      apps/api/src/app/activities/activities.service.spec.ts
  3. 14
      apps/api/src/app/activities/activities.service.ts
  4. 16
      apps/api/src/app/portfolio/current-rate.service.spec.ts
  5. 6
      apps/api/src/app/portfolio/current-rate.service.ts
  6. 18
      libs/ui/src/lib/line-chart/line-chart.component.ts
  7. 1759
      package-lock.json
  8. 24
      package.json

8
CHANGELOG.md

@ -11,6 +11,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Exposed the `ENABLE_FEATURE_SECURITY_HEADERS` environment variable to enable HTTP security headers (experimental) - Exposed the `ENABLE_FEATURE_SECURITY_HEADERS` environment variable to enable HTTP security headers (experimental)
### Changed
- Upgraded `Nx` from version `23.1.1` to `23.2.1`
### Fixed
- Fixed an issue where holdings without a market price have been valued at the unit price of a dividend, a fee, an interest or a liability
## 3.79.0 - 2026-10-04 ## 3.79.0 - 2026-10-04
### Added ### Added

44
apps/api/src/app/activities/activities.service.spec.ts

@ -3,6 +3,7 @@ import {
activityDummyData, activityDummyData,
assetProfileDummyData assetProfileDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { WHERE_ACTIVITY_NOT_DRAFT } from '@ghostfolio/api/helper/activity.helper';
import { AssetProfileSplitService } from '@ghostfolio/api/services/asset-profile-split/asset-profile-split.service'; import { AssetProfileSplitService } from '@ghostfolio/api/services/asset-profile-split/asset-profile-split.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
@ -14,7 +15,11 @@ import {
import { parseDate } from '@ghostfolio/common/helper'; import { parseDate } from '@ghostfolio/common/helper';
import { Activity, Filter } from '@ghostfolio/common/interfaces'; import { Activity, Filter } from '@ghostfolio/common/interfaces';
import { AssetProfileSplit, DataSource } from '@prisma/client'; import {
AssetProfileSplit,
DataSource,
Type as ActivityType
} from '@prisma/client';
import { Big } from 'big.js'; import { Big } from 'big.js';
import { ActivitiesService } from './activities.service'; import { ActivitiesService } from './activities.service';
@ -381,6 +386,43 @@ describe('ActivitiesService', () => {
return result.activities[0]; return result.activities[0];
} }
}); });
describe('getLatestActivity', () => {
it('filters by type and unit price and excludes draft activities', async () => {
const findFirst = jest.fn().mockResolvedValue(null);
const service = new ActivitiesService(
null,
null,
null,
null,
null,
null,
null,
null,
null,
{ order: { findFirst } } as unknown as PrismaService,
null,
null
);
await service.getLatestActivity({
dataSource: DataSource.YAHOO,
symbol: 'AAPL',
types: [ActivityType.BUY, ActivityType.SELL]
});
expect(findFirst).toHaveBeenCalledWith({
orderBy: [{ date: 'desc' }, { createdAt: 'desc' }],
where: {
...WHERE_ACTIVITY_NOT_DRAFT,
SymbolProfile: { dataSource: DataSource.YAHOO, symbol: 'AAPL' },
type: { in: [ActivityType.BUY, ActivityType.SELL] },
unitPrice: { gt: 0 }
}
});
});
});
}); });
function createActivity({ function createActivity({

14
apps/api/src/app/activities/activities.service.ts

@ -581,14 +581,16 @@ export class ActivitiesService {
public async getLatestActivity({ public async getLatestActivity({
dataSource, dataSource,
symbol symbol,
}: AssetProfileIdentifier) { types
}: AssetProfileIdentifier & { types: ActivityType[] }) {
return this.prismaService.order.findFirst({ return this.prismaService.order.findFirst({
orderBy: { orderBy: [{ date: 'desc' }, { createdAt: 'desc' }],
date: 'desc'
},
where: { where: {
SymbolProfile: { dataSource, symbol } ...WHERE_ACTIVITY_NOT_DRAFT,
SymbolProfile: { dataSource, symbol },
type: { in: types },
unitPrice: { gt: 0 }
} }
}); });
} }

16
apps/api/src/app/portfolio/current-rate.service.spec.ts

@ -5,7 +5,12 @@ import { PropertyService } from '@ghostfolio/api/services/property/property.serv
import { resetHours } from '@ghostfolio/common/helper'; import { resetHours } from '@ghostfolio/common/helper';
import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces'; import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
import { DataSource, MarketData, Order } from '@prisma/client'; import {
DataSource,
MarketData,
Order,
Type as ActivityType
} from '@prisma/client';
import { addDays, subDays } from 'date-fns'; import { addDays, subDays } from 'date-fns';
import { CurrentRateService } from './current-rate.service'; import { CurrentRateService } from './current-rate.service';
@ -220,8 +225,8 @@ describe('CurrentRateService', () => {
]); ]);
}); });
it('should fall back to the unit price of the latest activity without market data', async () => { it('should fall back to the unit price of the latest buy or sell activity without market data', async () => {
jest const getLatestActivity = jest
.spyOn(activitiesService, 'getLatestActivity') .spyOn(activitiesService, 'getLatestActivity')
.mockResolvedValue({ unitPrice: 1000 } as Order); .mockResolvedValue({ unitPrice: 1000 } as Order);
@ -232,6 +237,11 @@ describe('CurrentRateService', () => {
dateQuery dateQuery
}); });
expect(getLatestActivity).toHaveBeenCalledWith({
dataSource: DataSource.YAHOO,
symbol: 'AMZN',
types: [ActivityType.BUY, ActivityType.SELL]
});
expect(errors).toEqual(dataGatheringItems); expect(errors).toEqual(dataGatheringItems);
expect(values).toEqual([ expect(values).toEqual([
{ {

6
apps/api/src/app/portfolio/current-rate.service.ts

@ -13,6 +13,7 @@ import {
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Type as ActivityType } from '@prisma/client';
import { isBefore, isToday } from 'date-fns'; import { isBefore, isToday } from 'date-fns';
import { isEmpty, uniqBy } from 'lodash-es'; import { isEmpty, uniqBy } from 'lodash-es';
@ -140,11 +141,12 @@ export class CurrentRateService {
let marketPrice = latestMarketData?.marketPrice; let marketPrice = latestMarketData?.marketPrice;
if (!marketPrice) { if (!marketPrice) {
// Fallback to unit price of latest activity // Fallback to unit price of latest buy or sell activity
const latestActivity = const latestActivity =
await this.activitiesService.getLatestActivity({ await this.activitiesService.getLatestActivity({
dataSource, dataSource,
symbol symbol,
types: [ActivityType.BUY, ActivityType.SELL]
}); });
marketPrice = latestActivity?.unitPrice ?? 0; marketPrice = latestActivity?.unitPrice ?? 0;

18
libs/ui/src/lib/line-chart/line-chart.component.ts

@ -6,7 +6,10 @@ import {
} from '@ghostfolio/common/chart-helper'; } from '@ghostfolio/common/chart-helper';
import { primaryColorRgb, secondaryColorRgb } from '@ghostfolio/common/config'; import { primaryColorRgb, secondaryColorRgb } from '@ghostfolio/common/config';
import { getBackgroundColor, getLocale } from '@ghostfolio/common/helper'; import { getBackgroundColor, getLocale } from '@ghostfolio/common/helper';
import { LineChartItem } from '@ghostfolio/common/interfaces'; import {
LineChartItem,
NullableLineChartItem
} from '@ghostfolio/common/interfaces';
import { ColorScheme } from '@ghostfolio/common/types'; import { ColorScheme } from '@ghostfolio/common/types';
import { import {
@ -24,6 +27,7 @@ import {
import { import {
type AnimationsSpec, type AnimationsSpec,
Chart, Chart,
type ChartData,
Filler, Filler,
LinearScale, LinearScale,
LineController, LineController,
@ -52,10 +56,10 @@ import {
export class GfLineChartComponent export class GfLineChartComponent
implements AfterViewInit, OnChanges, OnDestroy implements AfterViewInit, OnChanges, OnDestroy
{ {
@Input() benchmarkDataItems: LineChartItem[] = []; @Input() benchmarkDataItems: NullableLineChartItem[] = [];
@Input() benchmarkLabel = ''; @Input() benchmarkLabel = '';
@Input() colorScheme?: ColorScheme; @Input() colorScheme?: ColorScheme;
@Input() currency: string; @Input() currency?: string;
@Input() historicalDataItems: LineChartItem[]; @Input() historicalDataItems: LineChartItem[];
@Input() isAnimated = false; @Input() isAnimated = false;
@Input() label: string; @Input() label: string;
@ -134,12 +138,12 @@ export class GfLineChartComponent
private initialize() { private initialize() {
this.isLoading = true; this.isLoading = true;
const benchmarkPrices: number[] = []; const benchmarkPrices: (number | null)[] = [];
const labels: string[] = []; const labels: string[] = [];
const marketPrices: number[] = []; const marketPrices: number[] = [];
this.historicalDataItems?.forEach((historicalDataItem, index) => { this.historicalDataItems?.forEach((historicalDataItem, index) => {
benchmarkPrices.push(this.benchmarkDataItems?.[index]?.value); benchmarkPrices.push(this.benchmarkDataItems?.[index]?.value ?? null);
labels.push(historicalDataItem.date); labels.push(historicalDataItem.date);
marketPrices.push(historicalDataItem.value); marketPrices.push(historicalDataItem.value);
}); });
@ -164,7 +168,7 @@ export class GfLineChartComponent
gradient.addColorStop(1, getBackgroundColor(this.colorScheme)); gradient.addColorStop(1, getBackgroundColor(this.colorScheme));
} }
const data = { const data: ChartData<'line'> = {
labels, labels,
datasets: [ datasets: [
{ {
@ -205,7 +209,7 @@ export class GfLineChartComponent
this.chart.update(); this.chart.update();
} else { } else {
this.chart = new Chart(this.chartCanvas.nativeElement, { this.chart = new Chart<'line'>(this.chartCanvas.nativeElement, {
data, data,
options: { options: {
animations: this.isAnimated ? animations : undefined, animations: this.isAnimated ? animations : undefined,

1759
package-lock.json

File diff suppressed because it is too large

24
package.json

@ -169,16 +169,17 @@
"@eslint/js": "9.35.0", "@eslint/js": "9.35.0",
"@nestjs/schematics": "11.1.0", "@nestjs/schematics": "11.1.0",
"@nestjs/testing": "11.2.6", "@nestjs/testing": "11.2.6",
"@nx/angular": "23.1.1", "@nx/angular": "23.2.1",
"@nx/eslint-plugin": "23.1.1", "@nx/eslint": "23.2.1",
"@nx/jest": "23.1.1", "@nx/eslint-plugin": "23.2.1",
"@nx/js": "23.1.1", "@nx/jest": "23.2.1",
"@nx/module-federation": "23.1.1", "@nx/js": "23.2.1",
"@nx/nest": "23.1.1", "@nx/nest": "23.2.1",
"@nx/node": "23.1.1", "@nx/node": "23.2.1",
"@nx/storybook": "23.1.1", "@nx/storybook": "23.2.1",
"@nx/web": "23.1.1", "@nx/web": "23.2.1",
"@nx/workspace": "23.1.1", "@nx/webpack": "23.2.1",
"@nx/workspace": "23.2.1",
"@prisma/config": "7.10.0", "@prisma/config": "7.10.0",
"@schematics/angular": "22.1.4", "@schematics/angular": "22.1.4",
"@storybook/addon-docs": "10.5.7", "@storybook/addon-docs": "10.5.7",
@ -205,7 +206,7 @@
"jest-environment-jsdom": "30.2.0", "jest-environment-jsdom": "30.2.0",
"jest-environment-node": "30.3.0", "jest-environment-node": "30.3.0",
"jest-preset-angular": "17.0.0", "jest-preset-angular": "17.0.0",
"nx": "23.1.1", "nx": "23.2.1",
"prettier": "3.9.9", "prettier": "3.9.9",
"prettier-plugin-organize-attributes": "1.0.0", "prettier-plugin-organize-attributes": "1.0.0",
"prisma": "7.10.0", "prisma": "7.10.0",
@ -219,6 +220,7 @@
"tslib": "2.8.1", "tslib": "2.8.1",
"typescript": "6.0.3", "typescript": "6.0.3",
"typescript-eslint": "8.67.0", "typescript-eslint": "8.67.0",
"webpack": "5.105.2",
"webpack-cli": "7.1.0" "webpack-cli": "7.1.0"
}, },
"engines": { "engines": {

Loading…
Cancel
Save