Browse Source

Merge branch 'main' into bug/csv_import_of_custom_asset_profile_fails

pull/5749/head
Thomas Kaul 2 weeks ago
committed by GitHub
parent
commit
5203f2c5d5
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 8
      CHANGELOG.md
  2. 6
      apps/api/src/app/portfolio/calculator/portfolio-calculator-test-utils.ts
  3. 44
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur.spec.ts
  4. 44
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-short.spec.ts
  5. 44
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd.spec.ts
  6. 44
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell-partially.spec.ts
  7. 44
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell.spec.ts
  8. 6
      apps/api/src/app/portfolio/portfolio.controller.ts
  9. 6
      apps/api/src/app/portfolio/portfolio.service.ts
  10. 4
      apps/api/src/helper/object.helper.spec.ts
  11. 7
      apps/client/src/app/app-routing.module.ts
  12. 2
      apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html
  13. 13
      apps/client/src/app/pages/blog/blog-page.component.ts
  14. 14
      apps/client/src/app/pages/blog/blog-page.module.ts
  15. 15
      apps/client/src/app/pages/blog/blog-page.routes.ts
  16. 10
      apps/client/src/app/pages/pricing/pricing-page.component.ts
  17. 43
      apps/client/src/app/pages/pricing/pricing-page.html
  18. 17
      apps/client/src/app/pages/resources/glossary/resources-glossary.component.html
  19. 11
      apps/client/src/app/services/data.service.ts
  20. 4
      libs/common/src/lib/interfaces/index.ts
  21. 5
      libs/common/src/lib/interfaces/portfolio-dividends.interface.ts
  22. 2
      libs/common/src/lib/interfaces/portfolio-summary.interface.ts
  23. 5
      libs/common/src/lib/interfaces/responses/portfolio-dividends-response.interface.ts
  24. 12
      libs/ui/src/lib/activities-table/activities-table.component.html
  25. 20
      libs/ui/src/lib/logo-carousel/logo-carousel.component.scss
  26. 7
      test/import/ok/novn-buy-and-sell-partially.json
  27. 7
      test/import/ok/novn-buy-and-sell.json

8
CHANGELOG.md

@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Extended the glossary of the resources page by _Stealth Wealth_
- Extended the content of the pricing page
- Added a _Storybook_ story for the holdings table component
### Changed
@ -18,11 +20,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Improved the currency validation in the search functionality of the data provider service
- Optimized the get quotes functionality by utilizing the asset profile resolutions in the _Financial Modeling Prep_ service
- Extracted the footer to a component
- Refactored the blog page component to standalone
- Improved the portfolio calculator unit tests to load the user currency from the exported file
### Fixed
- Fixed an issue in the `csv` file import where custom asset profiles failed due to validation errors
- Fixed an issue with the total buy and sell calculation in the summary related to activities in a custom currency
- Respected the include indices flag in the search functionality of the _Financial Modeling Prep_ service
- Fixed an issue where the scroll position was not restored when changing pages
- Fixed the word wrap in the menus of the activities table component
- Fixed the dark mode in the _As seen in_ section on the landing page
## 2.208.0 - 2025-10-11

6
apps/api/src/app/portfolio/calculator/portfolio-calculator-test-utils.ts

@ -1,3 +1,5 @@
import { Export } from '@ghostfolio/common/interfaces';
import { readFileSync } from 'node:fs';
export const activityDummyData = {
@ -37,6 +39,6 @@ export const userDummyData = {
id: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
};
export function loadActivityExportFile(filePath: string) {
return JSON.parse(readFileSync(filePath, 'utf8')).activities;
export function loadExportFile(filePath: string): Export {
return JSON.parse(readFileSync(filePath, 'utf8'));
}

44
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur.spec.ts

@ -1,8 +1,7 @@
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import {
activityDummyData,
loadActivityExportFile,
loadExportFile,
symbolProfileDummyData,
userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
@ -16,9 +15,9 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-
import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service';
import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock';
import { parseDate } from '@ghostfolio/common/helper';
import { Export } from '@ghostfolio/common/interfaces';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
import { Tag } from '@prisma/client';
import { Big } from 'big.js';
import { join } from 'node:path';
@ -53,7 +52,7 @@ jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
});
describe('PortfolioCalculator', () => {
let activityDtos: CreateOrderDto[];
let exportResponse: Export;
let configurationService: ConfigurationService;
let currentRateService: CurrentRateService;
@ -63,7 +62,7 @@ describe('PortfolioCalculator', () => {
let redisCacheService: RedisCacheService;
beforeAll(() => {
activityDtos = loadActivityExportFile(
exportResponse = loadExportFile(
join(__dirname, '../../../../../../../test/import/ok/btceur.json')
);
});
@ -97,28 +96,27 @@ describe('PortfolioCalculator', () => {
it.only('with BTCUSD buy (in EUR)', async () => {
jest.useFakeTimers().setSystemTime(parseDate('2022-01-14').getTime());
const activities: Activity[] = activityDtos.map((activity) => ({
...activityDummyData,
...activity,
date: parseDate(activity.date),
feeInAssetProfileCurrency: 4.46,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'USD',
dataSource: activity.dataSource,
name: 'Bitcoin',
symbol: activity.symbol
},
tags: activity.tags?.map((id) => {
return { id } as Tag;
}),
unitPriceInAssetProfileCurrency: 44558.42
}));
const activities: Activity[] = exportResponse.activities.map(
(activity) => ({
...activityDummyData,
...activity,
date: parseDate(activity.date),
feeInAssetProfileCurrency: 4.46,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'USD',
dataSource: activity.dataSource,
name: 'Bitcoin',
symbol: activity.symbol
},
unitPriceInAssetProfileCurrency: 44558.42
})
);
const portfolioCalculator = portfolioCalculatorFactory.createCalculator({
activities,
calculationType: PerformanceCalculationType.ROAI,
currency: 'USD',
currency: exportResponse.user.settings.currency,
userId: userDummyData.id
});

44
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-short.spec.ts

@ -1,8 +1,7 @@
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import {
activityDummyData,
loadActivityExportFile,
loadExportFile,
symbolProfileDummyData,
userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
@ -16,9 +15,9 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-
import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service';
import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock';
import { parseDate } from '@ghostfolio/common/helper';
import { Export } from '@ghostfolio/common/interfaces';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
import { Tag } from '@prisma/client';
import { Big } from 'big.js';
import { join } from 'node:path';
@ -53,7 +52,7 @@ jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
});
describe('PortfolioCalculator', () => {
let activityDtos: CreateOrderDto[];
let exportResponse: Export;
let configurationService: ConfigurationService;
let currentRateService: CurrentRateService;
@ -63,7 +62,7 @@ describe('PortfolioCalculator', () => {
let redisCacheService: RedisCacheService;
beforeAll(() => {
activityDtos = loadActivityExportFile(
exportResponse = loadExportFile(
join(__dirname, '../../../../../../../test/import/ok/btcusd-short.json')
);
});
@ -97,28 +96,27 @@ describe('PortfolioCalculator', () => {
it.only('with BTCUSD short sell (in USD)', async () => {
jest.useFakeTimers().setSystemTime(parseDate('2022-01-14').getTime());
const activities: Activity[] = activityDtos.map((activity) => ({
...activityDummyData,
...activity,
date: parseDate(activity.date),
feeInAssetProfileCurrency: activity.fee,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'USD',
dataSource: activity.dataSource,
name: 'Bitcoin',
symbol: activity.symbol
},
tags: activity.tags?.map((id) => {
return { id } as Tag;
}),
unitPriceInAssetProfileCurrency: activity.unitPrice
}));
const activities: Activity[] = exportResponse.activities.map(
(activity) => ({
...activityDummyData,
...activity,
date: parseDate(activity.date),
feeInAssetProfileCurrency: activity.fee,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'USD',
dataSource: activity.dataSource,
name: 'Bitcoin',
symbol: activity.symbol
},
unitPriceInAssetProfileCurrency: activity.unitPrice
})
);
const portfolioCalculator = portfolioCalculatorFactory.createCalculator({
activities,
calculationType: PerformanceCalculationType.ROAI,
currency: 'USD',
currency: exportResponse.user.settings.currency,
userId: userDummyData.id
});

44
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd.spec.ts

@ -1,8 +1,7 @@
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import {
activityDummyData,
loadActivityExportFile,
loadExportFile,
symbolProfileDummyData,
userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
@ -16,9 +15,9 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-
import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service';
import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock';
import { parseDate } from '@ghostfolio/common/helper';
import { Export } from '@ghostfolio/common/interfaces';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
import { Tag } from '@prisma/client';
import { Big } from 'big.js';
import { join } from 'node:path';
@ -53,7 +52,7 @@ jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
});
describe('PortfolioCalculator', () => {
let activityDtos: CreateOrderDto[];
let exportResponse: Export;
let configurationService: ConfigurationService;
let currentRateService: CurrentRateService;
@ -63,7 +62,7 @@ describe('PortfolioCalculator', () => {
let redisCacheService: RedisCacheService;
beforeAll(() => {
activityDtos = loadActivityExportFile(
exportResponse = loadExportFile(
join(__dirname, '../../../../../../../test/import/ok/btcusd.json')
);
});
@ -97,28 +96,27 @@ describe('PortfolioCalculator', () => {
it.only('with BTCUSD buy (in USD)', async () => {
jest.useFakeTimers().setSystemTime(parseDate('2022-01-14').getTime());
const activities: Activity[] = activityDtos.map((activity) => ({
...activityDummyData,
...activity,
date: parseDate(activity.date),
feeInAssetProfileCurrency: 4.46,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'USD',
dataSource: activity.dataSource,
name: 'Bitcoin',
symbol: activity.symbol
},
tags: activity.tags?.map((id) => {
return { id } as Tag;
}),
unitPriceInAssetProfileCurrency: 44558.42
}));
const activities: Activity[] = exportResponse.activities.map(
(activity) => ({
...activityDummyData,
...activity,
date: parseDate(activity.date),
feeInAssetProfileCurrency: 4.46,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'USD',
dataSource: activity.dataSource,
name: 'Bitcoin',
symbol: activity.symbol
},
unitPriceInAssetProfileCurrency: 44558.42
})
);
const portfolioCalculator = portfolioCalculatorFactory.createCalculator({
activities,
calculationType: PerformanceCalculationType.ROAI,
currency: 'USD',
currency: exportResponse.user.settings.currency,
userId: userDummyData.id
});

44
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell-partially.spec.ts

@ -1,8 +1,7 @@
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import {
activityDummyData,
loadActivityExportFile,
loadExportFile,
symbolProfileDummyData,
userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
@ -16,9 +15,9 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-
import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service';
import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock';
import { parseDate } from '@ghostfolio/common/helper';
import { Export } from '@ghostfolio/common/interfaces';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
import { Tag } from '@prisma/client';
import { Big } from 'big.js';
import { join } from 'node:path';
@ -53,7 +52,7 @@ jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
});
describe('PortfolioCalculator', () => {
let activityDtos: CreateOrderDto[];
let exportResponse: Export;
let configurationService: ConfigurationService;
let currentRateService: CurrentRateService;
@ -63,7 +62,7 @@ describe('PortfolioCalculator', () => {
let redisCacheService: RedisCacheService;
beforeAll(() => {
activityDtos = loadActivityExportFile(
exportResponse = loadExportFile(
join(
__dirname,
'../../../../../../../test/import/ok/novn-buy-and-sell-partially.json'
@ -100,28 +99,27 @@ describe('PortfolioCalculator', () => {
it.only('with NOVN.SW buy and sell partially', async () => {
jest.useFakeTimers().setSystemTime(parseDate('2022-04-11').getTime());
const activities: Activity[] = activityDtos.map((activity) => ({
...activityDummyData,
...activity,
date: parseDate(activity.date),
feeInAssetProfileCurrency: activity.fee,
SymbolProfile: {
...symbolProfileDummyData,
currency: activity.currency,
dataSource: activity.dataSource,
name: 'Novartis AG',
symbol: activity.symbol
},
tags: activity.tags?.map((id) => {
return { id } as Tag;
}),
unitPriceInAssetProfileCurrency: activity.unitPrice
}));
const activities: Activity[] = exportResponse.activities.map(
(activity) => ({
...activityDummyData,
...activity,
date: parseDate(activity.date),
feeInAssetProfileCurrency: activity.fee,
SymbolProfile: {
...symbolProfileDummyData,
currency: activity.currency,
dataSource: activity.dataSource,
name: 'Novartis AG',
symbol: activity.symbol
},
unitPriceInAssetProfileCurrency: activity.unitPrice
})
);
const portfolioCalculator = portfolioCalculatorFactory.createCalculator({
activities,
calculationType: PerformanceCalculationType.ROAI,
currency: 'CHF',
currency: exportResponse.user.settings.currency,
userId: userDummyData.id
});

44
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell.spec.ts

@ -1,8 +1,7 @@
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import {
activityDummyData,
loadActivityExportFile,
loadExportFile,
symbolProfileDummyData,
userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
@ -16,9 +15,9 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-
import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service';
import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock';
import { parseDate } from '@ghostfolio/common/helper';
import { Export } from '@ghostfolio/common/interfaces';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
import { Tag } from '@prisma/client';
import { Big } from 'big.js';
import { join } from 'node:path';
@ -53,7 +52,7 @@ jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
});
describe('PortfolioCalculator', () => {
let activityDtos: CreateOrderDto[];
let exportResponse: Export;
let configurationService: ConfigurationService;
let currentRateService: CurrentRateService;
@ -63,7 +62,7 @@ describe('PortfolioCalculator', () => {
let redisCacheService: RedisCacheService;
beforeAll(() => {
activityDtos = loadActivityExportFile(
exportResponse = loadExportFile(
join(
__dirname,
'../../../../../../../test/import/ok/novn-buy-and-sell.json'
@ -100,28 +99,27 @@ describe('PortfolioCalculator', () => {
it.only('with NOVN.SW buy and sell', async () => {
jest.useFakeTimers().setSystemTime(parseDate('2022-04-11').getTime());
const activities: Activity[] = activityDtos.map((activity) => ({
...activityDummyData,
...activity,
date: parseDate(activity.date),
feeInAssetProfileCurrency: activity.fee,
SymbolProfile: {
...symbolProfileDummyData,
currency: activity.currency,
dataSource: activity.dataSource,
name: 'Novartis AG',
symbol: activity.symbol
},
tags: activity.tags?.map((id) => {
return { id } as Tag;
}),
unitPriceInAssetProfileCurrency: activity.unitPrice
}));
const activities: Activity[] = exportResponse.activities.map(
(activity) => ({
...activityDummyData,
...activity,
date: parseDate(activity.date),
feeInAssetProfileCurrency: activity.fee,
SymbolProfile: {
...symbolProfileDummyData,
currency: activity.currency,
dataSource: activity.dataSource,
name: 'Novartis AG',
symbol: activity.symbol
},
unitPriceInAssetProfileCurrency: activity.unitPrice
})
);
const portfolioCalculator = portfolioCalculatorFactory.createCalculator({
activities,
calculationType: PerformanceCalculationType.ROAI,
currency: 'CHF',
currency: exportResponse.user.settings.currency,
userId: userDummyData.id
});

6
apps/api/src/app/portfolio/portfolio.controller.ts

@ -19,7 +19,7 @@ import {
} from '@ghostfolio/common/config';
import {
PortfolioDetails,
PortfolioDividends,
PortfolioDividendsResponse,
PortfolioHoldingResponse,
PortfolioHoldingsResponse,
PortfolioInvestments,
@ -197,7 +197,7 @@ export class PortfolioController {
'filteredValueInBaseCurrency',
'grossPerformance',
'grossPerformanceWithCurrencyEffect',
'interest',
'interestInBaseCurrency',
'items',
'liabilities',
'netPerformance',
@ -305,7 +305,7 @@ export class PortfolioController {
@Query('range') dateRange: DateRange = 'max',
@Query('symbol') filterBySymbol?: string,
@Query('tags') filterByTags?: string
): Promise<PortfolioDividends> {
): Promise<PortfolioDividendsResponse> {
const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts,
filterByAssetClasses,

6
apps/api/src/app/portfolio/portfolio.service.ts

@ -2105,7 +2105,7 @@ export class PortfolioService {
)
.plus(fees)
.toNumber(),
interest: interest.toNumber(),
interestInBaseCurrency: interest.toNumber(),
liabilitiesInBaseCurrency: liabilities.toNumber(),
totalInvestment: totalInvestment.toNumber(),
totalValueInBaseCurrency: netWorth
@ -2126,11 +2126,11 @@ export class PortfolioService {
.filter(({ isDraft, type }) => {
return isDraft === false && type === activityType;
})
.map(({ quantity, SymbolProfile, unitPrice }) => {
.map(({ currency, quantity, SymbolProfile, unitPrice }) => {
return new Big(
this.exchangeRateDataService.toCurrency(
new Big(quantity).mul(unitPrice).toNumber(),
SymbolProfile.currency,
currency ?? SymbolProfile.currency,
userCurrency
)
);

4
apps/api/src/helper/object.helper.spec.ts

@ -1536,7 +1536,7 @@ describe('redactAttributes', () => {
fireWealth: null,
grossPerformance: null,
grossPerformanceWithCurrencyEffect: null,
interest: null,
interestInBaseCurrency: null,
items: null,
liabilities: null,
totalInvestment: null,
@ -3039,7 +3039,7 @@ describe('redactAttributes', () => {
fireWealth: null,
grossPerformance: null,
grossPerformanceWithCurrencyEffect: null,
interest: null,
interestInBaseCurrency: null,
items: null,
liabilities: null,
totalInvestment: null,

7
apps/client/src/app/app-routing.module.ts

@ -48,7 +48,7 @@ const routes: Routes = [
{
path: publicRoutes.blog.path,
loadChildren: () =>
import('./pages/blog/blog-page.module').then((m) => m.BlogPageModule)
import('./pages/blog/blog-page.routes').then((m) => m.routes)
},
{
canActivate: [AuthGuard],
@ -155,8 +155,9 @@ const routes: Routes = [
// Preload all lazy loaded modules with the attribute preload === true
{
anchorScrolling: 'enabled',
preloadingStrategy: ModulePreloadService
// enableTracing: true // <-- debugging purposes only
// enableTracing: true, // <-- debugging purposes only
preloadingStrategy: ModulePreloadService,
scrollPositionRestoration: 'top'
}
)
],

2
apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html

@ -302,7 +302,7 @@
[isCurrency]="true"
[locale]="locale"
[unit]="baseCurrency"
[value]="isLoading ? undefined : summary?.interest"
[value]="isLoading ? undefined : summary?.interestInBaseCurrency"
/>
</div>
</div>

13
apps/client/src/app/pages/blog/blog-page.component.ts

@ -1,19 +1,24 @@
import { DataService } from '@ghostfolio/client/services/data.service';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { Component, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Component, CUSTOM_ELEMENTS_SCHEMA, OnDestroy } from '@angular/core';
import { MatCardModule } from '@angular/material/card';
import { RouterModule } from '@angular/router';
import { IonIcon } from '@ionic/angular/standalone';
import { addIcons } from 'ionicons';
import { chevronForwardOutline } from 'ionicons/icons';
import { Subject } from 'rxjs';
@Component({
host: { class: 'page' },
imports: [CommonModule, IonIcon, MatCardModule, RouterModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
selector: 'gf-blog-page',
styleUrls: ['./blog-page.scss'],
templateUrl: './blog-page.html',
standalone: false
templateUrl: './blog-page.html'
})
export class BlogPageComponent implements OnDestroy {
export class GfBlogPageComponent implements OnDestroy {
public hasPermissionForSubscription: boolean;
private unsubscribeSubject = new Subject<void>();

14
apps/client/src/app/pages/blog/blog-page.module.ts

@ -1,14 +0,0 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatCardModule } from '@angular/material/card';
import { IonIcon } from '@ionic/angular/standalone';
import { BlogPageRoutingModule } from './blog-page-routing.module';
import { BlogPageComponent } from './blog-page.component';
@NgModule({
declarations: [BlogPageComponent],
imports: [BlogPageRoutingModule, CommonModule, IonIcon, MatCardModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class BlogPageModule {}

15
apps/client/src/app/pages/blog/blog-page-routing.module.ts → apps/client/src/app/pages/blog/blog-page.routes.ts

@ -1,15 +1,14 @@
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
import { publicRoutes } from '@ghostfolio/common/routes/routes';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { Routes } from '@angular/router';
import { BlogPageComponent } from './blog-page.component';
import { GfBlogPageComponent } from './blog-page.component';
const routes: Routes = [
export const routes: Routes = [
{
canActivate: [AuthGuard],
component: BlogPageComponent,
component: GfBlogPageComponent,
path: '',
title: publicRoutes.blog.title
},
@ -212,9 +211,3 @@ const routes: Routes = [
title: 'Hacktoberfest 2025'
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class BlogPageRoutingModule {}

10
apps/client/src/app/pages/pricing/pricing-page.component.ts

@ -69,6 +69,16 @@ export class GfPricingPageComponent implements OnDestroy, OnInit {
public professionalDataProviderTooltipPremium = translate(
'PROFESSIONAL_DATA_PROVIDER_TOOLTIP_PREMIUM'
);
public referralBrokers = [
'DEGIRO',
'finpension',
'frankly',
'Interactive Brokers',
'Mintos',
'Swissquote',
'VIAC',
'Zak'
];
public routerLinkFeatures = publicRoutes.features.routerLink;
public routerLinkRegister = publicRoutes.register.routerLink;
public user: User;

43
apps/client/src/app/pages/pricing/pricing-page.html

@ -326,16 +326,43 @@
<div class="row">
<div class="col mt-3">
<p>
If you plan to open an account at <i>DEGIRO</i>, <i>finpension</i>,
<i>frankly</i>, <i>Interactive Brokers</i>, <i>Swissquote</i>,
<i>VIAC</i>, or <i>Zak</i>, please
<a href="mailto:hi@ghostfol.io?Subject=Referral link for..."
<ng-container i18n>If you plan to open an account at</ng-container>
<ng-container>&nbsp;</ng-container>
@for (
broker of referralBrokers;
track broker;
let i = $index;
let last = $last
) {
<i>{{ broker }}</i>
@if (last) {
<span>, </span>
} @else {
@if (i === referralBrokers.length - 2) {
<ng-container>&nbsp;</ng-container>
<ng-container i18n>or</ng-container>
<ng-container>&nbsp;</ng-container>
} @else {
<span>, </span>
}
}
}
<ng-container i18n>please</ng-container>
<ng-container>&nbsp;</ng-container>
<a href="mailto:hi@ghostfol.io?Subject=Referral link for..." i18n
>contact us</a
>
to use our referral link and get a Ghostfolio Premium membership for
one year. Looking for a student discount? Request it
<a href="mailto:hi@ghostfol.io?Subject=Student Discount">here</a>
with your university e-mail address.
<ng-container>&nbsp;</ng-container>
<ng-container i18n
>to use our referral link and get a Ghostfolio Premium membership
for one year</ng-container
>. <ng-container i18n>Looking for a student discount?</ng-container>
<ng-container>&nbsp;</ng-container>
<ng-container i18n>Request it</ng-container>
<ng-container>&nbsp;</ng-container>
<a href="mailto:hi@ghostfol.io?Subject=Student Discount" i18n>here</a>
<ng-container>&nbsp;</ng-container>
<ng-container i18n>with your university e-mail address</ng-container>.
</p>
</div>
</div>

17
apps/client/src/app/pages/resources/glossary/resources-glossary.component.html

@ -132,6 +132,23 @@
</div>
</div>
</div>
<div class="mb-4 media">
<div class="media-body">
<h3 class="h5 mt-0">Stealth Wealth</h3>
<div class="mb-1">
Stealth wealth is a lifestyle choice where you don’t openly show
off your wealth, but instead live quietly to maintain privacy and
security.
</div>
<div>
<a
href="https://en.wikipedia.org/wiki/Stealth_wealth"
target="_blank"
>Stealth Wealth →</a
>
</div>
</div>
</div>
</div>
</div>
</div>

11
apps/client/src/app/services/data.service.ts

@ -42,7 +42,7 @@ import {
MarketDataOfMarketsResponse,
OAuthResponse,
PortfolioDetails,
PortfolioDividends,
PortfolioDividendsResponse,
PortfolioHoldingResponse,
PortfolioHoldingsResponse,
PortfolioInvestments,
@ -270,9 +270,12 @@ export class DataService {
params = params.append('groupBy', groupBy);
params = params.append('range', range);
return this.http.get<PortfolioDividends>('/api/v1/portfolio/dividends', {
params
});
return this.http.get<PortfolioDividendsResponse>(
'/api/v1/portfolio/dividends',
{
params
}
);
}
public fetchDividendsImport({ dataSource, symbol }: AssetProfileIdentifier) {

4
libs/common/src/lib/interfaces/index.ts

@ -30,7 +30,6 @@ import type { LookupItem } from './lookup-item.interface';
import type { MarketData } from './market-data.interface';
import type { PortfolioChart } from './portfolio-chart.interface';
import type { PortfolioDetails } from './portfolio-details.interface';
import type { PortfolioDividends } from './portfolio-dividends.interface';
import type { PortfolioInvestments } from './portfolio-investments.interface';
import type { PortfolioPerformance } from './portfolio-performance.interface';
import type { PortfolioPosition } from './portfolio-position.interface';
@ -56,6 +55,7 @@ import type { LookupResponse } from './responses/lookup-response.interface';
import type { MarketDataDetailsResponse } from './responses/market-data-details-response.interface';
import type { MarketDataOfMarketsResponse } from './responses/market-data-of-markets-response.interface';
import type { OAuthResponse } from './responses/oauth-response.interface';
import type { PortfolioDividendsResponse } from './responses/portfolio-dividends-response.interface';
import { PortfolioHoldingResponse } from './responses/portfolio-holding-response.interface';
import type { PortfolioHoldingsResponse } from './responses/portfolio-holdings-response.interface';
import type { PortfolioPerformanceResponse } from './responses/portfolio-performance-response.interface';
@ -122,7 +122,7 @@ export {
OAuthResponse,
PortfolioChart,
PortfolioDetails,
PortfolioDividends,
PortfolioDividendsResponse,
PortfolioHoldingResponse,
PortfolioHoldingsResponse,
PortfolioInvestments,

5
libs/common/src/lib/interfaces/portfolio-dividends.interface.ts

@ -1,5 +0,0 @@
import { InvestmentItem } from './investment-item.interface';
export interface PortfolioDividends {
dividends: InvestmentItem[];
}

2
libs/common/src/lib/interfaces/portfolio-summary.interface.ts

@ -20,7 +20,7 @@ export interface PortfolioSummary extends PortfolioPerformance {
fireWealth: FireWealth;
grossPerformance: number;
grossPerformanceWithCurrencyEffect: number;
interest: number;
interestInBaseCurrency: number;
liabilitiesInBaseCurrency: number;
totalBuy: number;
totalSell: number;

5
libs/common/src/lib/interfaces/responses/portfolio-dividends-response.interface.ts

@ -0,0 +1,5 @@
import { InvestmentItem } from '../investment-item.interface';
export interface PortfolioDividendsResponse {
dividends: InvestmentItem[];
}

12
libs/ui/src/lib/activities-table/activities-table.component.html

@ -361,7 +361,11 @@
<ion-icon name="ellipsis-vertical" />
</button>
}
<mat-menu #activitiesMenu="matMenu" xPosition="before">
<mat-menu
#activitiesMenu="matMenu"
class="no-max-width"
xPosition="before"
>
@if (hasPermissionToCreateActivity) {
<button
class="align-items-center d-flex"
@ -425,7 +429,11 @@
<ion-icon name="ellipsis-horizontal" />
</button>
}
<mat-menu #activityMenu="matMenu" xPosition="before">
<mat-menu
#activityMenu="matMenu"
class="no-max-width"
xPosition="before"
>
<button mat-menu-item (click)="onUpdateActivity(element)">
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="create-outline" />

20
libs/ui/src/lib/logo-carousel/logo-carousel.component.scss

@ -194,19 +194,13 @@
);
}
.logo {
&.logo-alternative-to,
&.logo-dev-community,
&.logo-hacker-news,
&.logo-openalternative,
&.logo-privacy-tools,
&.logo-reddit,
&.logo-sackgeld,
&.logo-selfh-st,
&.logo-sourceforge,
&.logo-umbrel,
&.logo-unraid {
background-color: rgba(var(--light-primary-text));
.logo-carousel-track {
.logo-carousel-item {
.logo {
&.mask {
background-color: rgba(var(--light-secondary-text));
}
}
}
}
}

7
test/import/ok/novn-buy-and-sell-partially.json

@ -24,5 +24,10 @@
"date": "2022-03-07T00:00:00.000Z",
"symbol": "NOVN.SW"
}
]
],
"user": {
"settings": {
"currency": "CHF"
}
}
}

7
test/import/ok/novn-buy-and-sell.json

@ -24,5 +24,10 @@
"date": "2022-03-07T00:00:00.000Z",
"symbol": "NOVN.SW"
}
]
],
"user": {
"settings": {
"currency": "CHF"
}
}
}

Loading…
Cancel
Save