Browse Source

Merge branch 'main' into main

pull/5454/head
Thomas Kaul 2 months ago
committed by GitHub
parent
commit
e9773ac232
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 26
      CHANGELOG.md
  2. 25
      apps/api/src/app/admin/admin.service.ts
  3. 2
      apps/api/src/app/admin/queue/queue.service.ts
  4. 30
      apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
  5. 208
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-buy.spec.ts
  6. 132
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-short.spec.ts
  7. 3
      apps/client/project.json
  8. 4
      apps/client/src/app/app-routing.module.ts
  9. 39
      apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts
  10. 38
      apps/client/src/app/components/account-detail-dialog/account-detail-dialog.module.ts
  11. 27
      apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.component.ts
  12. 6
      apps/client/src/app/components/data-provider-status/data-provider-status.component.ts
  13. 34
      apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts
  14. 25
      apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.module.ts
  15. 6
      apps/client/src/app/components/user-account-access/user-account-access.component.ts
  16. 12
      apps/client/src/app/pages/accounts/accounts-page.component.ts
  17. 4
      apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts
  18. 20
      apps/client/src/app/pages/public/public-page-routing.module.ts
  19. 29
      apps/client/src/app/pages/public/public-page.component.ts
  20. 28
      apps/client/src/app/pages/public/public-page.module.ts
  21. 13
      apps/client/src/app/pages/public/public-page.routes.ts
  22. 588
      apps/client/src/locales/messages.ca.xlf
  23. 640
      apps/client/src/locales/messages.de.xlf
  24. 594
      apps/client/src/locales/messages.es.xlf
  25. 642
      apps/client/src/locales/messages.fr.xlf
  26. 638
      apps/client/src/locales/messages.it.xlf
  27. 564
      apps/client/src/locales/messages.nl.xlf
  28. 604
      apps/client/src/locales/messages.pl.xlf
  29. 644
      apps/client/src/locales/messages.pt.xlf
  30. 596
      apps/client/src/locales/messages.tr.xlf
  31. 638
      apps/client/src/locales/messages.uk.xlf
  32. 386
      apps/client/src/locales/messages.xlf
  33. 516
      apps/client/src/locales/messages.zh.xlf
  34. 5
      libs/common/src/lib/interfaces/admin-data.interface.ts
  35. 94
      package-lock.json
  36. 8
      package.json
  37. 42
      test/import/ok/btcusd-short.json

26
CHANGELOG.md

@ -7,21 +7,45 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
### Added
- Enabled automatic data gathering for custom currencies added via the currency management in the admin control panel
### Changed
- Restructured the response of the portfolio report endpoint (_X-ray_)
- Refactored the create or update access dialog component to standalone
- Upgraded `envalid` from version `8.0.0` to `8.1.0`
- Upgraded `prisma` from version `6.14.0` to `6.15.0`
### Fixed
- Fixed an issue related to the error handling in the data provider status component
## 2.196.0 - 2025-09-04
### Changed
- Localized the content of the about page
- Refactored the public page to standalone
- Refactored the dialog footer component
- Refactored the dialog header component
- Refactored the account detail dialog component to standalone
- Refactored the benchmark comparator component to standalone
- Refactored the portfolio summary component to standalone
- Refactored the world map chart component to standalone
- Restructured the response of the portfolio report endpoint (_X-ray_)
- Enabled the trim option in the `extract-i18n` configuration
- Improved the language localization for German (`de`)
- Upgraded the _Stripe_ dependencies
- Upgraded `ngx-device-detector` from version `10.0.2` to `10.1.0`
- Upgraded `ngx-skeleton-loader` from version `11.2.1` to `11.3.0`
- Upgraded `yahoo-finance2` from version `3.6.4` to `3.8.0`
### Fixed
- Fixed an issue in the average price calculation for buy and sell activities of short positions
- Fixed the number of attempts in the queue jobs view of the admin control panel
## 2.195.0 - 2025-08-29
### Changed

25
apps/api/src/app/admin/admin.service.ts

@ -136,11 +136,13 @@ export class AdminService {
public async get(): Promise<AdminData> {
const dataSources = Object.values(DataSource);
const [settings, transactionCount, userCount] = await Promise.all([
this.propertyService.get(),
this.prismaService.order.count(),
this.countUsersWithAnalytics()
]);
const [enabledDataSources, settings, transactionCount, userCount] =
await Promise.all([
this.dataProviderService.getDataSources(),
this.propertyService.get(),
this.prismaService.order.count(),
this.countUsersWithAnalytics()
]);
const dataProviders = (
await Promise.all(
@ -152,14 +154,23 @@ export class AdminService {
}
});
if (assetProfileCount > 0 || dataSource === 'GHOSTFOLIO') {
const isEnabled = enabledDataSources.includes(dataSource);
if (
assetProfileCount > 0 ||
dataSource === 'GHOSTFOLIO' ||
isEnabled
) {
const dataProviderInfo = this.dataProviderService
.getDataProvider(dataSource)
.getDataProviderInfo();
return {
...dataProviderInfo,
assetProfileCount
assetProfileCount,
useForExchangeRates:
dataSource ===
this.dataProviderService.getDataSourceForExchangeRates()
};
}

2
apps/api/src/app/admin/queue/queue.service.ts

@ -71,7 +71,7 @@ export class QueueService {
.slice(0, limit)
.map(async (job) => {
return {
attemptsMade: job.attemptsMade + 1,
attemptsMade: job.attemptsMade,
data: job.data,
finishedOn: job.finishedOn,
id: job.id,

30
apps/api/src/app/portfolio/calculator/portfolio-calculator.ts

@ -927,13 +927,25 @@ export abstract class PortfolioCalculator {
.plus(oldAccumulatedSymbol.quantity);
if (type === 'BUY') {
investment = oldAccumulatedSymbol.investment.plus(
quantity.mul(unitPrice)
);
if (oldAccumulatedSymbol.investment.gte(0)) {
investment = oldAccumulatedSymbol.investment.plus(
quantity.mul(unitPrice)
);
} else {
investment = oldAccumulatedSymbol.investment.plus(
quantity.mul(oldAccumulatedSymbol.averagePrice)
);
}
} else if (type === 'SELL') {
investment = oldAccumulatedSymbol.investment.minus(
quantity.mul(oldAccumulatedSymbol.averagePrice)
);
if (oldAccumulatedSymbol.investment.gt(0)) {
investment = oldAccumulatedSymbol.investment.minus(
quantity.mul(oldAccumulatedSymbol.averagePrice)
);
} else {
investment = oldAccumulatedSymbol.investment.minus(
quantity.mul(unitPrice)
);
}
}
currentTransactionPointItem = {
@ -942,9 +954,9 @@ export abstract class PortfolioCalculator {
investment,
skipErrors,
symbol,
averagePrice: newQuantity.gt(0)
? investment.div(newQuantity)
: new Big(0),
averagePrice: newQuantity.eq(0)
? new Big(0)
: investment.div(newQuantity).abs(),
dividend: new Big(0),
fee: oldAccumulatedSymbol.fee.plus(fee),
firstBuyDate: oldAccumulatedSymbol.firstBuyDate,

208
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-buy.spec.ts

@ -0,0 +1,208 @@
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import {
activityDummyData,
symbolProfileDummyData,
userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
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 { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
import { Big } from 'big.js';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
CurrentRateService: jest.fn().mockImplementation(() => {
return CurrentRateServiceMock;
})
};
});
jest.mock(
'@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service',
() => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
PortfolioSnapshotService: jest.fn().mockImplementation(() => {
return PortfolioSnapshotServiceMock;
})
};
}
);
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock;
})
};
});
describe('PortfolioCalculator', () => {
let configurationService: ConfigurationService;
let currentRateService: CurrentRateService;
let exchangeRateDataService: ExchangeRateDataService;
let portfolioCalculatorFactory: PortfolioCalculatorFactory;
let portfolioSnapshotService: PortfolioSnapshotService;
let redisCacheService: RedisCacheService;
beforeEach(() => {
configurationService = new ConfigurationService();
currentRateService = new CurrentRateService(null, null, null, null);
exchangeRateDataService = new ExchangeRateDataService(
null,
null,
null,
null
);
portfolioSnapshotService = new PortfolioSnapshotService(null);
redisCacheService = new RedisCacheService(null, null);
portfolioCalculatorFactory = new PortfolioCalculatorFactory(
configurationService,
currentRateService,
exchangeRateDataService,
portfolioSnapshotService,
redisCacheService
);
});
describe('get current positions', () => {
it.only('with BALN.SW buy and buy', async () => {
jest.useFakeTimers().setSystemTime(parseDate('2021-12-18').getTime());
const activities: Activity[] = [
{
...activityDummyData,
date: new Date('2021-11-22'),
feeInAssetProfileCurrency: 1.55,
quantity: 2,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'CHF',
dataSource: 'YAHOO',
name: 'Bâloise Holding AG',
symbol: 'BALN.SW'
},
type: 'BUY',
unitPriceInAssetProfileCurrency: 142.9
},
{
...activityDummyData,
date: new Date('2021-11-30'),
feeInAssetProfileCurrency: 1.65,
quantity: 2,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'CHF',
dataSource: 'YAHOO',
name: 'Bâloise Holding AG',
symbol: 'BALN.SW'
},
type: 'BUY',
unitPriceInAssetProfileCurrency: 136.6
}
];
const portfolioCalculator = portfolioCalculatorFactory.createCalculator({
activities,
calculationType: PerformanceCalculationType.ROAI,
currency: 'CHF',
userId: userDummyData.id
});
const portfolioSnapshot = await portfolioCalculator.computeSnapshot();
const investments = portfolioCalculator.getInvestments();
const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({
data: portfolioSnapshot.historicalData,
groupBy: 'month'
});
expect(portfolioSnapshot).toMatchObject({
currentValueInBaseCurrency: new Big('595.6'),
errors: [],
hasErrors: false,
positions: [
{
averagePrice: new Big('139.75'),
currency: 'CHF',
dataSource: 'YAHOO',
dividend: new Big('0'),
dividendInBaseCurrency: new Big('0'),
fee: new Big('3.2'),
feeInBaseCurrency: new Big('3.2'),
firstBuyDate: '2021-11-22',
grossPerformance: new Big('36.6'),
grossPerformancePercentage: new Big('0.07706261539956593567'),
grossPerformancePercentageWithCurrencyEffect: new Big(
'0.07706261539956593567'
),
grossPerformanceWithCurrencyEffect: new Big('36.6'),
investment: new Big('559'),
investmentWithCurrencyEffect: new Big('559'),
netPerformance: new Big('33.4'),
netPerformancePercentage: new Big('0.07032490039195361342'),
netPerformancePercentageWithCurrencyEffectMap: {
max: new Big('0.06986689805847808234')
},
netPerformanceWithCurrencyEffectMap: {
max: new Big('33.4')
},
marketPrice: 148.9,
marketPriceInBaseCurrency: 148.9,
quantity: new Big('4'),
symbol: 'BALN.SW',
tags: [],
timeWeightedInvestment: new Big('474.93846153846153846154'),
timeWeightedInvestmentWithCurrencyEffect: new Big(
'474.93846153846153846154'
),
transactionCount: 2,
valueInBaseCurrency: new Big('595.6')
}
],
totalFeesWithCurrencyEffect: new Big('3.2'),
totalInterestWithCurrencyEffect: new Big('0'),
totalInvestment: new Big('559'),
totalInvestmentWithCurrencyEffect: new Big('559'),
totalLiabilitiesWithCurrencyEffect: new Big('0')
});
expect(portfolioSnapshot.historicalData.at(-1)).toMatchObject(
expect.objectContaining({
netPerformance: 33.4,
netPerformanceInPercentage: 0.07032490039195362,
netPerformanceInPercentageWithCurrencyEffect: 0.07032490039195362,
netPerformanceWithCurrencyEffect: 33.4,
totalInvestmentValueWithCurrencyEffect: 559
})
);
expect(investments).toEqual([
{ date: '2021-11-22', investment: new Big('285.8') },
{ date: '2021-11-30', investment: new Big('559') }
]);
expect(investmentsByMonth).toEqual([
{ date: '2021-11-01', investment: 559 },
{ date: '2021-12-01', investment: 0 }
]);
});
});
});

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

@ -0,0 +1,132 @@
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import {
activityDummyData,
loadActivityExportFile,
symbolProfileDummyData,
userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
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 { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
import { Tag } from '@prisma/client';
import { Big } from 'big.js';
import { join } from 'path';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
CurrentRateService: jest.fn().mockImplementation(() => {
return CurrentRateServiceMock;
})
};
});
jest.mock(
'@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service',
() => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
PortfolioSnapshotService: jest.fn().mockImplementation(() => {
return PortfolioSnapshotServiceMock;
})
};
}
);
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock;
})
};
});
describe('PortfolioCalculator', () => {
let activityDtos: CreateOrderDto[];
let configurationService: ConfigurationService;
let currentRateService: CurrentRateService;
let exchangeRateDataService: ExchangeRateDataService;
let portfolioCalculatorFactory: PortfolioCalculatorFactory;
let portfolioSnapshotService: PortfolioSnapshotService;
let redisCacheService: RedisCacheService;
beforeAll(() => {
activityDtos = loadActivityExportFile(
join(__dirname, '../../../../../../../test/import/ok/btcusd-short.json')
);
});
beforeEach(() => {
configurationService = new ConfigurationService();
currentRateService = new CurrentRateService(null, null, null, null);
exchangeRateDataService = new ExchangeRateDataService(
null,
null,
null,
null
);
portfolioSnapshotService = new PortfolioSnapshotService(null);
redisCacheService = new RedisCacheService(null, null);
portfolioCalculatorFactory = new PortfolioCalculatorFactory(
configurationService,
currentRateService,
exchangeRateDataService,
portfolioSnapshotService,
redisCacheService
);
});
describe('get current positions', () => {
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 portfolioCalculator = portfolioCalculatorFactory.createCalculator({
activities,
calculationType: PerformanceCalculationType.ROAI,
currency: 'USD',
userId: userDummyData.id
});
const portfolioSnapshot = await portfolioCalculator.computeSnapshot();
expect(portfolioSnapshot.positions[0].averagePrice).toEqual(
Big(45647.95)
);
});
});
});

3
apps/client/project.json

@ -271,7 +271,8 @@
"messages.tr.xlf",
"messages.uk.xlf",
"messages.zh.xlf"
]
],
"trim": true
}
},
"lint": {

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

@ -111,9 +111,7 @@ const routes: Routes = [
{
path: publicRoutes.public.path,
loadChildren: () =>
import('./pages/public/public-page.module').then(
(m) => m.PublicPageModule
)
import('./pages/public/public-page.routes').then((m) => m.routes)
},
{
path: publicRoutes.register.path,

39
apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts

@ -1,5 +1,8 @@
import { CreateAccountBalanceDto } from '@ghostfolio/api/app/account-balance/create-account-balance.dto';
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { GfDialogFooterComponent } from '@ghostfolio/client/components/dialog-footer/dialog-footer.component';
import { GfDialogHeaderComponent } from '@ghostfolio/client/components/dialog-header/dialog-header.component';
import { GfInvestmentChartModule } from '@ghostfolio/client/components/investment-chart/investment-chart.module';
import { DataService } from '@ghostfolio/client/services/data.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { NUMERICAL_PRECISION_THRESHOLD_6_FIGURES } from '@ghostfolio/common/config';
@ -13,7 +16,12 @@ import {
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { internalRoutes } from '@ghostfolio/common/routes/routes';
import { OrderWithAccount } from '@ghostfolio/common/types';
import { GfAccountBalancesComponent } from '@ghostfolio/ui/account-balances';
import { GfActivitiesTableComponent } from '@ghostfolio/ui/activities-table';
import { GfHoldingsTableComponent } from '@ghostfolio/ui/holdings-table';
import { GfValueComponent } from '@ghostfolio/ui/value';
import { CommonModule } from '@angular/common';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
@ -22,10 +30,15 @@ import {
OnDestroy,
OnInit
} from '@angular/core';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { MatDialogModule } from '@angular/material/dialog';
import { Sort, SortDirection } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { MatTabsModule } from '@angular/material/tabs';
import { Router } from '@angular/router';
import { IonIcon } from '@ionic/angular/standalone';
import { Big } from 'big.js';
import { format, parseISO } from 'date-fns';
import { addIcons } from 'ionicons';
@ -35,20 +48,36 @@ import {
walletOutline
} from 'ionicons/icons';
import { isNumber } from 'lodash';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { forkJoin, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { AccountDetailDialogParams } from './interfaces/interfaces';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'd-flex flex-column h-100' },
imports: [
CommonModule,
GfAccountBalancesComponent,
GfActivitiesTableComponent,
GfDialogFooterComponent,
GfDialogHeaderComponent,
GfHoldingsTableComponent,
GfInvestmentChartModule,
GfValueComponent,
IonIcon,
MatButtonModule,
MatDialogModule,
MatTabsModule,
NgxSkeletonLoaderModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
selector: 'gf-account-detail-dialog',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: 'account-detail-dialog.html',
styleUrls: ['./account-detail-dialog.component.scss'],
standalone: false
templateUrl: 'account-detail-dialog.html'
})
export class AccountDetailDialog implements OnDestroy, OnInit {
export class GfAccountDetailDialogComponent implements OnDestroy, OnInit {
public accountBalances: AccountBalancesResponse['balances'];
public activities: OrderWithAccount[];
public balance: number;
@ -81,7 +110,7 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
private changeDetectorRef: ChangeDetectorRef,
@Inject(MAT_DIALOG_DATA) public data: AccountDetailDialogParams,
private dataService: DataService,
public dialogRef: MatDialogRef<AccountDetailDialog>,
public dialogRef: MatDialogRef<GfAccountDetailDialogComponent>,
private router: Router,
private userService: UserService
) {

38
apps/client/src/app/components/account-detail-dialog/account-detail-dialog.module.ts

@ -1,38 +0,0 @@
import { GfDialogFooterComponent } from '@ghostfolio/client/components/dialog-footer/dialog-footer.component';
import { GfDialogHeaderComponent } from '@ghostfolio/client/components/dialog-header/dialog-header.component';
import { GfInvestmentChartModule } from '@ghostfolio/client/components/investment-chart/investment-chart.module';
import { GfAccountBalancesComponent } from '@ghostfolio/ui/account-balances';
import { GfActivitiesTableComponent } from '@ghostfolio/ui/activities-table';
import { GfHoldingsTableComponent } from '@ghostfolio/ui/holdings-table';
import { GfValueComponent } from '@ghostfolio/ui/value';
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule } from '@angular/material/dialog';
import { MatTabsModule } from '@angular/material/tabs';
import { IonIcon } from '@ionic/angular/standalone';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { AccountDetailDialog } from './account-detail-dialog.component';
@NgModule({
declarations: [AccountDetailDialog],
imports: [
CommonModule,
GfAccountBalancesComponent,
GfActivitiesTableComponent,
GfDialogFooterComponent,
GfDialogHeaderComponent,
GfHoldingsTableComponent,
GfInvestmentChartModule,
GfValueComponent,
IonIcon,
MatButtonModule,
MatDialogModule,
MatTabsModule,
NgxSkeletonLoaderModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfAccountDetailDialogModule {}

27
apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.component.ts

@ -1,6 +1,7 @@
import { AdminService } from '@ghostfolio/client/services/admin.service';
import { DataService } from '@ghostfolio/client/services/data.service';
import {
DEFAULT_CURRENCY,
ghostfolioPrefix,
PROPERTY_CURRENCIES
} from '@ghostfolio/common/config';
@ -29,8 +30,9 @@ import { MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatRadioModule } from '@angular/material/radio';
import { DataSource } from '@prisma/client';
import { isISO4217CurrencyCode } from 'class-validator';
import { Subject, takeUntil } from 'rxjs';
import { Subject, switchMap, takeUntil } from 'rxjs';
import { CreateAssetProfileDialogMode } from './interfaces/interfaces';
@ -56,6 +58,7 @@ export class GfCreateAssetProfileDialogComponent implements OnInit, OnDestroy {
public mode: CreateAssetProfileDialogMode;
private customCurrencies: string[];
private dataSourceForExchangeRates: DataSource;
private unsubscribeSubject = new Subject<void>();
public constructor(
@ -67,7 +70,7 @@ export class GfCreateAssetProfileDialogComponent implements OnInit, OnDestroy {
) {}
public ngOnInit() {
this.initializeCustomCurrencies();
this.initialize();
this.createAssetProfileForm = this.formBuilder.group(
{
@ -115,7 +118,15 @@ export class GfCreateAssetProfileDialogComponent implements OnInit, OnDestroy {
.putAdminSetting(PROPERTY_CURRENCIES, {
value: JSON.stringify(currencies)
})
.pipe(takeUntil(this.unsubscribeSubject))
.pipe(
switchMap(() => {
return this.adminService.gatherSymbol({
dataSource: this.dataSourceForExchangeRates,
symbol: `${DEFAULT_CURRENCY}${currency}`
});
}),
takeUntil(this.unsubscribeSubject)
)
.subscribe(() => {
this.dialogRef.close();
});
@ -170,13 +181,19 @@ export class GfCreateAssetProfileDialogComponent implements OnInit, OnDestroy {
return { atLeastOneValid: true };
}
private initializeCustomCurrencies() {
private initialize() {
this.adminService
.fetchAdminData()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ settings }) => {
.subscribe(({ dataProviders, settings }) => {
this.customCurrencies = settings[PROPERTY_CURRENCIES] as string[];
const { dataSource } = dataProviders.find(({ useForExchangeRates }) => {
return useForExchangeRates;
});
this.dataSourceForExchangeRates = dataSource;
this.changeDetectorRef.markForCheck();
});
}

6
apps/client/src/app/components/data-provider-status/data-provider-status.component.ts

@ -33,12 +33,12 @@ export class GfDataProviderStatusComponent implements OnDestroy, OnInit {
this.status$ = this.dataService
.fetchDataProviderHealth(this.dataSource)
.pipe(
catchError(() => {
return of({ isHealthy: false });
}),
map(() => {
return { isHealthy: true };
}),
catchError(() => {
return of({ isHealthy: false });
}),
takeUntil(this.unsubscribeSubject)
);
}

34
apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts

@ -10,8 +10,22 @@ import {
Inject,
OnDestroy
} from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import {
FormBuilder,
FormGroup,
FormsModule,
ReactiveFormsModule,
Validators
} from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import {
MAT_DIALOG_DATA,
MatDialogModule,
MatDialogRef
} from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { StatusCodes } from 'http-status-codes';
import { EMPTY, Subject, catchError, takeUntil } from 'rxjs';
@ -20,12 +34,20 @@ import { CreateOrUpdateAccessDialogParams } from './interfaces/interfaces';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'h-100' },
imports: [
FormsModule,
MatButtonModule,
MatDialogModule,
MatFormFieldModule,
MatInputModule,
MatSelectModule,
ReactiveFormsModule
],
selector: 'gf-create-or-update-access-dialog',
styleUrls: ['./create-or-update-access-dialog.scss'],
templateUrl: 'create-or-update-access-dialog.html',
standalone: false
templateUrl: 'create-or-update-access-dialog.html'
})
export class CreateOrUpdateAccessDialog implements OnDestroy {
export class GfCreateOrUpdateAccessDialog implements OnDestroy {
public accessForm: FormGroup;
private unsubscribeSubject = new Subject<void>();
@ -33,7 +55,7 @@ export class CreateOrUpdateAccessDialog implements OnDestroy {
public constructor(
private changeDetectorRef: ChangeDetectorRef,
@Inject(MAT_DIALOG_DATA) private data: CreateOrUpdateAccessDialogParams,
public dialogRef: MatDialogRef<CreateOrUpdateAccessDialog>,
public dialogRef: MatDialogRef<GfCreateOrUpdateAccessDialog>,
private dataService: DataService,
private formBuilder: FormBuilder,
private notificationService: NotificationService

25
apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.module.ts

@ -1,25 +0,0 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { CreateOrUpdateAccessDialog } from './create-or-update-access-dialog.component';
@NgModule({
declarations: [CreateOrUpdateAccessDialog],
imports: [
CommonModule,
FormsModule,
MatButtonModule,
MatDialogModule,
MatFormFieldModule,
MatInputModule,
MatSelectModule,
ReactiveFormsModule
]
})
export class GfCreateOrUpdateAccessDialogModule {}

6
apps/client/src/app/components/user-account-access/user-account-access.component.ts

@ -30,15 +30,13 @@ import { DeviceDetectorService } from 'ngx-device-detector';
import { EMPTY, Subject } from 'rxjs';
import { catchError, takeUntil } from 'rxjs/operators';
import { CreateOrUpdateAccessDialog } from './create-or-update-access-dialog/create-or-update-access-dialog.component';
import { GfCreateOrUpdateAccessDialogModule } from './create-or-update-access-dialog/create-or-update-access-dialog.module';
import { GfCreateOrUpdateAccessDialog } from './create-or-update-access-dialog/create-or-update-access-dialog.component';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'has-fab' },
imports: [
GfAccessTableComponent,
GfCreateOrUpdateAccessDialogModule,
GfPremiumIndicatorComponent,
IonIcon,
MatButtonModule,
@ -181,7 +179,7 @@ export class GfUserAccountAccessComponent implements OnDestroy, OnInit {
}
private openCreateAccessDialog() {
const dialogRef = this.dialog.open(CreateOrUpdateAccessDialog, {
const dialogRef = this.dialog.open(GfCreateOrUpdateAccessDialog, {
data: {
access: {
alias: '',

12
apps/client/src/app/pages/accounts/accounts-page.component.ts

@ -1,8 +1,7 @@
import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto';
import { TransferBalanceDto } from '@ghostfolio/api/app/account/transfer-balance.dto';
import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto';
import { AccountDetailDialog } from '@ghostfolio/client/components/account-detail-dialog/account-detail-dialog.component';
import { GfAccountDetailDialogModule } from '@ghostfolio/client/components/account-detail-dialog/account-detail-dialog.module';
import { GfAccountDetailDialogComponent } from '@ghostfolio/client/components/account-detail-dialog/account-detail-dialog.component';
import { AccountDetailDialogParams } from '@ghostfolio/client/components/account-detail-dialog/interfaces/interfaces';
import { NotificationService } from '@ghostfolio/client/core/notification/notification.service';
import { DataService } from '@ghostfolio/client/services/data.service';
@ -28,12 +27,7 @@ import { GfTransferBalanceDialogComponent } from './transfer-balance/transfer-ba
@Component({
host: { class: 'has-fab page' },
imports: [
GfAccountDetailDialogModule,
GfAccountsTableComponent,
MatButtonModule,
RouterModule
],
imports: [GfAccountsTableComponent, MatButtonModule, RouterModule],
selector: 'gf-accounts-page',
styleUrls: ['./accounts-page.scss'],
templateUrl: './accounts-page.html'
@ -233,7 +227,7 @@ export class GfAccountsPageComponent implements OnDestroy, OnInit {
}
private openAccountDetailDialog(aAccountId: string) {
const dialogRef = this.dialog.open(AccountDetailDialog, {
const dialogRef = this.dialog.open(GfAccountDetailDialogComponent, {
autoFocus: false,
data: {
accountId: aAccountId,

4
apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts

@ -1,4 +1,4 @@
import { AccountDetailDialog } from '@ghostfolio/client/components/account-detail-dialog/account-detail-dialog.component';
import { GfAccountDetailDialogComponent } from '@ghostfolio/client/components/account-detail-dialog/account-detail-dialog.component';
import { AccountDetailDialogParams } from '@ghostfolio/client/components/account-detail-dialog/interfaces/interfaces';
import { GfWorldMapChartComponent } from '@ghostfolio/client/components/world-map-chart/world-map-chart.component';
import { DataService } from '@ghostfolio/client/services/data.service';
@ -558,7 +558,7 @@ export class GfAllocationsPageComponent implements OnDestroy, OnInit {
}
private openAccountDetailDialog(aAccountId: string) {
const dialogRef = this.dialog.open(AccountDetailDialog, {
const dialogRef = this.dialog.open(GfAccountDetailDialogComponent, {
autoFocus: false,
data: {
accountId: aAccountId,

20
apps/client/src/app/pages/public/public-page-routing.module.ts

@ -1,20 +0,0 @@
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { PublicPageComponent } from './public-page.component';
const routes: Routes = [
{
canActivate: [AuthGuard],
component: PublicPageComponent,
path: ':id'
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class PublicPageRoutingModule {}

29
apps/client/src/app/pages/public/public-page.component.ts

@ -1,3 +1,4 @@
import { GfWorldMapChartComponent } from '@ghostfolio/client/components/world-map-chart/world-map-chart.component';
import { DataService } from '@ghostfolio/client/services/data.service';
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
import { prettifySymbol } from '@ghostfolio/common/helper';
@ -6,8 +7,19 @@ import {
PublicPortfolioResponse
} from '@ghostfolio/common/interfaces';
import { Market } from '@ghostfolio/common/types';
import { GfHoldingsTableComponent } from '@ghostfolio/ui/holdings-table/holdings-table.component';
import { GfPortfolioProportionChartComponent } from '@ghostfolio/ui/portfolio-proportion-chart/portfolio-proportion-chart.component';
import { GfValueComponent } from '@ghostfolio/ui/value';
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import {
ChangeDetectorRef,
Component,
CUSTOM_ELEMENTS_SCHEMA,
OnInit
} from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { ActivatedRoute, Router } from '@angular/router';
import { AssetClass } from '@prisma/client';
import { StatusCodes } from 'http-status-codes';
@ -18,12 +30,21 @@ import { catchError, takeUntil } from 'rxjs/operators';
@Component({
host: { class: 'page' },
imports: [
CommonModule,
GfHoldingsTableComponent,
GfPortfolioProportionChartComponent,
GfValueComponent,
GfWorldMapChartComponent,
MatButtonModule,
MatCardModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
selector: 'gf-public-page',
styleUrls: ['./public-page.scss'],
templateUrl: './public-page.html',
standalone: false
templateUrl: './public-page.html'
})
export class PublicPageComponent implements OnInit {
export class GfPublicPageComponent implements OnInit {
public continents: {
[code: string]: { name: string; value: number };
};

28
apps/client/src/app/pages/public/public-page.module.ts

@ -1,28 +0,0 @@
import { GfWorldMapChartComponent } from '@ghostfolio/client/components/world-map-chart/world-map-chart.component';
import { GfHoldingsTableComponent } from '@ghostfolio/ui/holdings-table';
import { GfPortfolioProportionChartComponent } from '@ghostfolio/ui/portfolio-proportion-chart';
import { GfValueComponent } from '@ghostfolio/ui/value';
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { PublicPageRoutingModule } from './public-page-routing.module';
import { PublicPageComponent } from './public-page.component';
@NgModule({
declarations: [PublicPageComponent],
imports: [
CommonModule,
GfHoldingsTableComponent,
GfPortfolioProportionChartComponent,
GfValueComponent,
GfWorldMapChartComponent,
MatButtonModule,
MatCardModule,
PublicPageRoutingModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class PublicPageModule {}

13
apps/client/src/app/pages/public/public-page.routes.ts

@ -0,0 +1,13 @@
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
import { Routes } from '@angular/router';
import { GfPublicPageComponent } from './public-page.component';
export const routes: Routes = [
{
canActivate: [AuthGuard],
component: GfPublicPageComponent,
path: ':id'
}
];

588
apps/client/src/locales/messages.ca.xlf

File diff suppressed because it is too large

640
apps/client/src/locales/messages.de.xlf

File diff suppressed because it is too large

594
apps/client/src/locales/messages.es.xlf

File diff suppressed because it is too large

642
apps/client/src/locales/messages.fr.xlf

File diff suppressed because it is too large

638
apps/client/src/locales/messages.it.xlf

File diff suppressed because it is too large

564
apps/client/src/locales/messages.nl.xlf

File diff suppressed because it is too large

604
apps/client/src/locales/messages.pl.xlf

File diff suppressed because it is too large

644
apps/client/src/locales/messages.pt.xlf

File diff suppressed because it is too large

596
apps/client/src/locales/messages.tr.xlf

File diff suppressed because it is too large

638
apps/client/src/locales/messages.uk.xlf

File diff suppressed because it is too large

386
apps/client/src/locales/messages.xlf

File diff suppressed because it is too large

516
apps/client/src/locales/messages.zh.xlf

File diff suppressed because it is too large

5
libs/common/src/lib/interfaces/admin-data.interface.ts

@ -1,7 +1,10 @@
import { DataProviderInfo } from './data-provider-info.interface';
export interface AdminData {
dataProviders: (DataProviderInfo & { assetProfileCount: number })[];
dataProviders: (DataProviderInfo & {
assetProfileCount: number;
useForExchangeRates: boolean;
})[];
settings: { [key: string]: boolean | object | string | string[] };
transactionCount: number;
userCount: number;

94
package-lock.json

@ -1,12 +1,12 @@
{
"name": "ghostfolio",
"version": "2.195.0",
"version": "2.196.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ghostfolio",
"version": "2.195.0",
"version": "2.196.0",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {
@ -44,7 +44,7 @@
"@nestjs/schedule": "6.0.0",
"@nestjs/serve-static": "5.0.3",
"@openrouter/ai-sdk-provider": "0.7.2",
"@prisma/client": "6.14.0",
"@prisma/client": "6.15.0",
"@simplewebauthn/browser": "13.1.0",
"@simplewebauthn/server": "13.1.1",
"@stripe/stripe-js": "7.9.0",
@ -66,7 +66,7 @@
"countries-list": "3.1.1",
"countup.js": "2.8.2",
"date-fns": "4.1.0",
"envalid": "8.0.0",
"envalid": "8.1.0",
"fuse.js": "7.1.0",
"google-spreadsheet": "3.2.0",
"helmet": "7.0.0",
@ -149,7 +149,7 @@
"nx": "21.3.9",
"prettier": "3.6.2",
"prettier-plugin-organize-attributes": "1.0.0",
"prisma": "6.14.0",
"prisma": "6.15.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"replace-in-file": "8.3.0",
@ -10494,9 +10494,9 @@
"license": "MIT"
},
"node_modules/@prisma/client": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.14.0.tgz",
"integrity": "sha512-8E/Nk3eL5g7RQIg/LUj1ICyDmhD053STjxrPxUtCRybs2s/2sOEcx9NpITuAOPn07HEpWBfhAVe1T/HYWXUPOw==",
"version": "6.15.0",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.15.0.tgz",
"integrity": "sha512-wR2LXUbOH4cL/WToatI/Y2c7uzni76oNFND7+23ypLllBmIS8e3ZHhO+nud9iXSXKFt1SoM3fTZvHawg63emZw==",
"hasInstallScript": true,
"license": "Apache-2.0",
"engines": {
@ -10516,9 +10516,9 @@
}
},
"node_modules/@prisma/config": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.14.0.tgz",
"integrity": "sha512-IwC7o5KNNGhmblLs23swnfBjADkacBb7wvyDXUWLwuvUQciKJZqyecU0jw0d7JRkswrj+XTL8fdr0y2/VerKQQ==",
"version": "6.15.0",
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.15.0.tgz",
"integrity": "sha512-KMEoec9b2u6zX0EbSEx/dRpx1oNLjqJEBZYyK0S3TTIbZ7GEGoVyGyFRk4C72+A38cuPLbfQGQvgOD+gBErKlA==",
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
@ -10529,53 +10529,53 @@
}
},
"node_modules/@prisma/debug": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.14.0.tgz",
"integrity": "sha512-j4Lf+y+5QIJgQD4sJWSbkOD7geKx9CakaLp/TyTy/UDu9Wo0awvWCBH/BAxTHUaCpIl9USA5VS/KJhDqKJSwug==",
"version": "6.15.0",
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.15.0.tgz",
"integrity": "sha512-y7cSeLuQmyt+A3hstAs6tsuAiVXSnw9T55ra77z0nbNkA8Lcq9rNcQg6PI00by/+WnE/aMRJ/W7sZWn2cgIy1g==",
"devOptional": true,
"license": "Apache-2.0"
},
"node_modules/@prisma/engines": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.14.0.tgz",
"integrity": "sha512-LhJjqsALFEcoAtF07nSaOkVguaxw/ZsgfROIYZ8bAZDobe7y8Wy+PkYQaPOK1iLSsFgV2MhCO/eNrI1gdSOj6w==",
"version": "6.15.0",
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.15.0.tgz",
"integrity": "sha512-opITiR5ddFJ1N2iqa7mkRlohCZqVSsHhRcc29QXeldMljOf4FSellLT0J5goVb64EzRTKcIDeIsJBgmilNcKxA==",
"devOptional": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/debug": "6.14.0",
"@prisma/engines-version": "6.14.0-25.717184b7b35ea05dfa71a3236b7af656013e1e49",
"@prisma/fetch-engine": "6.14.0",
"@prisma/get-platform": "6.14.0"
"@prisma/debug": "6.15.0",
"@prisma/engines-version": "6.15.0-5.85179d7826409ee107a6ba334b5e305ae3fba9fb",
"@prisma/fetch-engine": "6.15.0",
"@prisma/get-platform": "6.15.0"
}
},
"node_modules/@prisma/engines-version": {
"version": "6.14.0-25.717184b7b35ea05dfa71a3236b7af656013e1e49",
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.14.0-25.717184b7b35ea05dfa71a3236b7af656013e1e49.tgz",
"integrity": "sha512-EgN9ODJpiX45yvwcngoStp3uQPJ3l+AEVoQ6dMMO2QvmwIlnxfApzKmJQExzdo7/hqQANrz5txHJdGYHzOnGHA==",
"version": "6.15.0-5.85179d7826409ee107a6ba334b5e305ae3fba9fb",
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.15.0-5.85179d7826409ee107a6ba334b5e305ae3fba9fb.tgz",
"integrity": "sha512-a/46aK5j6L3ePwilZYEgYDPrhBQ/n4gYjLxT5YncUTJJNRnTCVjPF86QdzUOLRdYjCLfhtZp9aum90W0J+trrg==",
"devOptional": true,
"license": "Apache-2.0"
},
"node_modules/@prisma/fetch-engine": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.14.0.tgz",
"integrity": "sha512-MPzYPOKMENYOaY3AcAbaKrfvXVlvTc6iHmTXsp9RiwCX+bPyfDMqMFVUSVXPYrXnrvEzhGHfyiFy0PRLHPysNg==",
"version": "6.15.0",
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.15.0.tgz",
"integrity": "sha512-xcT5f6b+OWBq6vTUnRCc7qL+Im570CtwvgSj+0MTSGA1o9UDSKZ/WANvwtiRXdbYWECpyC3CukoG3A04VTAPHw==",
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/debug": "6.14.0",
"@prisma/engines-version": "6.14.0-25.717184b7b35ea05dfa71a3236b7af656013e1e49",
"@prisma/get-platform": "6.14.0"
"@prisma/debug": "6.15.0",
"@prisma/engines-version": "6.15.0-5.85179d7826409ee107a6ba334b5e305ae3fba9fb",
"@prisma/get-platform": "6.15.0"
}
},
"node_modules/@prisma/get-platform": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.14.0.tgz",
"integrity": "sha512-7VjuxKNwjnBhKfqPpMeWiHEa2sVjYzmHdl1slW6STuUCe9QnOY0OY1ljGSvz6wpG4U8DfbDqkG1yofd/1GINww==",
"version": "6.15.0",
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.15.0.tgz",
"integrity": "sha512-Jbb+Xbxyp05NSR1x2epabetHiXvpO8tdN2YNoWoA/ZsbYyxxu/CO/ROBauIFuMXs3Ti+W7N7SJtWsHGaWte9Rg==",
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/debug": "6.14.0"
"@prisma/debug": "6.15.0"
}
},
"node_modules/@redis/client": {
@ -19956,23 +19956,17 @@
}
},
"node_modules/envalid": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/envalid/-/envalid-8.0.0.tgz",
"integrity": "sha512-PGeYJnJB5naN0ME6SH8nFcDj9HVbLpYIfg1p5lAyM9T4cH2lwtu2fLbozC/bq+HUUOIFxhX/LP0/GmlqPHT4tQ==",
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/envalid/-/envalid-8.1.0.tgz",
"integrity": "sha512-OT6+qVhKVyCidaGoXflb2iK1tC8pd0OV2Q+v9n33wNhUJ+lus+rJobUj4vJaQBPxPZ0vYrPGuxdrenyCAIJcow==",
"license": "MIT",
"dependencies": {
"tslib": "2.6.2"
"tslib": "2.8.1"
},
"engines": {
"node": ">=8.12"
"node": ">=18"
}
},
"node_modules/envalid/node_modules/tslib": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
"license": "0BSD"
},
"node_modules/environment": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz",
@ -34537,15 +34531,15 @@
}
},
"node_modules/prisma": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.14.0.tgz",
"integrity": "sha512-QEuCwxu+Uq9BffFw7in8In+WfbSUN0ewnaSUKloLkbJd42w6EyFckux4M0f7VwwHlM3A8ssaz4OyniCXlsn0WA==",
"version": "6.15.0",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.15.0.tgz",
"integrity": "sha512-E6RCgOt+kUVtjtZgLQDBJ6md2tDItLJNExwI0XJeBc1FKL+Vwb+ovxXxuok9r8oBgsOXBA33fGDuE/0qDdCWqQ==",
"devOptional": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/config": "6.14.0",
"@prisma/engines": "6.14.0"
"@prisma/config": "6.15.0",
"@prisma/engines": "6.15.0"
},
"bin": {
"prisma": "build/index.js"

8
package.json

@ -1,6 +1,6 @@
{
"name": "ghostfolio",
"version": "2.195.0",
"version": "2.196.0",
"homepage": "https://ghostfol.io",
"license": "AGPL-3.0",
"repository": "https://github.com/ghostfolio/ghostfolio",
@ -90,7 +90,7 @@
"@nestjs/schedule": "6.0.0",
"@nestjs/serve-static": "5.0.3",
"@openrouter/ai-sdk-provider": "0.7.2",
"@prisma/client": "6.14.0",
"@prisma/client": "6.15.0",
"@simplewebauthn/browser": "13.1.0",
"@simplewebauthn/server": "13.1.1",
"@stripe/stripe-js": "7.9.0",
@ -112,7 +112,7 @@
"countries-list": "3.1.1",
"countup.js": "2.8.2",
"date-fns": "4.1.0",
"envalid": "8.0.0",
"envalid": "8.1.0",
"fuse.js": "7.1.0",
"google-spreadsheet": "3.2.0",
"helmet": "7.0.0",
@ -195,7 +195,7 @@
"nx": "21.3.9",
"prettier": "3.6.2",
"prettier-plugin-organize-attributes": "1.0.0",
"prisma": "6.14.0",
"prisma": "6.15.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"replace-in-file": "8.3.0",

42
test/import/ok/btcusd-short.json

@ -0,0 +1,42 @@
{
"meta": {
"date": "2021-12-12T00:00:00.000Z",
"version": "dev"
},
"accounts": [],
"platforms": [],
"tags": [],
"activities": [
{
"accountId": null,
"comment": null,
"fee": 4.46,
"quantity": 1,
"type": "SELL",
"unitPrice": 44558.42,
"currency": "USD",
"dataSource": "YAHOO",
"date": "2021-12-12T00:00:00.000Z",
"symbol": "BTCUSD",
"tags": []
},
{
"accountId": null,
"comment": null,
"fee": 4.46,
"quantity": 1,
"type": "SELL",
"unitPrice": 46737.48,
"currency": "USD",
"dataSource": "YAHOO",
"date": "2021-12-13T00:00:00.000Z",
"symbol": "BTCUSD",
"tags": []
}
],
"user": {
"settings": {
"currency": "USD"
}
}
}
Loading…
Cancel
Save