diff --git a/CHANGELOG.md b/CHANGELOG.md index 27cb78932..9a72aa776 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,8 +13,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Changed the build executor of the client from `@nx/angular:webpack-browser` to `@nx/angular:browser-esbuild` + +### Fixed + +- Fixed the style of the safe withdrawal rate selector in the _FIRE_ section (experimental) + +## 2.214.0 - 2025-11-01 + +### Changed + - Improved the icon of the _View Holding_ menu item in the activities table +- Ensured atomic data replacement during historical market data gathering +- Removed _Internet Identity_ as a social login provider - Refreshed the cryptocurrencies list +- Upgraded `countries-list` from version `3.1.1` to `3.2.0` +- Upgraded `ng-extract-i18n-merge` from version `3.0.0` to `3.1.0` +- Upgraded `twitter-api-v2` from version `1.23.0` to `1.27.0` ## 2.213.0 - 2025-10-30 diff --git a/apps/api/src/app/admin/admin.controller.ts b/apps/api/src/app/admin/admin.controller.ts index d7c4c5d3d..2419b0a7d 100644 --- a/apps/api/src/app/admin/admin.controller.ts +++ b/apps/api/src/app/admin/admin.controller.ts @@ -17,7 +17,7 @@ import { getAssetProfileIdentifier } from '@ghostfolio/common/helper'; import { AdminData, AdminMarketData, - AdminUsers, + AdminUsersResponse, EnhancedSymbolProfile, ScraperConfiguration } from '@ghostfolio/common/interfaces'; @@ -315,7 +315,7 @@ export class AdminController { public async getUsers( @Query('skip') skip?: number, @Query('take') take?: number - ): Promise { + ): Promise { return this.adminService.getUsers({ skip: isNaN(skip) ? undefined : skip, take: isNaN(take) ? undefined : take diff --git a/apps/api/src/app/admin/admin.service.ts b/apps/api/src/app/admin/admin.service.ts index 11f6f0599..683e72cb8 100644 --- a/apps/api/src/app/admin/admin.service.ts +++ b/apps/api/src/app/admin/admin.service.ts @@ -23,7 +23,7 @@ import { AdminMarketData, AdminMarketDataDetails, AdminMarketDataItem, - AdminUsers, + AdminUsersResponse, AssetProfileIdentifier, EnhancedSymbolProfile, Filter @@ -513,7 +513,7 @@ export class AdminService { }: { skip?: number; take?: number; - }): Promise { + }): Promise { const [count, users] = await Promise.all([ this.countUsersWithAnalytics(), this.getUsersWithAnalytics({ skip, take }) @@ -818,7 +818,7 @@ export class AdminService { }: { skip?: number; take?: number; - }): Promise { + }): Promise { let orderBy: Prisma.Enumerable = [ { createdAt: 'desc' } ]; diff --git a/apps/api/src/app/auth/auth.controller.ts b/apps/api/src/app/auth/auth.controller.ts index 13d8e37f6..57fd04bc7 100644 --- a/apps/api/src/app/auth/auth.controller.ts +++ b/apps/api/src/app/auth/auth.controller.ts @@ -102,23 +102,6 @@ export class AuthController { } } - @Post('internet-identity') - public async internetIdentityLogin( - @Body() body: { principalId: string } - ): Promise { - try { - const authToken = await this.authService.validateInternetIdentityLogin( - body.principalId - ); - return { authToken }; - } catch { - throw new HttpException( - getReasonPhrase(StatusCodes.FORBIDDEN), - StatusCodes.FORBIDDEN - ); - } - } - @Get('webauthn/generate-registration-options') @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async generateRegistrationOptions() { diff --git a/apps/api/src/app/auth/auth.service.ts b/apps/api/src/app/auth/auth.service.ts index ceff492a0..a6ee5d260 100644 --- a/apps/api/src/app/auth/auth.service.ts +++ b/apps/api/src/app/auth/auth.service.ts @@ -4,7 +4,6 @@ import { PropertyService } from '@ghostfolio/api/services/property/property.serv import { Injectable, InternalServerErrorException } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; -import { Provider } from '@prisma/client'; import { ValidateOAuthLoginParams } from './interfaces/interfaces'; @@ -44,42 +43,6 @@ export class AuthService { }); } - public async validateInternetIdentityLogin(principalId: string) { - try { - const provider: Provider = 'INTERNET_IDENTITY'; - - let [user] = await this.userService.users({ - where: { provider, thirdPartyId: principalId } - }); - - if (!user) { - const isUserSignupEnabled = - await this.propertyService.isUserSignupEnabled(); - - if (!isUserSignupEnabled || true) { - throw new Error('Sign up forbidden'); - } - - // Create new user if not found - user = await this.userService.createUser({ - data: { - provider, - thirdPartyId: principalId - } - }); - } - - return this.jwtService.sign({ - id: user.id - }); - } catch (error) { - throw new InternalServerErrorException( - 'validateInternetIdentityLogin', - error.message - ); - } - } - public async validateOAuthLogin({ provider, thirdPartyId diff --git a/apps/api/src/app/auth/google.strategy.ts b/apps/api/src/app/auth/google.strategy.ts index 02f82a7a8..3e4b4ca0d 100644 --- a/apps/api/src/app/auth/google.strategy.ts +++ b/apps/api/src/app/auth/google.strategy.ts @@ -3,6 +3,7 @@ import { ConfigurationService } from '@ghostfolio/api/services/configuration/con import { Injectable, Logger } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { Provider } from '@prisma/client'; +import { DoneCallback } from 'passport'; import { Profile, Strategy } from 'passport-google-oauth20'; import { AuthService } from './auth.service'; @@ -29,7 +30,7 @@ export class GoogleStrategy extends PassportStrategy(Strategy, 'google') { _token: string, _refreshToken: string, profile: Profile, - done: Function + done: DoneCallback ) { try { const jwt = await this.authService.validateOAuthLogin({ diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur-in-base-currency-eur.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur-in-base-currency-eur.spec.ts new file mode 100644 index 000000000..87893e647 --- /dev/null +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur-in-base-currency-eur.spec.ts @@ -0,0 +1,140 @@ +import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; +import { + activityDummyData, + loadExportFile, + 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 { ExchangeRateDataServiceMock } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service.mock'; +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 { ExportResponse } from '@ghostfolio/common/interfaces'; +import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; + +import { Big } from 'big.js'; +import { join } from 'node:path'; + +jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { + return { + CurrentRateService: jest.fn().mockImplementation(() => { + return CurrentRateServiceMock; + }) + }; +}); + +jest.mock( + '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service', + () => { + return { + ExchangeRateDataService: jest.fn().mockImplementation(() => { + return ExchangeRateDataServiceMock; + }) + }; + } +); + +jest.mock( + '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service', + () => { + return { + PortfolioSnapshotService: jest.fn().mockImplementation(() => { + return PortfolioSnapshotServiceMock; + }) + }; + } +); + +jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { + return { + RedisCacheService: jest.fn().mockImplementation(() => { + return RedisCacheServiceMock; + }) + }; +}); + +describe('PortfolioCalculator', () => { + let exportResponse: ExportResponse; + + let configurationService: ConfigurationService; + let currentRateService: CurrentRateService; + let exchangeRateDataService: ExchangeRateDataService; + let portfolioCalculatorFactory: PortfolioCalculatorFactory; + let portfolioSnapshotService: PortfolioSnapshotService; + let redisCacheService: RedisCacheService; + + beforeAll(() => { + exportResponse = loadExportFile( + join(__dirname, '../../../../../../../test/import/ok/btceur.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 buy (in EUR)', async () => { + jest.useFakeTimers().setSystemTime(parseDate('2022-01-14').getTime()); + + 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: 'EUR', + userId: userDummyData.id + }); + + const portfolioSnapshot = await portfolioCalculator.computeSnapshot(); + + expect(portfolioSnapshot.positions[0].fee).toEqual(new Big(4.46)); + expect( + portfolioSnapshot.positions[0].feeInBaseCurrency.toNumber() + ).toBeCloseTo(3.94, 1); + }); + }); +}); diff --git a/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.mock.ts b/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.mock.ts index 8f5d1c28a..076375523 100644 --- a/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.mock.ts +++ b/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.mock.ts @@ -17,11 +17,21 @@ export const ExchangeRateDataServiceMock = { '2023-07-10': 0.8854 } }); + } else if (targetCurrency === 'EUR') { + return Promise.resolve({ + EUREUR: { + '2021-12-12': 1 + }, + USDEUR: { + '2021-12-12': 0.8855 + } + }); } else if (targetCurrency === 'USD') { return Promise.resolve({ USDUSD: { '2018-01-01': 1, '2021-11-16': 1, + '2021-12-12': 1, '2023-07-10': 1 } }); diff --git a/apps/api/src/services/i18n/i18n.service.ts b/apps/api/src/services/i18n/i18n.service.ts index 0f1f6239d..cf340d7c6 100644 --- a/apps/api/src/services/i18n/i18n.service.ts +++ b/apps/api/src/services/i18n/i18n.service.ts @@ -65,7 +65,7 @@ export class I18nService { } private parseLanguageCode(aFileName: string) { - const match = aFileName.match(/\.([a-zA-Z]+)\.xlf$/); + const match = /\.([a-zA-Z]+)\.xlf$/.exec(aFileName); return match ? match[1] : DEFAULT_LANGUAGE_CODE; } diff --git a/apps/api/src/services/interfaces/interfaces.ts b/apps/api/src/services/interfaces/interfaces.ts index 7469754b5..492c2bd35 100644 --- a/apps/api/src/services/interfaces/interfaces.ts +++ b/apps/api/src/services/interfaces/interfaces.ts @@ -20,4 +20,5 @@ export interface DataProviderResponse { export interface DataGatheringItem extends AssetProfileIdentifier { date?: Date; + force?: boolean; } diff --git a/apps/api/src/services/market-data/market-data.service.ts b/apps/api/src/services/market-data/market-data.service.ts index 38ad61663..d318b9a70 100644 --- a/apps/api/src/services/market-data/market-data.service.ts +++ b/apps/api/src/services/market-data/market-data.service.ts @@ -132,6 +132,61 @@ export class MarketDataService { }); } + /** + * Atomically replace market data for a symbol within a date range. + * Deletes existing data in the range and inserts new data within a single + * transaction to prevent data loss if the operation fails. + */ + public async replaceForSymbol({ + data, + dataSource, + symbol + }: AssetProfileIdentifier & { data: Prisma.MarketDataUpdateInput[] }) { + await this.prismaService.$transaction(async (prisma) => { + if (data.length > 0) { + let minTime = Infinity; + let maxTime = -Infinity; + + for (const { date } of data) { + const time = (date as Date).getTime(); + + if (time < minTime) { + minTime = time; + } + + if (time > maxTime) { + maxTime = time; + } + } + + const minDate = new Date(minTime); + const maxDate = new Date(maxTime); + + await prisma.marketData.deleteMany({ + where: { + dataSource, + symbol, + date: { + gte: minDate, + lte: maxDate + } + } + }); + + await prisma.marketData.createMany({ + data: data.map(({ date, marketPrice, state }) => ({ + dataSource, + symbol, + date: date as Date, + marketPrice: marketPrice as number, + state: state as MarketDataState + })), + skipDuplicates: true + }); + } + }); + } + public async updateAssetProfileIdentifier( oldAssetProfileIdentifier: AssetProfileIdentifier, newAssetProfileIdentifier: AssetProfileIdentifier diff --git a/apps/api/src/services/queues/data-gathering/data-gathering.processor.ts b/apps/api/src/services/queues/data-gathering/data-gathering.processor.ts index 1a172f3ea..1a4038652 100644 --- a/apps/api/src/services/queues/data-gathering/data-gathering.processor.ts +++ b/apps/api/src/services/queues/data-gathering/data-gathering.processor.ts @@ -100,7 +100,7 @@ export class DataGatheringProcessor { name: GATHER_HISTORICAL_MARKET_DATA_PROCESS_JOB_NAME }) public async gatherHistoricalMarketData(job: Job) { - const { dataSource, date, symbol } = job.data; + const { dataSource, date, force, symbol } = job.data; try { let currentDate = parseISO(date as unknown as string); @@ -109,7 +109,7 @@ export class DataGatheringProcessor { `Historical market data gathering has been started for ${symbol} (${dataSource}) at ${format( currentDate, DATE_FORMAT - )}`, + )}${force ? ' (forced update)' : ''}`, `DataGatheringProcessor (${GATHER_HISTORICAL_MARKET_DATA_PROCESS_JOB_NAME})` ); @@ -157,7 +157,15 @@ export class DataGatheringProcessor { currentDate = addDays(currentDate, 1); } - await this.marketDataService.updateMany({ data }); + if (force) { + await this.marketDataService.replaceForSymbol({ + data, + dataSource, + symbol + }); + } else { + await this.marketDataService.updateMany({ data }); + } Logger.log( `Historical market data gathering has been completed for ${symbol} (${dataSource}) at ${format( diff --git a/apps/api/src/services/queues/data-gathering/data-gathering.service.ts b/apps/api/src/services/queues/data-gathering/data-gathering.service.ts index 2d3ec45ad..c433f692f 100644 --- a/apps/api/src/services/queues/data-gathering/data-gathering.service.ts +++ b/apps/api/src/services/queues/data-gathering/data-gathering.service.ts @@ -2,7 +2,6 @@ import { DataProviderService } from '@ghostfolio/api/services/data-provider/data import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { DataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces'; -import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; @@ -41,7 +40,6 @@ export class DataGatheringService { private readonly dataGatheringQueue: Queue, private readonly dataProviderService: DataProviderService, private readonly exchangeRateDataService: ExchangeRateDataService, - private readonly marketDataService: MarketDataService, private readonly prismaService: PrismaService, private readonly propertyService: PropertyService, private readonly symbolProfileService: SymbolProfileService @@ -95,8 +93,6 @@ export class DataGatheringService { } public async gatherSymbol({ dataSource, date, symbol }: DataGatheringItem) { - await this.marketDataService.deleteMany({ dataSource, symbol }); - const dataGatheringItems = (await this.getSymbolsMax()) .filter((dataGatheringItem) => { return ( @@ -111,6 +107,7 @@ export class DataGatheringService { await this.gatherSymbols({ dataGatheringItems, + force: true, priority: DATA_GATHERING_QUEUE_PRIORITY_HIGH }); } @@ -274,9 +271,11 @@ export class DataGatheringService { public async gatherSymbols({ dataGatheringItems, + force = false, priority }: { dataGatheringItems: DataGatheringItem[]; + force?: boolean; priority: number; }) { await this.addJobsToQueue( @@ -285,6 +284,7 @@ export class DataGatheringService { data: { dataSource, date, + force, symbol }, name: GATHER_HISTORICAL_MARKET_DATA_PROCESS_JOB_NAME, diff --git a/apps/client/project.json b/apps/client/project.json index adb63d5c1..0d3571cdf 100644 --- a/apps/client/project.json +++ b/apps/client/project.json @@ -61,30 +61,30 @@ }, "targets": { "build": { - "executor": "@nx/angular:webpack-browser", + "executor": "@nx/angular:browser-esbuild", "options": { - "deleteOutputPath": false, - "localize": true, - "outputPath": "dist/apps/client", "index": "apps/client/src/index.html", "main": "apps/client/src/main.ts", - "polyfills": "apps/client/src/polyfills.ts", + "outputPath": "dist/apps/client", "tsConfig": "apps/client/tsconfig.app.json", + "buildOptimizer": false, + "deleteOutputPath": false, + "extractLicenses": false, + "localize": true, + "namedChunks": true, + "ngswConfigPath": "apps/client/ngsw-config.json", + "optimization": false, + "polyfills": "apps/client/src/polyfills.ts", + "scripts": ["node_modules/marked/marked.min.js"], + "serviceWorker": true, + "sourceMap": true, "styles": [ "apps/client/src/assets/fonts/inter.css", "apps/client/src/styles/theme.scss", "apps/client/src/styles.scss", "node_modules/open-color/open-color.css" ], - "scripts": ["node_modules/marked/marked.min.js"], - "vendorChunk": true, - "extractLicenses": false, - "buildOptimizer": false, - "sourceMap": true, - "optimization": false, - "namedChunks": true, - "serviceWorker": true, - "ngswConfigPath": "apps/client/ngsw-config.json" + "vendorChunk": true }, "configurations": { "development-ca": { @@ -136,19 +136,6 @@ "localize": ["zh"] }, "production": { - "fileReplacements": [ - { - "replace": "apps/client/src/environments/environment.ts", - "with": "apps/client/src/environments/environment.prod.ts" - } - ], - "optimization": true, - "outputHashing": "all", - "sourceMap": false, - "namedChunks": false, - "extractLicenses": true, - "vendorChunk": false, - "buildOptimizer": true, "budgets": [ { "type": "initial", @@ -160,7 +147,20 @@ "maximumWarning": "6kb", "maximumError": "10kb" } - ] + ], + "buildOptimizer": true, + "extractLicenses": true, + "fileReplacements": [ + { + "replace": "apps/client/src/environments/environment.ts", + "with": "apps/client/src/environments/environment.prod.ts" + } + ], + "namedChunks": false, + "optimization": true, + "outputHashing": "all", + "sourceMap": false, + "vendorChunk": false } }, "outputs": ["{options.outputPath}"], diff --git a/apps/client/src/app/components/admin-users/admin-users.component.ts b/apps/client/src/app/components/admin-users/admin-users.component.ts index c0d058ad2..94b5839c6 100644 --- a/apps/client/src/app/components/admin-users/admin-users.component.ts +++ b/apps/client/src/app/components/admin-users/admin-users.component.ts @@ -5,7 +5,11 @@ import { getDateFormatString, getEmojiFlag } from '@ghostfolio/common/helper'; -import { AdminUsers, InfoItem, User } from '@ghostfolio/common/interfaces'; +import { + AdminUsersResponse, + InfoItem, + User +} from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator'; import { GfValueComponent } from '@ghostfolio/ui/value'; @@ -75,7 +79,7 @@ import { GfUserDetailDialogComponent } from '../user-detail-dialog/user-detail-d export class GfAdminUsersComponent implements OnDestroy, OnInit { @ViewChild(MatPaginator) paginator: MatPaginator; - public dataSource = new MatTableDataSource(); + public dataSource = new MatTableDataSource(); public defaultDateFormat: string; public deviceType: string; public displayedColumns: string[] = []; diff --git a/apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.component.ts b/apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.component.ts index aaca03594..c0926150f 100644 --- a/apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.component.ts +++ b/apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.component.ts @@ -1,10 +1,8 @@ import { GfDialogHeaderComponent } from '@ghostfolio/client/components/dialog-header/dialog-header.component'; -import { InternetIdentityService } from '@ghostfolio/client/services/internet-identity.service'; import { KEY_STAY_SIGNED_IN, SettingsStorageService } from '@ghostfolio/client/services/settings-storage.service'; -import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service'; import { CommonModule } from '@angular/common'; import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'; @@ -21,7 +19,6 @@ import { } from '@angular/material/dialog'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; -import { Router } from '@angular/router'; import { IonIcon } from '@ionic/angular/standalone'; import { addIcons } from 'ionicons'; import { eyeOffOutline, eyeOutline } from 'ionicons/icons'; @@ -55,10 +52,7 @@ export class GfLoginWithAccessTokenDialogComponent { public constructor( @Inject(MAT_DIALOG_DATA) public data: LoginWithAccessTokenDialogParams, public dialogRef: MatDialogRef, - private internetIdentityService: InternetIdentityService, - private router: Router, - private settingsStorageService: SettingsStorageService, - private tokenStorageService: TokenStorageService + private settingsStorageService: SettingsStorageService ) { addIcons({ eyeOffOutline, eyeOutline }); } @@ -81,14 +75,4 @@ export class GfLoginWithAccessTokenDialogComponent { }); } } - - public async onLoginWithInternetIdentity() { - try { - const { authToken } = await this.internetIdentityService.login(); - - this.tokenStorageService.saveToken(authToken); - this.dialogRef.close(); - this.router.navigate(['/']); - } catch {} - } } diff --git a/apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html b/apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html index b51802caf..15e68822a 100644 --- a/apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html +++ b/apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html @@ -26,17 +26,6 @@ @if (data.hasPermissionToUseSocialLogin) {
or
- { + preload(route: Route, load: () => Observable): Observable { return route.data?.preload ? load() : of(null); } } diff --git a/apps/client/src/app/pages/portfolio/fire/fire-page.scss b/apps/client/src/app/pages/portfolio/fire/fire-page.scss index 2892885c9..3a0618ed6 100644 --- a/apps/client/src/app/pages/portfolio/fire/fire-page.scss +++ b/apps/client/src/app/pages/portfolio/fire/fire-page.scss @@ -1,9 +1,21 @@ +@use '../../../../styles/variables.scss' as variables; + +@mixin select-arrow($color) { + background-image: url("data:image/svg+xml;utf8,"); +} + :host { display: block; .safe-withdrawal-rate-select { + @include select-arrow(variables.$dark-primary-text); + + appearance: none; background-color: transparent; + background-position: right 0 center; + background-repeat: no-repeat; color: rgb(var(--dark-primary-text)); + padding: 0 0.75rem 0 0.25rem; &:focus { box-shadow: none; @@ -14,6 +26,8 @@ :host-context(.theme-dark) { .safe-withdrawal-rate-select { + @include select-arrow(variables.$light-primary-text); + color: rgb(var(--light-primary-text)); } } diff --git a/apps/client/src/app/pages/register/register-page.component.ts b/apps/client/src/app/pages/register/register-page.component.ts index acf3c40eb..78162487d 100644 --- a/apps/client/src/app/pages/register/register-page.component.ts +++ b/apps/client/src/app/pages/register/register-page.component.ts @@ -1,5 +1,4 @@ import { DataService } from '@ghostfolio/client/services/data.service'; -import { InternetIdentityService } from '@ghostfolio/client/services/internet-identity.service'; import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service'; import { InfoItem, LineChartItem } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; @@ -43,7 +42,6 @@ export class GfRegisterPageComponent implements OnDestroy, OnInit { private dataService: DataService, private deviceService: DeviceDetectorService, private dialog: MatDialog, - private internetIdentityService: InternetIdentityService, private router: Router, private tokenStorageService: TokenStorageService ) { @@ -73,16 +71,6 @@ export class GfRegisterPageComponent implements OnDestroy, OnInit { ); } - public async onLoginWithInternetIdentity() { - try { - const { authToken } = await this.internetIdentityService.login(); - - this.tokenStorageService.saveToken(authToken); - - await this.router.navigate(['/']); - } catch {} - } - public openShowAccessTokenDialog() { const dialogRef = this.dialog.open< GfUserAccountRegistrationDialogComponent, diff --git a/apps/client/src/app/pages/register/register-page.html b/apps/client/src/app/pages/register/register-page.html index eee49083a..de53777fa 100644 --- a/apps/client/src/app/pages/register/register-page.html +++ b/apps/client/src/app/pages/register/register-page.html @@ -28,20 +28,6 @@ @if (hasPermissionForSocialLogin) {
or
- @if (false) { - - }
('/api/v1/admin/user', { params }); + return this.http.get('/api/v1/admin/user', { params }); } public gather7Days() { diff --git a/apps/client/src/app/services/internet-identity.service.ts b/apps/client/src/app/services/internet-identity.service.ts deleted file mode 100644 index 30ae13679..000000000 --- a/apps/client/src/app/services/internet-identity.service.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { OAuthResponse } from '@ghostfolio/common/interfaces'; - -import { HttpClient } from '@angular/common/http'; -import { Injectable, OnDestroy } from '@angular/core'; -import { AuthClient } from '@dfinity/auth-client'; -import { EMPTY, Subject } from 'rxjs'; -import { catchError, takeUntil } from 'rxjs/operators'; - -@Injectable({ - providedIn: 'root' -}) -export class InternetIdentityService implements OnDestroy { - private unsubscribeSubject = new Subject(); - - public constructor(private http: HttpClient) {} - - public async login(): Promise { - const authClient = await AuthClient.create({ - idleOptions: { - disableDefaultIdleCallback: true, - disableIdle: true - } - }); - - return new Promise((resolve, reject) => { - authClient.login({ - onError: async () => { - return reject(); - }, - onSuccess: () => { - const principalId = authClient.getIdentity().getPrincipal(); - - this.http - .post(`/api/v1/auth/internet-identity`, { - principalId: principalId.toText() - }) - .pipe( - catchError(() => { - reject(); - return EMPTY; - }), - takeUntil(this.unsubscribeSubject) - ) - .subscribe((response) => { - resolve(response); - }); - } - }); - }); - } - - public ngOnDestroy() { - this.unsubscribeSubject.next(); - this.unsubscribeSubject.complete(); - } -} diff --git a/apps/client/src/assets/icons/internet-computer.svg b/apps/client/src/assets/icons/internet-computer.svg deleted file mode 100644 index 6a1bf6c86..000000000 --- a/apps/client/src/assets/icons/internet-computer.svg +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/apps/client/src/styles/theme.scss b/apps/client/src/styles/theme.scss index fe9fd44a5..8dd6d8e36 100644 --- a/apps/client/src/styles/theme.scss +++ b/apps/client/src/styles/theme.scss @@ -1,9 +1,6 @@ @use '@angular/material' as mat; -$dark-primary-text: rgba(black, 0.87); -$light-primary-text: white; - -$mat-css-dark-theme-selector: '.theme-dark'; +@use './variables.scss' as variables; $gf-primary: ( 50: var(--gf-theme-primary-50), @@ -21,20 +18,20 @@ $gf-primary: ( A400: var(--gf-theme-primary-A400), A700: var(--gf-theme-primary-A700), contrast: ( - 50: $dark-primary-text, - 100: $dark-primary-text, - 200: $dark-primary-text, - 300: $light-primary-text, - 400: $light-primary-text, - 500: $light-primary-text, - 600: $light-primary-text, - 700: $light-primary-text, - 800: $light-primary-text, - 900: $light-primary-text, - A100: $dark-primary-text, - A200: $light-primary-text, - A400: $light-primary-text, - A700: $light-primary-text + 50: variables.$dark-primary-text, + 100: variables.$dark-primary-text, + 200: variables.$dark-primary-text, + 300: variables.$light-primary-text, + 400: variables.$light-primary-text, + 500: variables.$light-primary-text, + 600: variables.$light-primary-text, + 700: variables.$light-primary-text, + 800: variables.$light-primary-text, + 900: variables.$light-primary-text, + A100: variables.$dark-primary-text, + A200: variables.$light-primary-text, + A400: variables.$light-primary-text, + A700: variables.$light-primary-text ) ); @@ -54,20 +51,20 @@ $gf-secondary: ( A400: var(--gf-theme-secondary-A400), A700: var(--gf-theme-secondary-A700), contrast: ( - 50: $dark-primary-text, - 100: $dark-primary-text, - 200: $dark-primary-text, - 300: $light-primary-text, - 400: $light-primary-text, - 500: $light-primary-text, - 600: $light-primary-text, - 700: $light-primary-text, - 800: $light-primary-text, - 900: $light-primary-text, - A100: $dark-primary-text, - A200: $light-primary-text, - A400: $light-primary-text, - A700: $light-primary-text + 50: variables.$dark-primary-text, + 100: variables.$dark-primary-text, + 200: variables.$dark-primary-text, + 300: variables.$light-primary-text, + 400: variables.$light-primary-text, + 500: variables.$light-primary-text, + 600: variables.$light-primary-text, + 700: variables.$light-primary-text, + 800: variables.$light-primary-text, + 900: variables.$light-primary-text, + A100: variables.$dark-primary-text, + A200: variables.$light-primary-text, + A400: variables.$light-primary-text, + A700: variables.$light-primary-text ) ); diff --git a/apps/client/src/styles/variables.scss b/apps/client/src/styles/variables.scss new file mode 100644 index 000000000..dcf26eecc --- /dev/null +++ b/apps/client/src/styles/variables.scss @@ -0,0 +1,4 @@ +$dark-primary-text: rgba(black, 0.87); +$light-primary-text: white; + +$mat-css-dark-theme-selector: '.theme-dark'; diff --git a/eslint.config.cjs b/eslint.config.cjs index c7e08821c..a88d0cc85 100644 --- a/eslint.config.cjs +++ b/eslint.config.cjs @@ -152,7 +152,6 @@ module.exports = [ // The following rules are part of eslint:recommended // and can be remove once solved - 'no-constant-binary-expression': 'warn', 'no-loss-of-precision': 'warn', // The following rules are part of @typescript-eslint/recommended-type-checked @@ -170,7 +169,6 @@ module.exports = [ '@typescript-eslint/no-unsafe-argument': 'warn', '@typescript-eslint/no-unsafe-assignment': 'warn', '@typescript-eslint/no-unsafe-enum-comparison': 'warn', - '@typescript-eslint/no-unsafe-function-type': 'warn', '@typescript-eslint/no-unsafe-member-access': 'warn', '@typescript-eslint/no-unsafe-return': 'warn', '@typescript-eslint/no-unsafe-call': 'warn', @@ -189,8 +187,7 @@ module.exports = [ // The following rules are part of @typescript-eslint/stylistic-type-checked // and can be remove once solved - '@typescript-eslint/prefer-nullish-coalescing': 'warn', // TODO: Requires strictNullChecks: true - '@typescript-eslint/prefer-regexp-exec': 'warn' + '@typescript-eslint/prefer-nullish-coalescing': 'warn' // TODO: Requires strictNullChecks: true } })) ]; diff --git a/libs/common/src/lib/helper.ts b/libs/common/src/lib/helper.ts index 97b762267..7452b604c 100644 --- a/libs/common/src/lib/helper.ts +++ b/libs/common/src/lib/helper.ts @@ -375,7 +375,7 @@ export function parseDate(date: string): Date { // Transform 'yyyyMMdd' format to supported format by parse function if (date?.length === 8) { - const match = date.match(/^(\d{4})(\d{2})(\d{2})$/); + const match = /^(\d{4})(\d{2})(\d{2})$/.exec(date); if (match) { const [, year, month, day] = match; diff --git a/libs/common/src/lib/interfaces/index.ts b/libs/common/src/lib/interfaces/index.ts index eac5db68c..06ecf32e8 100644 --- a/libs/common/src/lib/interfaces/index.ts +++ b/libs/common/src/lib/interfaces/index.ts @@ -7,7 +7,6 @@ import type { AdminMarketData, AdminMarketDataItem } from './admin-market-data.interface'; -import type { AdminUsers } from './admin-users.interface'; import type { AssetClassSelectorOption } from './asset-class-selector-option.interface'; import type { AssetProfileIdentifier } from './asset-profile-identifier.interface'; import type { BenchmarkProperty } from './benchmark-property.interface'; @@ -39,6 +38,7 @@ import type { AccountBalancesResponse } from './responses/account-balances-respo import type { AccountsResponse } from './responses/accounts-response.interface'; import type { ActivitiesResponse } from './responses/activities-response.interface'; import type { ActivityResponse } from './responses/activity-response.interface'; +import type { AdminUsersResponse } from './responses/admin-users-response.interface'; import type { AiPromptResponse } from './responses/ai-prompt-response.interface'; import type { ApiKeyResponse } from './responses/api-key-response.interface'; import type { AssetResponse } from './responses/asset-response.interface'; @@ -92,7 +92,7 @@ export { AdminMarketData, AdminMarketDataDetails, AdminMarketDataItem, - AdminUsers, + AdminUsersResponse, AiPromptResponse, ApiKeyResponse, AssetClassSelectorOption, diff --git a/libs/common/src/lib/interfaces/admin-users.interface.ts b/libs/common/src/lib/interfaces/responses/admin-users-response.interface.ts similarity index 88% rename from libs/common/src/lib/interfaces/admin-users.interface.ts rename to libs/common/src/lib/interfaces/responses/admin-users-response.interface.ts index 79031425a..d9f58ee18 100644 --- a/libs/common/src/lib/interfaces/admin-users.interface.ts +++ b/libs/common/src/lib/interfaces/responses/admin-users-response.interface.ts @@ -1,6 +1,6 @@ import { Role } from '@prisma/client'; -export interface AdminUsers { +export interface AdminUsersResponse { count: number; users: { accountCount: number; diff --git a/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts b/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts index df7ca79fa..44276ec43 100644 --- a/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts +++ b/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts @@ -185,7 +185,7 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy { 'principalInvestmentAmount' ).value, projectedTotalAmount: - Number(this.getProjectedTotalAmount().toFixed(0)) ?? 0, + Math.round(this.getProjectedTotalAmount()) || 0, retirementDate: this.getRetirementDate() ?? this.DEFAULT_RETIREMENT_DATE }, diff --git a/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts b/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts index 3f062a374..2d8a03ac0 100644 --- a/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts +++ b/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts @@ -31,6 +31,7 @@ import ChartDataLabels from 'chartjs-plugin-datalabels'; import { isUUID } from 'class-validator'; import Color from 'color'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; +import OpenColor from 'open-color'; import { translate } from '../i18n'; @@ -47,7 +48,7 @@ const { teal, violet, yellow -} = require('open-color'); +} = OpenColor; @Component({ changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts b/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts index 4e06d49cc..6ae958b83 100644 --- a/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts +++ b/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts @@ -33,10 +33,11 @@ import { isUUID } from 'class-validator'; import { differenceInDays, max } from 'date-fns'; import { orderBy } from 'lodash'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; +import OpenColor from 'open-color'; import { GetColorParams } from './interfaces/interfaces'; -const { gray, green, red } = require('open-color'); +const { gray, green, red } = OpenColor; @Component({ changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/package-lock.json b/package-lock.json index b306692a3..6429912bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ghostfolio", - "version": "2.213.0", + "version": "2.214.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ghostfolio", - "version": "2.213.0", + "version": "2.214.0", "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { @@ -23,11 +23,6 @@ "@angular/service-worker": "20.2.4", "@codewithdan/observable-store": "2.2.15", "@date-fns/utc": "2.1.0", - "@dfinity/agent": "0.15.7", - "@dfinity/auth-client": "0.15.7", - "@dfinity/candid": "0.15.7", - "@dfinity/identity": "0.15.7", - "@dfinity/principal": "0.15.7", "@internationalized/number": "3.6.3", "@ionic/angular": "8.7.3", "@keyv/redis": "4.4.0", @@ -62,7 +57,7 @@ "class-validator": "0.14.2", "color": "5.0.0", "countries-and-timezones": "3.8.0", - "countries-list": "3.1.1", + "countries-list": "3.2.0", "countup.js": "2.9.0", "date-fns": "4.1.0", "dotenv": "17.2.3", @@ -77,7 +72,7 @@ "lodash": "4.17.21", "marked": "15.0.4", "ms": "3.0.0-canary.1", - "ng-extract-i18n-merge": "3.0.0", + "ng-extract-i18n-merge": "3.1.0", "ngx-device-detector": "10.1.0", "ngx-markdown": "20.0.0", "ngx-skeleton-loader": "11.3.0", @@ -93,7 +88,7 @@ "stripe": "18.5.0", "svgmap": "2.12.2", "tablemark": "4.1.0", - "twitter-api-v2": "1.23.0", + "twitter-api-v2": "1.27.0", "uuid": "11.1.0", "yahoo-finance2": "3.10.0", "zone.js": "0.15.1" @@ -4713,6 +4708,7 @@ "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "0.3.9" @@ -4725,6 +4721,7 @@ "version": "0.3.9", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", @@ -4953,73 +4950,6 @@ "node": "^16.13.0 || >=18.0.0" } }, - "node_modules/@dfinity/agent": { - "version": "0.15.7", - "resolved": "https://registry.npmjs.org/@dfinity/agent/-/agent-0.15.7.tgz", - "integrity": "sha512-w34yvlUTpPBG8nLOD0t/ao3k2xonOFq4QGvfJ1HiS/nIggdza/3xC3nLBszGrjVYWj1jqu8BLFvQXCAeWin75A==", - "license": "Apache-2.0", - "dependencies": { - "base64-arraybuffer": "^0.2.0", - "bignumber.js": "^9.0.0", - "borc": "^2.1.1", - "js-sha256": "0.9.0", - "simple-cbor": "^0.4.1", - "ts-node": "^10.8.2" - }, - "peerDependencies": { - "@dfinity/candid": "^0.15.7", - "@dfinity/principal": "^0.15.7" - } - }, - "node_modules/@dfinity/auth-client": { - "version": "0.15.7", - "resolved": "https://registry.npmjs.org/@dfinity/auth-client/-/auth-client-0.15.7.tgz", - "integrity": "sha512-f6cRqXayCf+7+9gNcDnAZZwJrgBYKIzfxjxeRLlpsueQeo+E/BX2yVSANxzTkCNc4U3p+ttHI1RNtasLunYTcA==", - "license": "Apache-2.0", - "dependencies": { - "idb": "^7.0.2" - }, - "peerDependencies": { - "@dfinity/agent": "^0.15.7", - "@dfinity/identity": "^0.15.7", - "@dfinity/principal": "^0.15.7" - } - }, - "node_modules/@dfinity/candid": { - "version": "0.15.7", - "resolved": "https://registry.npmjs.org/@dfinity/candid/-/candid-0.15.7.tgz", - "integrity": "sha512-lTcjK/xrSyT7wvUQ2pApG+yklQAwxaofQ04D1IWv0/8gKbY0eUbh8G2w6+CypJ15Hb1CH24ijUj8nWdeX/z3jg==", - "license": "Apache-2.0", - "dependencies": { - "ts-node": "^10.8.2" - } - }, - "node_modules/@dfinity/identity": { - "version": "0.15.7", - "resolved": "https://registry.npmjs.org/@dfinity/identity/-/identity-0.15.7.tgz", - "integrity": "sha512-kBAkx9wq78jSQf6T5aayLyWm8YgtOZw8bW6+OuzX6tR3hkAEa85A9TcKA7BjkmMWSIskjEDVQub4fFfKWS2vOQ==", - "license": "Apache-2.0", - "dependencies": { - "borc": "^2.1.1", - "js-sha256": "^0.9.0", - "tweetnacl": "^1.0.1" - }, - "peerDependencies": { - "@dfinity/agent": "^0.15.7", - "@dfinity/principal": "^0.15.7", - "@peculiar/webcrypto": "^1.4.0" - } - }, - "node_modules/@dfinity/principal": { - "version": "0.15.7", - "resolved": "https://registry.npmjs.org/@dfinity/principal/-/principal-0.15.7.tgz", - "integrity": "sha512-6/AkYzpGEH6Jw/0+B/EeeQn+5u2GDDvRLt1kQPhIG4txQYFnOy04H3VvyrymmfAj6/CXUgrOrux6OxgYSLYVJg==", - "license": "Apache-2.0", - "dependencies": { - "js-sha256": "^0.9.0", - "ts-node": "^10.8.2" - } - }, "node_modules/@discoveryjs/json-ext": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz", @@ -11908,36 +11838,6 @@ "tslib": "^2.8.1" } }, - "node_modules/@peculiar/json-schema": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/@peculiar/json-schema/-/json-schema-1.1.12.tgz", - "integrity": "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==", - "license": "MIT", - "peer": true, - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@peculiar/webcrypto": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@peculiar/webcrypto/-/webcrypto-1.5.0.tgz", - "integrity": "sha512-BRs5XUAwiyCDQMsVA9IDvDa7UBR9gAvPHgugOeGng3YN6vJ9JYonyDc0lNczErgtCWtucjR5N7VtaonboD/ezg==", - "license": "MIT", - "peer": true, - "dependencies": { - "@peculiar/asn1-schema": "^2.3.8", - "@peculiar/json-schema": "^1.1.12", - "pvtsutils": "^1.3.5", - "tslib": "^2.6.2", - "webcrypto-core": "^1.8.0" - }, - "engines": { - "node": ">=10.12.0" - } - }, "node_modules/@phenomnomnominal/tsquery": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@phenomnomnominal/tsquery/-/tsquery-5.0.1.tgz", @@ -13754,24 +13654,28 @@ "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, "license": "MIT" }, "node_modules/@tsconfig/node12": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, "license": "MIT" }, "node_modules/@tsconfig/node14": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, "license": "MIT" }, "node_modules/@tsconfig/node16": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, "license": "MIT" }, "node_modules/@tufjs/canonical-json": { @@ -15686,6 +15590,7 @@ "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "devOptional": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -15732,6 +15637,7 @@ "version": "8.3.4", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, "license": "MIT", "dependencies": { "acorn": "^8.11.0" @@ -16057,6 +15963,7 @@ "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, "license": "MIT" }, "node_modules/argparse": { @@ -16709,14 +16616,6 @@ "dev": true, "license": "MIT" }, - "node_modules/base64-arraybuffer": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.2.0.tgz", - "integrity": "sha512-7emyCsu1/xiBXgQZrscw/8KPRT44I4Yq9Pe6EGs3aPRTsWuggML1/1DTuZUuIaJPIm1FTDUVXl4x/yW8s0kQDQ==", - "engines": { - "node": ">= 0.6.0" - } - }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -17040,30 +16939,6 @@ "popper.js": "^1.16.1" } }, - "node_modules/borc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/borc/-/borc-2.1.2.tgz", - "integrity": "sha512-Sy9eoUi4OiKzq7VovMn246iTo17kzuyHJKomCfpWMlI6RpfN1gk95w7d7gH264nApVLg0HZfcpz62/g4VH1Y4w==", - "license": "MIT", - "dependencies": { - "bignumber.js": "^9.0.0", - "buffer": "^5.5.0", - "commander": "^2.15.0", - "ieee754": "^1.1.13", - "iso-url": "~0.4.7", - "json-text-sequence": "~0.1.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/borc/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "license": "MIT" - }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -17160,6 +17035,7 @@ "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, "funding": [ { "type": "github", @@ -18647,9 +18523,9 @@ } }, "node_modules/countries-list": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/countries-list/-/countries-list-3.1.1.tgz", - "integrity": "sha512-nPklKJ5qtmY5MdBKw1NiBAoyx5Sa7p2yPpljZyQ7gyCN1m+eMFs9I6CT37Mxt8zvR5L3VzD3DJBE4WQzX3WF4A==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/countries-list/-/countries-list-3.2.0.tgz", + "integrity": "sha512-HYHAo2fwEsG3TmbsNdVmIQPHizRlqeYMTtLEAl0IANG/3jRYX7p3NR6VapDqKP0n60TmsRy1dyRjVN5JbywDbA==", "license": "MIT" }, "node_modules/countup.js": { @@ -19325,6 +19201,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, "license": "MIT" }, "node_modules/cron": { @@ -20808,12 +20685,6 @@ "dev": true, "license": "MIT" }, - "node_modules/delimit-stream": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/delimit-stream/-/delimit-stream-0.1.0.tgz", - "integrity": "sha512-a02fiQ7poS5CnjiJBAsjGLPp5EwVoGHNeu9sziBd9huppRfsAFIpv5zNLv0V1gbop53ilngAf5Kf331AwcoRBQ==", - "license": "BSD-2-Clause" - }, "node_modules/denque": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", @@ -20908,6 +20779,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" @@ -24840,12 +24712,6 @@ "postcss": "^8.1.0" } }, - "node_modules/idb": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", - "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", - "license": "ISC" - }, "node_modules/identity-obj-proxy": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz", @@ -25837,15 +25703,6 @@ "dev": true, "license": "ISC" }, - "node_modules/iso-url": { - "version": "0.4.7", - "resolved": "https://registry.npmjs.org/iso-url/-/iso-url-0.4.7.tgz", - "integrity": "sha512-27fFRDnPAMnHGLq36bWTpKET+eiXct3ENlCcdcMdk+mjXrb2kw3mhBUg1B7ewAC0kVzlOPhADzQgz1SE6Tglog==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, "node_modules/isobject": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", @@ -30098,12 +29955,6 @@ "license": "MIT", "peer": true }, - "node_modules/js-sha256": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz", - "integrity": "sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA==", - "license": "MIT" - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -30316,15 +30167,6 @@ "dev": true, "license": "ISC" }, - "node_modules/json-text-sequence": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/json-text-sequence/-/json-text-sequence-0.1.1.tgz", - "integrity": "sha512-L3mEegEWHRekSHjc7+sc8eJhba9Clq1PZ8kMkzf8OxElhXc8O4TS5MwcVlj9aEbm5dr81N90WHC5nAz3UO971w==", - "license": "MIT", - "dependencies": { - "delimit-stream": "0.1.0" - } - }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -32083,6 +31925,7 @@ "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, "license": "ISC" }, "node_modules/make-fetch-happen": { @@ -32780,9 +32623,9 @@ "license": "MIT" }, "node_modules/ng-extract-i18n-merge": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ng-extract-i18n-merge/-/ng-extract-i18n-merge-3.0.0.tgz", - "integrity": "sha512-vTWtAz6a/wVYxnUMFHp1ur6o4JSLm+LcxdSMV8o8Ml2p5oCsSB4iFd5E6h8Yb8X8D596qyBz0ELgiDmbn4YyRQ==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/ng-extract-i18n-merge/-/ng-extract-i18n-merge-3.1.0.tgz", + "integrity": "sha512-4rJRcpTcP54xf5cjoz3S1By0T04X2RoyQcMDxr4wLdRx3fVxkeP8jeuLzmj9F4G5n0yMQb+6jhUiFERxpkfs1w==", "license": "MIT", "dependencies": { "@angular-devkit/architect": "^0.2000.0", @@ -38081,12 +37924,6 @@ "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/simple-cbor": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/simple-cbor/-/simple-cbor-0.4.1.tgz", - "integrity": "sha512-rijcxtwx2b4Bje3sqeIqw5EeW7UlOIC4YfOdwqIKacpvRQ/D78bWg/4/0m5e0U91oKvlGh7LlJuZCu07ISCC7w==", - "license": "ISC" - }, "node_modules/sirv": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", @@ -40042,6 +39879,7 @@ "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, "license": "MIT", "dependencies": { "@cspotcode/source-map-support": "^0.8.0", @@ -40219,16 +40057,10 @@ "node": "*" } }, - "node_modules/tweetnacl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", - "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", - "license": "Unlicense" - }, "node_modules/twitter-api-v2": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/twitter-api-v2/-/twitter-api-v2-1.23.0.tgz", - "integrity": "sha512-5i1agETVpTuY68Zuk9i2B3N9wHzc4JIWw0WKyG4CEaFv9mRKmU87roa+U1oYYXTChWb0HMcqfkwoBJHYmLbeDA==", + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/twitter-api-v2/-/twitter-api-v2-1.27.0.tgz", + "integrity": "sha512-hbIFwzg0NeOcFOdmJqtKMCXjLjc0INff/7NwhnZ2zpnw65oku8i+0eMxo5M0iTc1hs+inD/IpDw3S0Xh2c45QQ==", "license": "Apache-2.0" }, "node_modules/type-check": { @@ -40795,6 +40627,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, "license": "MIT" }, "node_modules/v8-to-istanbul": { @@ -41110,20 +40943,6 @@ "license": "MIT", "optional": true }, - "node_modules/webcrypto-core": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-1.8.1.tgz", - "integrity": "sha512-P+x1MvlNCXlKbLSOY4cYrdreqPG5hbzkmawbcXLKN/mf6DZW0SdNNkZ+sjwsqVkI4A4Ko2sPZmkZtCKY58w83A==", - "license": "MIT", - "peer": true, - "dependencies": { - "@peculiar/asn1-schema": "^2.3.13", - "@peculiar/json-schema": "^1.1.12", - "asn1js": "^3.0.5", - "pvtsutils": "^1.3.5", - "tslib": "^2.7.0" - } - }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", @@ -42424,6 +42243,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" diff --git a/package.json b/package.json index cbbb12652..7648cee02 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "2.213.0", + "version": "2.214.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "repository": "https://github.com/ghostfolio/ghostfolio", @@ -69,11 +69,6 @@ "@angular/service-worker": "20.2.4", "@codewithdan/observable-store": "2.2.15", "@date-fns/utc": "2.1.0", - "@dfinity/agent": "0.15.7", - "@dfinity/auth-client": "0.15.7", - "@dfinity/candid": "0.15.7", - "@dfinity/identity": "0.15.7", - "@dfinity/principal": "0.15.7", "@internationalized/number": "3.6.3", "@ionic/angular": "8.7.3", "@keyv/redis": "4.4.0", @@ -108,7 +103,7 @@ "class-validator": "0.14.2", "color": "5.0.0", "countries-and-timezones": "3.8.0", - "countries-list": "3.1.1", + "countries-list": "3.2.0", "countup.js": "2.9.0", "date-fns": "4.1.0", "dotenv": "17.2.3", @@ -123,7 +118,7 @@ "lodash": "4.17.21", "marked": "15.0.4", "ms": "3.0.0-canary.1", - "ng-extract-i18n-merge": "3.0.0", + "ng-extract-i18n-merge": "3.1.0", "ngx-device-detector": "10.1.0", "ngx-markdown": "20.0.0", "ngx-skeleton-loader": "11.3.0", @@ -139,7 +134,7 @@ "stripe": "18.5.0", "svgmap": "2.12.2", "tablemark": "4.1.0", - "twitter-api-v2": "1.23.0", + "twitter-api-v2": "1.27.0", "uuid": "11.1.0", "yahoo-finance2": "3.10.0", "zone.js": "0.15.1" diff --git a/test/import/not-ok/invalid-data-source.json b/test/import/not-ok/invalid-data-source.json index 472e295ee..f8e920c67 100644 --- a/test/import/not-ok/invalid-data-source.json +++ b/test/import/not-ok/invalid-data-source.json @@ -14,5 +14,11 @@ "type": "BUY", "unitPrice": 100.0 } - ] + ], + "user": { + "settings": { + "currency": "USD", + "performanceCalculationType": "ROAI" + } + } } diff --git a/test/import/not-ok/invalid-date-before-min.json b/test/import/not-ok/invalid-date-before-min.json index 3040581b2..260d79166 100644 --- a/test/import/not-ok/invalid-date-before-min.json +++ b/test/import/not-ok/invalid-date-before-min.json @@ -14,5 +14,11 @@ "type": "BUY", "unitPrice": 100.0 } - ] + ], + "user": { + "settings": { + "currency": "USD", + "performanceCalculationType": "ROAI" + } + } } diff --git a/test/import/not-ok/invalid-date.json b/test/import/not-ok/invalid-date.json index 99cd6d156..4522c6dcc 100644 --- a/test/import/not-ok/invalid-date.json +++ b/test/import/not-ok/invalid-date.json @@ -14,5 +14,11 @@ "type": "BUY", "unitPrice": 100.0 } - ] + ], + "user": { + "settings": { + "currency": "USD", + "performanceCalculationType": "ROAI" + } + } } diff --git a/test/import/not-ok/invalid-symbol.json b/test/import/not-ok/invalid-symbol.json index 14f0051ec..0bbbf53db 100644 --- a/test/import/not-ok/invalid-symbol.json +++ b/test/import/not-ok/invalid-symbol.json @@ -14,5 +14,11 @@ "type": "BUY", "unitPrice": 100.0 } - ] + ], + "user": { + "settings": { + "currency": "USD", + "performanceCalculationType": "ROAI" + } + } } diff --git a/test/import/not-ok/invalid-type.json b/test/import/not-ok/invalid-type.json index a23f72411..a8967d81a 100644 --- a/test/import/not-ok/invalid-type.json +++ b/test/import/not-ok/invalid-type.json @@ -14,5 +14,11 @@ "type": "", "unitPrice": 100.0 } - ] + ], + "user": { + "settings": { + "currency": "USD", + "performanceCalculationType": "ROAI" + } + } } diff --git a/test/import/not-ok/unavailable-exchange-rate.json b/test/import/not-ok/unavailable-exchange-rate.json index 4d8be156a..66c7044d7 100644 --- a/test/import/not-ok/unavailable-exchange-rate.json +++ b/test/import/not-ok/unavailable-exchange-rate.json @@ -15,5 +15,11 @@ "date": "1990-01-01T00:00:00.000Z", "symbol": "MSFT" } - ] + ], + "user": { + "settings": { + "currency": "USD", + "performanceCalculationType": "ROAI" + } + } } diff --git a/test/import/ok/500-activities.json b/test/import/ok/500-activities.json index b691a6f9f..2793c695e 100644 --- a/test/import/ok/500-activities.json +++ b/test/import/ok/500-activities.json @@ -6019,7 +6019,8 @@ ], "user": { "settings": { - "currency": "USD" + "currency": "USD", + "performanceCalculationType": "ROAI" } } } diff --git a/test/import/ok/btceur.json b/test/import/ok/btceur.json index b370682f9..ae9eb8921 100644 --- a/test/import/ok/btceur.json +++ b/test/import/ok/btceur.json @@ -23,7 +23,8 @@ ], "user": { "settings": { - "currency": "USD" + "currency": "USD", + "performanceCalculationType": "ROAI" } } } diff --git a/test/import/ok/btcusd-short.json b/test/import/ok/btcusd-short.json index bc4152de9..6f25a7740 100644 --- a/test/import/ok/btcusd-short.json +++ b/test/import/ok/btcusd-short.json @@ -36,7 +36,8 @@ ], "user": { "settings": { - "currency": "USD" + "currency": "USD", + "performanceCalculationType": "ROAI" } } } diff --git a/test/import/ok/btcusd.json b/test/import/ok/btcusd.json index fc2e1f66e..4a85f967e 100644 --- a/test/import/ok/btcusd.json +++ b/test/import/ok/btcusd.json @@ -23,7 +23,8 @@ ], "user": { "settings": { - "currency": "USD" + "currency": "USD", + "performanceCalculationType": "ROAI" } } } diff --git a/test/import/ok/derived-currency.json b/test/import/ok/derived-currency.json index e740c1ae3..637ab21b6 100644 --- a/test/import/ok/derived-currency.json +++ b/test/import/ok/derived-currency.json @@ -31,7 +31,8 @@ ], "user": { "settings": { - "currency": "USD" + "currency": "USD", + "performanceCalculationType": "ROAI" } } } diff --git a/test/import/ok/novn-buy-and-sell-partially.json b/test/import/ok/novn-buy-and-sell-partially.json index 8c5778566..3bdd7eb7e 100644 --- a/test/import/ok/novn-buy-and-sell-partially.json +++ b/test/import/ok/novn-buy-and-sell-partially.json @@ -27,7 +27,8 @@ ], "user": { "settings": { - "currency": "CHF" + "currency": "CHF", + "performanceCalculationType": "ROAI" } } } diff --git a/test/import/ok/novn-buy-and-sell.json b/test/import/ok/novn-buy-and-sell.json index 71ee9b7a9..6ae519d87 100644 --- a/test/import/ok/novn-buy-and-sell.json +++ b/test/import/ok/novn-buy-and-sell.json @@ -27,7 +27,8 @@ ], "user": { "settings": { - "currency": "CHF" + "currency": "CHF", + "performanceCalculationType": "ROAI" } } } diff --git a/test/import/ok/penthouse-apartment.json b/test/import/ok/penthouse-apartment.json index 2bc7f0cf8..0c35521e6 100644 --- a/test/import/ok/penthouse-apartment.json +++ b/test/import/ok/penthouse-apartment.json @@ -47,7 +47,8 @@ ], "user": { "settings": { - "currency": "USD" + "currency": "USD", + "performanceCalculationType": "ROAI" } } } diff --git a/test/import/ok/sample.json b/test/import/ok/sample.json index 21277129f..bc2798718 100644 --- a/test/import/ok/sample.json +++ b/test/import/ok/sample.json @@ -147,7 +147,8 @@ ], "user": { "settings": { - "currency": "USD" + "currency": "USD", + "performanceCalculationType": "ROAI" } } } diff --git a/test/import/ok/vti-buy-long-history.json b/test/import/ok/vti-buy-long-history.json index c8cd25e60..88b38d2b1 100644 --- a/test/import/ok/vti-buy-long-history.json +++ b/test/import/ok/vti-buy-long-history.json @@ -40,7 +40,8 @@ ], "user": { "settings": { - "currency": "USD" + "currency": "USD", + "performanceCalculationType": "ROAI" } } } diff --git a/test/import/ok/without-accounts.json b/test/import/ok/without-accounts.json index 8a24f86fc..2283dd889 100644 --- a/test/import/ok/without-accounts.json +++ b/test/import/ok/without-accounts.json @@ -47,7 +47,8 @@ ], "user": { "settings": { - "currency": "USD" + "currency": "USD", + "performanceCalculationType": "ROAI" } } }