diff --git a/CHANGELOG.md b/CHANGELOG.md index c627e82418..8c47efb2e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,12 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Harmonized the data format of the export functionality - Improved the language localization for German (`de`) - Upgraded `prisma` from version `7.8.0` to `7.9.1` ### Fixed - Fixed the scroll behavior of the page content behind an open dialog +- Fixed the export functionality to only include the accounts of the exported activities if a filter is applied ## 3.38.0 - 2026-07-31 diff --git a/apps/api/src/app/export/export.service.ts b/apps/api/src/app/export/export.service.ts index 6e920553b7..ec9b89a819 100644 --- a/apps/api/src/app/export/export.service.ts +++ b/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 = ( await this.accountService.accounts({ where, @@ -87,7 +94,7 @@ export class ExportService { }) ) .filter(({ id }) => { - return activityIds?.length > 0 + return isFilteredExport ? activities.some(({ accountId }) => { return accountId === id; }) diff --git a/apps/api/src/app/portfolio/portfolio.service.spec.ts b/apps/api/src/app/portfolio/portfolio.service.spec.ts index d85258f7ef..699e79e0d8 100644 --- a/apps/api/src/app/portfolio/portfolio.service.spec.ts +++ b/apps/api/src/app/portfolio/portfolio.service.spec.ts @@ -1,6 +1,7 @@ import { AccountService } from '@ghostfolio/api/app/account/account.service'; import { CashDetails } from '@ghostfolio/api/app/account/interfaces/cash-details.interface'; 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 { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; 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 { UNKNOWN_KEY } from '@ghostfolio/common/config'; 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 { 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; + } + ).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>); + + jest.spyOn(userService, 'user').mockResolvedValue({ + id: userDummyData.id, + settings: { + settings: { + baseCurrency: 'CHF' + } + } + } as unknown as Awaited>); + }); + + 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', () => { const getValueOfAccountsAndPlatforms = (args: object) => { return ( diff --git a/apps/client/src/app/components/user-account-settings/user-account-settings.component.ts b/apps/client/src/app/components/user-account-settings/user-account-settings.component.ts index a556a68d60..0a049a7aed 100644 --- a/apps/client/src/app/components/user-account-settings/user-account-settings.component.ts +++ b/apps/client/src/app/components/user-account-settings/user-account-settings.component.ts @@ -266,10 +266,6 @@ export class GfUserAccountSettingsComponent implements OnInit { .fetchExport() .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((data) => { - for (const activity of data.activities) { - delete (activity as Omit & { id?: string }).id; - } - downloadAsFile({ content: data, fileName: `ghostfolio-export-${format( diff --git a/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts b/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts index 87da2fb844..0ff4168ac5 100644 --- a/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts +++ b/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts @@ -177,10 +177,6 @@ export class GfActivitiesPageComponent implements OnInit { .fetchExport(fetchExportParams) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((data) => { - for (const activity of data.activities) { - delete (activity as Omit & { id?: string }).id; - } - downloadAsFile({ content: data, fileName: `ghostfolio-export-${format( @@ -194,7 +190,7 @@ export class GfActivitiesPageComponent implements OnInit { protected onExportDrafts(activityIds?: string[]) { this.dataService - .fetchExport({ activityIds }) + .fetchExport({ activityIds, withActivityIds: true }) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((data) => { downloadAsFile({ diff --git a/libs/ui/src/lib/services/data.service.ts b/libs/ui/src/lib/services/data.service.ts index 870719702a..bc1512550b 100644 --- a/libs/ui/src/lib/services/data.service.ts +++ b/libs/ui/src/lib/services/data.service.ts @@ -476,12 +476,14 @@ export class DataService { activityIds, activityTypes, filters, - range + range, + withActivityIds = false }: { activityIds?: string[]; activityTypes?: string[]; filters?: Filter[]; range?: DateRange; + withActivityIds?: boolean; } = {}) { let params = this.buildFiltersAsQueryParams({ filters }); @@ -497,9 +499,22 @@ export class DataService { params = params.append('range', range); } - return this.http.get('/api/v1/export', { - params - }); + return this.http + .get('/api/v1/export', { + params + }) + .pipe( + map((exportResponse) => { + if (!withActivityIds) { + for (const activity of exportResponse.activities) { + delete (activity as Omit & { id?: string }) + .id; + } + } + + return exportResponse; + }) + ); } public fetchHoldingDetail({