Browse Source

Merge branch 'main' into task/remove-deprecated-first-order-date-from-portfolio-performance-endpoint-response

pull/7483/head
Thomas Kaul 1 month ago
committed by GitHub
parent
commit
60dadfc0cb
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 5
      CHANGELOG.md
  2. 9
      apps/api/src/app/export/export.service.ts
  3. 86
      apps/api/src/app/portfolio/portfolio.service.spec.ts
  4. 4
      apps/client/src/app/components/user-account-settings/user-account-settings.component.ts
  5. 6
      apps/client/src/app/pages/portfolio/activities/activities-page.component.ts
  6. 21
      libs/ui/src/lib/services/data.service.ts

5
CHANGELOG.md

@ -9,10 +9,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Harmonized the data format of the export functionality
- Removed the deprecated `firstOrderDate` attribute from the `GET api/v2/portfolio/performance` endpoint response - Removed the deprecated `firstOrderDate` attribute from the `GET api/v2/portfolio/performance` endpoint response
- Improved the language localization for German (`de`) - Improved the language localization for German (`de`)
- Upgraded `prisma` from version `7.8.0` to `7.9.1` - Upgraded `prisma` from version `7.8.0` to `7.9.1`
### Fixed
- Fixed the export functionality to only include the accounts of the exported activities if a filter is applied
## 3.38.0 - 2026-07-31 ## 3.38.0 - 2026-07-31
### Added ### Added

9
apps/api/src/app/export/export.service.ts

@ -73,6 +73,13 @@ export class ExportService {
}; };
} }
const isFilteredExport =
activityIds?.length > 0 ||
activityTypes?.length > 0 ||
filters?.length > 0 ||
!!endDate ||
!!startDate;
const accounts = ( const accounts = (
await this.accountService.accounts({ await this.accountService.accounts({
where, where,
@ -87,7 +94,7 @@ export class ExportService {
}) })
) )
.filter(({ id }) => { .filter(({ id }) => {
return activityIds?.length > 0 return isFilteredExport
? activities.some(({ accountId }) => { ? activities.some(({ accountId }) => {
return accountId === id; return accountId === id;
}) })

86
apps/api/src/app/portfolio/portfolio.service.spec.ts

@ -1,6 +1,7 @@
import { AccountService } from '@ghostfolio/api/app/account/account.service'; import { AccountService } from '@ghostfolio/api/app/account/account.service';
import { CashDetails } from '@ghostfolio/api/app/account/interfaces/cash-details.interface'; import { CashDetails } from '@ghostfolio/api/app/account/interfaces/cash-details.interface';
import { ActivitiesService } from '@ghostfolio/api/app/activities/activities.service'; import { ActivitiesService } from '@ghostfolio/api/app/activities/activities.service';
import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator';
import { userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; import { userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
import { UserService } from '@ghostfolio/api/app/user/user.service'; import { UserService } from '@ghostfolio/api/app/user/user.service';
@ -11,7 +12,10 @@ import { ImpersonationService } from '@ghostfolio/api/services/impersonation/imp
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import { UNKNOWN_KEY } from '@ghostfolio/common/config'; import { UNKNOWN_KEY } from '@ghostfolio/common/config';
import { parseDate } from '@ghostfolio/common/helper'; import { parseDate } from '@ghostfolio/common/helper';
import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces'; import {
AssetProfileIdentifier,
PortfolioSummary
} from '@ghostfolio/common/interfaces';
import { Account, DataSource } from '@prisma/client'; import { Account, DataSource } from '@prisma/client';
import { Big } from 'big.js'; import { Big } from 'big.js';
@ -338,6 +342,86 @@ describe('PortfolioService', () => {
}); });
}); });
describe('getSummary', () => {
const getSummary = (args: object) => {
return (
portfolioService as unknown as {
getSummary: (aArgs: object) => Promise<PortfolioSummary>;
}
).getSummary(args);
};
function createPortfolioCalculator() {
return {
getDividendInBaseCurrency: jest.fn().mockResolvedValue(new Big(0)),
getFeesInBaseCurrency: jest.fn().mockResolvedValue(new Big(0)),
getInterestInBaseCurrency: jest.fn().mockResolvedValue(new Big(0)),
getLiabilitiesInBaseCurrency: jest.fn().mockResolvedValue(new Big(0)),
getSnapshot: jest.fn().mockResolvedValue({
currentValueInBaseCurrency: new Big(3000),
totalCashInBaseCurrency: new Big(1000),
totalInvestment: new Big(2000),
totalInvestmentWithCurrencyEffect: new Big(2000)
}),
getStartDate: jest.fn().mockReturnValue(parseDate('2024-01-01'))
} as unknown as PortfolioCalculator;
}
beforeEach(() => {
jest
.spyOn(activitiesService, 'getActivities')
.mockResolvedValue({ activities: [], count: 0 });
jest
.spyOn(impersonationService, 'validateImpersonationId')
.mockResolvedValue(null);
jest.spyOn(portfolioService, 'getPerformance').mockResolvedValue({
performance: {
currentValueInBaseCurrency: 3000,
netPerformance: 500,
netPerformancePercentage: 0.2,
netPerformancePercentageWithCurrencyEffect: 0.2,
netPerformanceWithCurrencyEffect: 500
}
} as Awaited<ReturnType<typeof portfolioService.getPerformance>>);
jest.spyOn(userService, 'user').mockResolvedValue({
id: userDummyData.id,
settings: {
settings: {
baseCurrency: 'CHF'
}
}
} as unknown as Awaited<ReturnType<typeof userService.user>>);
});
it('should derive the cash and net worth from the account balance when there are no excluded accounts, no emergency fund and no liabilities', async () => {
jest.spyOn(accountService, 'getCashDetails').mockResolvedValue({
accounts: [],
balanceInBaseCurrency: 1000
});
const portfolioCalculator = createPortfolioCalculator();
const summary = await getSummary({
portfolioCalculator,
balanceInBaseCurrency: 1000,
emergencyFundHoldingsValueInBaseCurrency: 0,
filteredValueInBaseCurrency: new Big(3000),
impersonationId: undefined,
userCurrency: 'CHF',
userId: userDummyData.id
});
expect(summary.cash).toBe(1000);
expect(summary.emergencyFund.total).toBe(0);
expect(summary.excludedAccountsAndActivities).toBe(0);
expect(summary.totalAssetsInBaseCurrency).toBe(3000);
expect(summary.totalValueInBaseCurrency).toBe(3000);
});
});
describe('getValueOfAccountsAndPlatforms', () => { describe('getValueOfAccountsAndPlatforms', () => {
const getValueOfAccountsAndPlatforms = (args: object) => { const getValueOfAccountsAndPlatforms = (args: object) => {
return ( return (

4
apps/client/src/app/components/user-account-settings/user-account-settings.component.ts

@ -266,10 +266,6 @@ export class GfUserAccountSettingsComponent implements OnInit {
.fetchExport() .fetchExport()
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((data) => { .subscribe((data) => {
for (const activity of data.activities) {
delete (activity as Omit<typeof activity, 'id'> & { id?: string }).id;
}
downloadAsFile({ downloadAsFile({
content: data, content: data,
fileName: `ghostfolio-export-${format( fileName: `ghostfolio-export-${format(

6
apps/client/src/app/pages/portfolio/activities/activities-page.component.ts

@ -177,10 +177,6 @@ export class GfActivitiesPageComponent implements OnInit {
.fetchExport(fetchExportParams) .fetchExport(fetchExportParams)
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((data) => { .subscribe((data) => {
for (const activity of data.activities) {
delete (activity as Omit<typeof activity, 'id'> & { id?: string }).id;
}
downloadAsFile({ downloadAsFile({
content: data, content: data,
fileName: `ghostfolio-export-${format( fileName: `ghostfolio-export-${format(
@ -194,7 +190,7 @@ export class GfActivitiesPageComponent implements OnInit {
protected onExportDrafts(activityIds?: string[]) { protected onExportDrafts(activityIds?: string[]) {
this.dataService this.dataService
.fetchExport({ activityIds }) .fetchExport({ activityIds, withActivityIds: true })
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((data) => { .subscribe((data) => {
downloadAsFile({ downloadAsFile({

21
libs/ui/src/lib/services/data.service.ts

@ -476,12 +476,14 @@ export class DataService {
activityIds, activityIds,
activityTypes, activityTypes,
filters, filters,
range range,
withActivityIds = false
}: { }: {
activityIds?: string[]; activityIds?: string[];
activityTypes?: string[]; activityTypes?: string[];
filters?: Filter[]; filters?: Filter[];
range?: DateRange; range?: DateRange;
withActivityIds?: boolean;
} = {}) { } = {}) {
let params = this.buildFiltersAsQueryParams({ filters }); let params = this.buildFiltersAsQueryParams({ filters });
@ -497,9 +499,22 @@ export class DataService {
params = params.append('range', range); params = params.append('range', range);
} }
return this.http.get<ExportResponse>('/api/v1/export', { return this.http
.get<ExportResponse>('/api/v1/export', {
params params
}); })
.pipe(
map((exportResponse) => {
if (!withActivityIds) {
for (const activity of exportResponse.activities) {
delete (activity as Omit<typeof activity, 'id'> & { id?: string })
.id;
}
}
return exportResponse;
})
);
} }
public fetchHoldingDetail({ public fetchHoldingDetail({

Loading…
Cancel
Save