Browse Source

Merge branch 'main' into pr/5786

pull/5786/head
Thomas Kaul 2 weeks ago
parent
commit
a6e43c1436
  1. 14
      .config/prisma.ts
  2. 42
      CHANGELOG.md
  3. 4
      Dockerfile
  4. 2
      apps/api/src/app/admin/admin.controller.ts
  5. 1
      apps/api/src/app/app.module.ts
  6. 4
      apps/api/src/app/asset/asset.controller.ts
  7. 66
      apps/api/src/app/endpoints/ai/ai.service.ts
  8. 6
      apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.service.ts
  9. 4
      apps/api/src/app/exchange-rate/exchange-rate.controller.ts
  10. 8
      apps/api/src/app/export/export.controller.ts
  11. 21
      apps/api/src/app/export/export.service.ts
  12. 12
      apps/api/src/app/import/import.service.ts
  13. 4
      apps/api/src/app/info/info.controller.ts
  14. 3
      apps/api/src/app/order/create-order.dto.ts
  15. 5
      apps/api/src/app/order/interfaces/activities.interface.ts
  16. 9
      apps/api/src/app/order/order.controller.ts
  17. 9
      apps/api/src/app/order/order.service.ts
  18. 4
      apps/api/src/app/portfolio/calculator/portfolio-calculator-test-utils.ts
  19. 4
      apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
  20. 3
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-buy.spec.ts
  21. 3
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts
  22. 3
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell.spec.ts
  23. 3
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy.spec.ts
  24. 7
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur.spec.ts
  25. 4
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts
  26. 7
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-short.spec.ts
  27. 7
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd.spec.ts
  28. 3
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-fee.spec.ts
  29. 4
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-googl-buy.spec.ts
  30. 3
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-liability.spec.ts
  31. 2
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-and-sell.spec.ts
  32. 3
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-with-dividend.spec.ts
  33. 3
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-no-orders.spec.ts
  34. 7
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell-partially.spec.ts
  35. 7
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell.spec.ts
  36. 3
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-valuable.spec.ts
  37. 4
      apps/api/src/app/portfolio/interfaces/get-values-params.interface.ts
  38. 11
      apps/api/src/app/subscription/subscription.controller.ts
  39. 62
      apps/api/src/app/subscription/subscription.service.ts
  40. 4
      apps/api/src/app/symbol/symbol.controller.ts
  41. 10
      apps/api/src/app/symbol/symbol.service.ts
  42. 3
      apps/api/src/dependencies.ts
  43. 1
      apps/api/src/models/interfaces/rule-settings.interface.ts
  44. 7
      apps/api/src/models/rules/account-cluster-risk/current-investment.ts
  45. 3
      apps/api/src/models/rules/account-cluster-risk/single-account.ts
  46. 7
      apps/api/src/models/rules/asset-class-cluster-risk/equity.ts
  47. 7
      apps/api/src/models/rules/asset-class-cluster-risk/fixed-income.ts
  48. 7
      apps/api/src/models/rules/currency-cluster-risk/base-currency-current-investment.ts
  49. 7
      apps/api/src/models/rules/currency-cluster-risk/current-investment.ts
  50. 7
      apps/api/src/models/rules/economic-market-cluster-risk/developed-markets.ts
  51. 7
      apps/api/src/models/rules/economic-market-cluster-risk/emerging-markets.ts
  52. 7
      apps/api/src/models/rules/emergency-fund/emergency-fund-setup.ts
  53. 7
      apps/api/src/models/rules/fees/fee-ratio-initial-investment.ts
  54. 15
      apps/api/src/models/rules/liquidity/buying-power.ts
  55. 7
      apps/api/src/models/rules/regional-market-cluster-risk/asia-pacific.ts
  56. 7
      apps/api/src/models/rules/regional-market-cluster-risk/emerging-markets.ts
  57. 7
      apps/api/src/models/rules/regional-market-cluster-risk/europe.ts
  58. 7
      apps/api/src/models/rules/regional-market-cluster-risk/japan.ts
  59. 7
      apps/api/src/models/rules/regional-market-cluster-risk/north-america.ts
  60. 14
      apps/api/src/services/data-provider/alpha-vantage/alpha-vantage.service.ts
  61. 2
      apps/api/src/services/data-provider/alpha-vantage/interfaces/interfaces.ts
  62. 12
      apps/api/src/services/data-provider/coingecko/coingecko.service.ts
  63. 24
      apps/api/src/services/data-provider/data-provider.service.ts
  64. 14
      apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts
  65. 14
      apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts
  66. 14
      apps/api/src/services/data-provider/ghostfolio/ghostfolio.service.ts
  67. 12
      apps/api/src/services/data-provider/google-sheets/google-sheets.service.ts
  68. 10
      apps/api/src/services/data-provider/interfaces/data-provider.interface.ts
  69. 12
      apps/api/src/services/data-provider/manual/manual.service.ts
  70. 2
      apps/api/src/services/data-provider/rapid-api/interfaces/interfaces.ts
  71. 8
      apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts
  72. 14
      apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts
  73. 4
      apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts
  74. 6
      apps/api/src/services/interfaces/interfaces.ts
  75. 4
      apps/api/src/services/market-data/market-data.service.ts
  76. 4
      apps/api/src/services/queues/data-gathering/data-gathering.processor.ts
  77. 12
      apps/api/src/services/queues/data-gathering/data-gathering.service.ts
  78. 2
      apps/api/src/services/queues/portfolio-snapshot/interfaces/portfolio-snapshot-queue-job.interface.ts
  79. 6
      apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.processor.ts
  80. 4
      apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock.ts
  81. 4
      apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.service.ts
  82. 37
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts
  83. 18
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html
  84. 70
      apps/client/src/app/components/admin-users/admin-users.component.ts
  85. 12
      apps/client/src/app/components/admin-users/admin-users.html
  86. 57
      apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html
  87. 2
      apps/client/src/app/components/rule/rule-settings-dialog/interfaces/interfaces.ts
  88. 4
      apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.component.ts
  89. 4
      apps/client/src/app/components/rule/rule.component.ts
  90. 7
      apps/client/src/app/components/user-account-membership/user-account-membership.component.ts
  91. 7
      apps/client/src/app/components/user-detail-dialog/interfaces/interfaces.ts
  92. 7
      apps/client/src/app/components/user-detail-dialog/user-detail-dialog.component.scss
  93. 52
      apps/client/src/app/components/user-detail-dialog/user-detail-dialog.component.ts
  94. 32
      apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html
  95. 4
      apps/client/src/app/core/notification/alert-dialog/alert-dialog.component.ts
  96. 2
      apps/client/src/app/core/notification/alert-dialog/interfaces/interfaces.ts
  97. 4
      apps/client/src/app/core/notification/confirmation-dialog/confirmation-dialog.component.ts
  98. 2
      apps/client/src/app/core/notification/confirmation-dialog/interfaces/interfaces.ts
  99. 6
      apps/client/src/app/core/notification/interfaces/interfaces.ts
  100. 12
      apps/client/src/app/core/notification/notification.service.ts

14
.config/prisma.ts

@ -0,0 +1,14 @@
import { defineConfig } from '@prisma/config';
import { config } from 'dotenv';
import { expand } from 'dotenv-expand';
import { join } from 'node:path';
expand(config({ quiet: true }));
export default defineConfig({
migrations: {
path: join(__dirname, '..', 'prisma', 'migrations'),
seed: `node ${join(__dirname, '..', 'prisma', 'seed.mts')}`
},
schema: join(__dirname, '..', 'prisma', 'schema.prisma')
});

42
CHANGELOG.md

@ -5,6 +5,46 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## 2.211.0-beta.0 - 2025-10-24
### Added
- Extended the export functionality by the user account’s performance calculation type
- Added a user detail dialog to the users section of the admin control panel
### Changed
- Localized the number formatting in the static portfolio analysis rule: _Liquidity_ (Buying Power)
- Moved the _Prisma Configuration File_ from `prisma.config.ts` to `.config/prisma.ts`
- Improved the language localization for German (`de`)
- Upgraded `prisma` from version `6.17.1` to `6.18.0`
- Upgraded `tablemark` from version `3.1.0` to `4.1.0`
### Fixed
- Fixed the style in the footer row of the accounts table
- Fixed the rendering of names and symbols for custom assets in the import activities dialog
## 2.210.1 - 2025-10-22
### Added
- Added support for data gathering by date range in the asset profile details dialog of the admin control panel
### Changed
- Extracted the portfolio filter form of the assistant to a reusable component
- Formatted the holdings table in the _Copy AI prompt to clipboard for analysis_ action on the analysis page (experimental)
- Formatted the holdings table in the _Copy portfolio data to clipboard for AI prompt_ action on the analysis page (experimental)
- Reverted the explicit configuration of the _Redis_ address family in the job queue module
- Improved the language localization for German (`de`)
- Upgraded `ioredis` from version `5.6.1` to `5.8.2`
### Fixed
- Fixed the enter key press to submit the form of the login with access token dialog
- Fixed an issue in the database seeding process caused by unresolved environment variables in `DATABASE_URL`
## 2.209.0 - 2025-10-18 ## 2.209.0 - 2025-10-18
### Added ### Added
@ -47,7 +87,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Removed the deprecated endpoint `GET api/v1/portfolio/position/:dataSource/:symbol` - Removed the deprecated endpoint `GET api/v1/portfolio/position/:dataSource/:symbol`
- Removed the deprecated endpoint `PUT api/v1/portfolio/position/:dataSource/:symbol/tags` - Removed the deprecated endpoint `PUT api/v1/portfolio/position/:dataSource/:symbol/tags`
- Improved the language localization for German (`de`) - Improved the language localization for German (`de`)
- Upgraded `prisma` from version `6.16.1` to `6.16.3` - Upgraded `prisma` from version `6.16.1` to `6.17.1`
### Fixed ### Fixed

4
Dockerfile

@ -13,11 +13,11 @@ RUN apt-get update && apt-get install -y --no-install-suggests \
# Only add basic files without the application itself to avoid rebuilding # Only add basic files without the application itself to avoid rebuilding
# layers when files (package.json etc.) have not changed # layers when files (package.json etc.) have not changed
COPY ./.config .config/
COPY ./CHANGELOG.md CHANGELOG.md COPY ./CHANGELOG.md CHANGELOG.md
COPY ./LICENSE LICENSE COPY ./LICENSE LICENSE
COPY ./package.json package.json COPY ./package.json package.json
COPY ./package-lock.json package-lock.json COPY ./package-lock.json package-lock.json
COPY ./prisma.config.ts prisma.config.ts
COPY ./prisma/schema.prisma prisma/ COPY ./prisma/schema.prisma prisma/
RUN npm install RUN npm install
@ -44,7 +44,7 @@ WORKDIR /ghostfolio/dist/apps/api
COPY ./package-lock.json /ghostfolio/dist/apps/api/ COPY ./package-lock.json /ghostfolio/dist/apps/api/
RUN npm install RUN npm install
COPY prisma.config.ts /ghostfolio/dist/apps/api/ COPY .config /ghostfolio/dist/apps/api/.config/
COPY prisma /ghostfolio/dist/apps/api/prisma/ COPY prisma /ghostfolio/dist/apps/api/prisma/
# Overwrite the generated package.json with the original one to ensure having # Overwrite the generated package.json with the original one to ensure having

2
apps/api/src/app/admin/admin.controller.ts

@ -169,7 +169,7 @@ export class AdminController {
let date: Date; let date: Date;
if (dateRange) { if (dateRange) {
const { startDate } = getIntervalFromDateRange(dateRange, new Date()); const { startDate } = getIntervalFromDateRange(dateRange);
date = startDate; date = startDate;
} }

1
apps/api/src/app/app.module.ts

@ -71,7 +71,6 @@ import { UserModule } from './user/user.module';
BullModule.forRoot({ BullModule.forRoot({
redis: { redis: {
db: parseInt(process.env.REDIS_DB ?? '0', 10), db: parseInt(process.env.REDIS_DB ?? '0', 10),
family: 0,
host: process.env.REDIS_HOST, host: process.env.REDIS_HOST,
password: process.env.REDIS_PASSWORD, password: process.env.REDIS_PASSWORD,
port: parseInt(process.env.REDIS_PORT ?? '6379', 10) port: parseInt(process.env.REDIS_PORT ?? '6379', 10)

4
apps/api/src/app/asset/asset.controller.ts

@ -1,7 +1,7 @@
import { AdminService } from '@ghostfolio/api/app/admin/admin.service'; import { AdminService } from '@ghostfolio/api/app/admin/admin.service';
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor'; import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor';
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor'; import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor';
import type { AdminMarketDataDetails } from '@ghostfolio/common/interfaces'; import type { AssetResponse } from '@ghostfolio/common/interfaces';
import { Controller, Get, Param, UseInterceptors } from '@nestjs/common'; import { Controller, Get, Param, UseInterceptors } from '@nestjs/common';
import { DataSource } from '@prisma/client'; import { DataSource } from '@prisma/client';
@ -17,7 +17,7 @@ export class AssetController {
public async getAsset( public async getAsset(
@Param('dataSource') dataSource: DataSource, @Param('dataSource') dataSource: DataSource,
@Param('symbol') symbol: string @Param('symbol') symbol: string
): Promise<AdminMarketDataDetails> { ): Promise<AssetResponse> {
const { assetProfile, marketData } = const { assetProfile, marketData } =
await this.adminService.getMarketDataBySymbol({ dataSource, symbol }); await this.adminService.getMarketDataBySymbol({ dataSource, symbol });

66
apps/api/src/app/endpoints/ai/ai.service.ts

@ -10,6 +10,7 @@ import type { AiPromptMode } from '@ghostfolio/common/types';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { createOpenRouter } from '@openrouter/ai-sdk-provider'; import { createOpenRouter } from '@openrouter/ai-sdk-provider';
import { generateText } from 'ai'; import { generateText } from 'ai';
import type { ColumnDescriptor } from 'tablemark';
@Injectable() @Injectable()
export class AiService { export class AiService {
@ -58,34 +59,57 @@ export class AiService {
userId userId
}); });
const holdingsTable = [ const holdingsTableColumns: ColumnDescriptor[] = [
'| Name | Symbol | Currency | Asset Class | Asset Sub Class | Allocation in Percentage |', { name: 'Name' },
'| --- | --- | --- | --- | --- | --- |', { name: 'Symbol' },
...Object.values(holdings) { name: 'Currency' },
.sort((a, b) => { { name: 'Asset Class' },
return b.allocationInPercentage - a.allocationInPercentage; { name: 'Asset Sub Class' },
}) { align: 'right', name: 'Allocation in Percentage' }
.map(
({
allocationInPercentage,
assetClass,
assetSubClass,
currency,
name,
symbol
}) => {
return `| ${name} | ${symbol} | ${currency} | ${assetClass} | ${assetSubClass} | ${(allocationInPercentage * 100).toFixed(3)}% |`;
}
)
]; ];
const holdingsTableRows = Object.values(holdings)
.sort((a, b) => {
return b.allocationInPercentage - a.allocationInPercentage;
})
.map(
({
allocationInPercentage,
assetClass,
assetSubClass,
currency,
name,
symbol
}) => {
return {
Name: name,
Symbol: symbol,
Currency: currency,
'Asset Class': assetClass ?? '',
'Asset Sub Class': assetSubClass ?? '',
'Allocation in Percentage': `${(allocationInPercentage * 100).toFixed(3)}%`
};
}
);
// Dynamic import to load ESM module from CommonJS context
// eslint-disable-next-line @typescript-eslint/no-implied-eval
const dynamicImport = new Function('s', 'return import(s)') as (
s: string
) => Promise<typeof import('tablemark')>;
const { tablemark } = await dynamicImport('tablemark');
const holdingsTableString = tablemark(holdingsTableRows, {
columns: holdingsTableColumns
});
if (mode === 'portfolio') { if (mode === 'portfolio') {
return holdingsTable.join('\n'); return holdingsTableString;
} }
return [ return [
`You are a neutral financial assistant. Please analyze the following investment portfolio (base currency being ${userCurrency}) in simple words.`, `You are a neutral financial assistant. Please analyze the following investment portfolio (base currency being ${userCurrency}) in simple words.`,
...holdingsTable, holdingsTableString,
'Structure your answer with these sections:', 'Structure your answer with these sections:',
'Overview: Briefly summarize the portfolio’s composition and allocation rationale.', 'Overview: Briefly summarize the portfolio’s composition and allocation rationale.',
'Risk Assessment: Identify potential risks, including market volatility, concentration, and sectoral imbalances.', 'Risk Assessment: Identify potential risks, including market volatility, concentration, and sectoral imbalances.',

6
apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.service.ts

@ -8,7 +8,7 @@ import {
GetQuotesParams, GetQuotesParams,
GetSearchParams GetSearchParams
} from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface'; } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces'; import { DataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import { import {
@ -114,7 +114,7 @@ export class GhostfolioService {
try { try {
const promises: Promise<{ const promises: Promise<{
[date: string]: IDataProviderHistoricalResponse; [date: string]: DataProviderHistoricalResponse;
}>[] = []; }>[] = [];
for (const dataProviderService of this.getDataProviderServices()) { for (const dataProviderService of this.getDataProviderServices()) {
@ -156,7 +156,7 @@ export class GhostfolioService {
try { try {
const promises: Promise<{ const promises: Promise<{
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; [symbol: string]: { [date: string]: DataProviderHistoricalResponse };
}>[] = []; }>[] = [];
for (const dataProviderService of this.getDataProviderServices()) { for (const dataProviderService of this.getDataProviderServices()) {

4
apps/api/src/app/exchange-rate/exchange-rate.controller.ts

@ -1,5 +1,5 @@
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces'; import { DataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
import { import {
Controller, Controller,
@ -25,7 +25,7 @@ export class ExchangeRateController {
public async getExchangeRate( public async getExchangeRate(
@Param('dateString') dateString: string, @Param('dateString') dateString: string,
@Param('symbol') symbol: string @Param('symbol') symbol: string
): Promise<IDataProviderHistoricalResponse> { ): Promise<DataProviderHistoricalResponse> {
const date = parseISO(dateString); const date = parseISO(dateString);
const exchangeRate = await this.exchangeRateService.getExchangeRate({ const exchangeRate = await this.exchangeRateService.getExchangeRate({

8
apps/api/src/app/export/export.controller.ts

@ -1,7 +1,7 @@
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor'; import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor';
import { ApiService } from '@ghostfolio/api/services/api/api.service'; import { ApiService } from '@ghostfolio/api/services/api/api.service';
import { Export } from '@ghostfolio/common/interfaces'; import { ExportResponse } from '@ghostfolio/common/interfaces';
import type { RequestWithUser } from '@ghostfolio/common/types'; import type { RequestWithUser } from '@ghostfolio/common/types';
import { import {
@ -35,7 +35,7 @@ export class ExportController {
@Query('dataSource') filterByDataSource?: string, @Query('dataSource') filterByDataSource?: string,
@Query('symbol') filterBySymbol?: string, @Query('symbol') filterBySymbol?: string,
@Query('tags') filterByTags?: string @Query('tags') filterByTags?: string
): Promise<Export> { ): Promise<ExportResponse> {
const activityIds = filterByActivityIds?.split(',') ?? []; const activityIds = filterByActivityIds?.split(',') ?? [];
const filters = this.apiService.buildFiltersFromQueryParams({ const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts, filterByAccounts,
@ -48,8 +48,8 @@ export class ExportController {
return this.exportService.export({ return this.exportService.export({
activityIds, activityIds,
filters, filters,
userCurrency: this.request.user.settings.settings.baseCurrency, userId: this.request.user.id,
userId: this.request.user.id userSettings: this.request.user.settings.settings
}); });
} }
} }

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

@ -3,7 +3,11 @@ import { OrderService } from '@ghostfolio/api/app/order/order.service';
import { environment } from '@ghostfolio/api/environments/environment'; import { environment } from '@ghostfolio/api/environments/environment';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { TagService } from '@ghostfolio/api/services/tag/tag.service'; import { TagService } from '@ghostfolio/api/services/tag/tag.service';
import { Filter, Export } from '@ghostfolio/common/interfaces'; import {
ExportResponse,
Filter,
UserSettings
} from '@ghostfolio/common/interfaces';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Platform, Prisma } from '@prisma/client'; import { Platform, Prisma } from '@prisma/client';
@ -21,14 +25,14 @@ export class ExportService {
public async export({ public async export({
activityIds, activityIds,
filters, filters,
userCurrency, userId,
userId userSettings
}: { }: {
activityIds?: string[]; activityIds?: string[];
filters?: Filter[]; filters?: Filter[];
userCurrency: string;
userId: string; userId: string;
}): Promise<Export> { userSettings: UserSettings;
}): Promise<ExportResponse> {
const { ACCOUNT: filtersByAccount } = groupBy(filters, ({ type }) => { const { ACCOUNT: filtersByAccount } = groupBy(filters, ({ type }) => {
return type; return type;
}); });
@ -36,11 +40,11 @@ export class ExportService {
let { activities } = await this.orderService.getOrders({ let { activities } = await this.orderService.getOrders({
filters, filters,
userCurrency,
userId, userId,
includeDrafts: true, includeDrafts: true,
sortColumn: 'date', sortColumn: 'date',
sortDirection: 'asc', sortDirection: 'asc',
userCurrency: userSettings?.baseCurrency,
withExcludedAccountsAndActivities: true withExcludedAccountsAndActivities: true
}); });
@ -244,7 +248,10 @@ export class ExportService {
} }
), ),
user: { user: {
settings: { currency: userCurrency } settings: {
currency: userSettings?.baseCurrency,
performanceCalculationType: userSettings?.performanceCalculationType
}
} }
}; };
} }

12
apps/api/src/app/import/import.service.ts

@ -539,6 +539,7 @@ export class ImportService {
connectOrCreate: { connectOrCreate: {
create: { create: {
dataSource, dataSource,
name,
symbol, symbol,
currency: assetProfile.currency, currency: assetProfile.currency,
userId: dataSource === 'MANUAL' ? user.id : undefined userId: dataSource === 'MANUAL' ? user.id : undefined
@ -746,10 +747,19 @@ export class ImportService {
if (['FEE', 'INTEREST', 'LIABILITY'].includes(type)) { if (['FEE', 'INTEREST', 'LIABILITY'].includes(type)) {
// Skip asset profile validation for FEE, INTEREST, and LIABILITY // Skip asset profile validation for FEE, INTEREST, and LIABILITY
// as these activity types don't require asset profiles // as these activity types don't require asset profiles
const assetProfileInImport = assetProfilesWithMarketDataDto?.find(
(profile) => {
return (
profile.dataSource === dataSource && profile.symbol === symbol
);
}
);
assetProfiles[getAssetProfileIdentifier({ dataSource, symbol })] = { assetProfiles[getAssetProfileIdentifier({ dataSource, symbol })] = {
currency, currency,
dataSource, dataSource,
symbol symbol,
name: assetProfileInImport?.name
}; };
continue; continue;

4
apps/api/src/app/info/info.controller.ts

@ -1,5 +1,5 @@
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor'; import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor';
import { InfoItem } from '@ghostfolio/common/interfaces'; import { InfoResponse } from '@ghostfolio/common/interfaces';
import { Controller, Get, UseInterceptors } from '@nestjs/common'; import { Controller, Get, UseInterceptors } from '@nestjs/common';
@ -11,7 +11,7 @@ export class InfoController {
@Get() @Get()
@UseInterceptors(TransformDataSourceInResponseInterceptor) @UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getInfo(): Promise<InfoItem> { public async getInfo(): Promise<InfoResponse> {
return this.infoService.get(); return this.infoService.get();
} }
} }

3
apps/api/src/app/order/create-order.dto.ts

@ -44,7 +44,8 @@ export class CreateOrderDto {
customCurrency?: string; customCurrency?: string;
@IsEnum(DataSource) @IsEnum(DataSource)
dataSource: DataSource; @IsOptional() // Optional for type FEE, INTEREST, and LIABILITY (default data source is resolved in the backend)
dataSource?: DataSource;
@IsISO8601() @IsISO8601()
@Validate(IsAfter1970Constraint) @Validate(IsAfter1970Constraint)

5
apps/api/src/app/order/interfaces/activities.interface.ts

@ -3,11 +3,6 @@ import { AccountWithPlatform } from '@ghostfolio/common/types';
import { Order, Tag } from '@prisma/client'; import { Order, Tag } from '@prisma/client';
export interface Activities {
activities: Activity[];
count: number;
}
export interface Activity extends Order { export interface Activity extends Order {
account?: AccountWithPlatform; account?: AccountWithPlatform;
error?: ActivityError; error?: ActivityError;

9
apps/api/src/app/order/order.controller.ts

@ -11,6 +11,10 @@ import {
DATA_GATHERING_QUEUE_PRIORITY_HIGH, DATA_GATHERING_QUEUE_PRIORITY_HIGH,
HEADER_KEY_IMPERSONATION HEADER_KEY_IMPERSONATION
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import {
ActivitiesResponse,
ActivityResponse
} from '@ghostfolio/common/interfaces';
import { permissions } from '@ghostfolio/common/permissions'; import { permissions } from '@ghostfolio/common/permissions';
import type { DateRange, RequestWithUser } from '@ghostfolio/common/types'; import type { DateRange, RequestWithUser } from '@ghostfolio/common/types';
@ -36,7 +40,6 @@ import { parseISO } from 'date-fns';
import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { CreateOrderDto } from './create-order.dto'; import { CreateOrderDto } from './create-order.dto';
import { Activities, Activity } from './interfaces/activities.interface';
import { OrderService } from './order.service'; import { OrderService } from './order.service';
import { UpdateOrderDto } from './update-order.dto'; import { UpdateOrderDto } from './update-order.dto';
@ -113,7 +116,7 @@ export class OrderController {
@Query('symbol') filterBySymbol?: string, @Query('symbol') filterBySymbol?: string,
@Query('tags') filterByTags?: string, @Query('tags') filterByTags?: string,
@Query('take') take?: number @Query('take') take?: number
): Promise<Activities> { ): Promise<ActivitiesResponse> {
let endDate: Date; let endDate: Date;
let startDate: Date; let startDate: Date;
@ -157,7 +160,7 @@ export class OrderController {
public async getOrderById( public async getOrderById(
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string, @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string,
@Param('id') id: string @Param('id') id: string
): Promise<Activity> { ): Promise<ActivityResponse> {
const impersonationUserId = const impersonationUserId =
await this.impersonationService.validateImpersonationId(impersonationId); await this.impersonationService.validateImpersonationId(impersonationId);
const userCurrency = this.request.user.settings.settings.baseCurrency; const userCurrency = this.request.user.settings.settings.baseCurrency;

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

@ -14,6 +14,7 @@ import {
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { getAssetProfileIdentifier } from '@ghostfolio/common/helper'; import { getAssetProfileIdentifier } from '@ghostfolio/common/helper';
import { import {
ActivitiesResponse,
AssetProfileIdentifier, AssetProfileIdentifier,
EnhancedSymbolProfile, EnhancedSymbolProfile,
Filter Filter
@ -37,8 +38,6 @@ import { endOfToday, isAfter } from 'date-fns';
import { groupBy, uniqBy } from 'lodash'; import { groupBy, uniqBy } from 'lodash';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { Activities } from './interfaces/activities.interface';
@Injectable() @Injectable()
export class OrderService { export class OrderService {
public constructor( public constructor(
@ -129,7 +128,7 @@ export class OrderService {
const assetSubClass = data.assetSubClass; const assetSubClass = data.assetSubClass;
const dataSource: DataSource = 'MANUAL'; const dataSource: DataSource = 'MANUAL';
let name: string; let name = data.SymbolProfile.connectOrCreate.create.name;
let symbol: string; let symbol: string;
if ( if (
@ -142,7 +141,7 @@ export class OrderService {
symbol = data.SymbolProfile.connectOrCreate.create.symbol; symbol = data.SymbolProfile.connectOrCreate.create.symbol;
} else { } else {
// Create custom asset profile // Create custom asset profile
name = data.SymbolProfile.connectOrCreate.create.symbol; name = name ?? data.SymbolProfile.connectOrCreate.create.symbol;
symbol = uuidv4(); symbol = uuidv4();
} }
@ -345,7 +344,7 @@ export class OrderService {
userCurrency: string; userCurrency: string;
userId: string; userId: string;
withExcludedAccountsAndActivities?: boolean; withExcludedAccountsAndActivities?: boolean;
}): Promise<Activities> { }): Promise<ActivitiesResponse> {
let orderBy: Prisma.Enumerable<Prisma.OrderOrderByWithRelationInput> = [ let orderBy: Prisma.Enumerable<Prisma.OrderOrderByWithRelationInput> = [
{ date: 'asc' } { date: 'asc' }
]; ];

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

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

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

@ -9,7 +9,7 @@ import { getFactor } from '@ghostfolio/api/helper/portfolio.helper';
import { LogPerformance } from '@ghostfolio/api/interceptors/performance-logging/performance-logging.interceptor'; import { LogPerformance } from '@ghostfolio/api/interceptors/performance-logging/performance-logging.interceptor';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces'; import { DataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service'; import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service';
import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper'; import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper';
import { import {
@ -193,7 +193,7 @@ export abstract class PortfolioCalculator {
} }
const currencies: { [symbol: string]: string } = {}; const currencies: { [symbol: string]: string } = {};
const dataGatheringItems: IDataGatheringItem[] = []; const dataGatheringItems: DataGatheringItem[] = [];
let firstIndex = transactionPoints.length; let firstIndex = transactionPoints.length;
let firstTransactionPoint: TransactionPoint = null; let firstTransactionPoint: TransactionPoint = null;
let totalInterestWithCurrencyEffect = new Big(0); let totalInterestWithCurrencyEffect = new Big(0);

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

@ -20,7 +20,6 @@ import { Big } from 'big.js';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
return { return {
// eslint-disable-next-line @typescript-eslint/naming-convention
CurrentRateService: jest.fn().mockImplementation(() => { CurrentRateService: jest.fn().mockImplementation(() => {
return CurrentRateServiceMock; return CurrentRateServiceMock;
}) })
@ -31,7 +30,6 @@ jest.mock(
'@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service', '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service',
() => { () => {
return { return {
// eslint-disable-next-line @typescript-eslint/naming-convention
PortfolioSnapshotService: jest.fn().mockImplementation(() => { PortfolioSnapshotService: jest.fn().mockImplementation(() => {
return PortfolioSnapshotServiceMock; return PortfolioSnapshotServiceMock;
}) })
@ -41,7 +39,6 @@ jest.mock(
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return { return {
// eslint-disable-next-line @typescript-eslint/naming-convention
RedisCacheService: jest.fn().mockImplementation(() => { RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock; return RedisCacheServiceMock;
}) })

3
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts

@ -20,7 +20,6 @@ import { Big } from 'big.js';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
return { return {
// eslint-disable-next-line @typescript-eslint/naming-convention
CurrentRateService: jest.fn().mockImplementation(() => { CurrentRateService: jest.fn().mockImplementation(() => {
return CurrentRateServiceMock; return CurrentRateServiceMock;
}) })
@ -31,7 +30,6 @@ jest.mock(
'@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service', '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service',
() => { () => {
return { return {
// eslint-disable-next-line @typescript-eslint/naming-convention
PortfolioSnapshotService: jest.fn().mockImplementation(() => { PortfolioSnapshotService: jest.fn().mockImplementation(() => {
return PortfolioSnapshotServiceMock; return PortfolioSnapshotServiceMock;
}) })
@ -41,7 +39,6 @@ jest.mock(
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return { return {
// eslint-disable-next-line @typescript-eslint/naming-convention
RedisCacheService: jest.fn().mockImplementation(() => { RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock; return RedisCacheServiceMock;
}) })

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

@ -20,7 +20,6 @@ import { Big } from 'big.js';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
return { return {
// eslint-disable-next-line @typescript-eslint/naming-convention
CurrentRateService: jest.fn().mockImplementation(() => { CurrentRateService: jest.fn().mockImplementation(() => {
return CurrentRateServiceMock; return CurrentRateServiceMock;
}) })
@ -31,7 +30,6 @@ jest.mock(
'@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service', '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service',
() => { () => {
return { return {
// eslint-disable-next-line @typescript-eslint/naming-convention
PortfolioSnapshotService: jest.fn().mockImplementation(() => { PortfolioSnapshotService: jest.fn().mockImplementation(() => {
return PortfolioSnapshotServiceMock; return PortfolioSnapshotServiceMock;
}) })
@ -41,7 +39,6 @@ jest.mock(
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return { return {
// eslint-disable-next-line @typescript-eslint/naming-convention
RedisCacheService: jest.fn().mockImplementation(() => { RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock; return RedisCacheServiceMock;
}) })

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

@ -20,7 +20,6 @@ import { Big } from 'big.js';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
return { return {
// eslint-disable-next-line @typescript-eslint/naming-convention
CurrentRateService: jest.fn().mockImplementation(() => { CurrentRateService: jest.fn().mockImplementation(() => {
return CurrentRateServiceMock; return CurrentRateServiceMock;
}) })
@ -31,7 +30,6 @@ jest.mock(
'@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service', '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service',
() => { () => {
return { return {
// eslint-disable-next-line @typescript-eslint/naming-convention
PortfolioSnapshotService: jest.fn().mockImplementation(() => { PortfolioSnapshotService: jest.fn().mockImplementation(() => {
return PortfolioSnapshotServiceMock; return PortfolioSnapshotServiceMock;
}) })
@ -41,7 +39,6 @@ jest.mock(
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return { return {
// eslint-disable-next-line @typescript-eslint/naming-convention
RedisCacheService: jest.fn().mockImplementation(() => { RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock; return RedisCacheServiceMock;
}) })

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

@ -15,7 +15,7 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-
import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.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 { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock';
import { parseDate } from '@ghostfolio/common/helper'; import { parseDate } from '@ghostfolio/common/helper';
import { Export } from '@ghostfolio/common/interfaces'; import { ExportResponse } from '@ghostfolio/common/interfaces';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
import { Big } from 'big.js'; import { Big } from 'big.js';
@ -23,7 +23,6 @@ import { join } from 'node:path';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
return { return {
// eslint-disable-next-line @typescript-eslint/naming-convention
CurrentRateService: jest.fn().mockImplementation(() => { CurrentRateService: jest.fn().mockImplementation(() => {
return CurrentRateServiceMock; return CurrentRateServiceMock;
}) })
@ -34,7 +33,6 @@ jest.mock(
'@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service', '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service',
() => { () => {
return { return {
// eslint-disable-next-line @typescript-eslint/naming-convention
PortfolioSnapshotService: jest.fn().mockImplementation(() => { PortfolioSnapshotService: jest.fn().mockImplementation(() => {
return PortfolioSnapshotServiceMock; return PortfolioSnapshotServiceMock;
}) })
@ -44,7 +42,6 @@ jest.mock(
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return { return {
// eslint-disable-next-line @typescript-eslint/naming-convention
RedisCacheService: jest.fn().mockImplementation(() => { RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock; return RedisCacheServiceMock;
}) })
@ -52,7 +49,7 @@ jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
}); });
describe('PortfolioCalculator', () => { describe('PortfolioCalculator', () => {
let exportResponse: Export; let exportResponse: ExportResponse;
let configurationService: ConfigurationService; let configurationService: ConfigurationService;
let currentRateService: CurrentRateService; let currentRateService: CurrentRateService;

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

@ -21,7 +21,6 @@ import { Big } from 'big.js';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
return { return {
// eslint-disable-next-line @typescript-eslint/naming-convention
CurrentRateService: jest.fn().mockImplementation(() => { CurrentRateService: jest.fn().mockImplementation(() => {
return CurrentRateServiceMock; return CurrentRateServiceMock;
}) })
@ -32,7 +31,6 @@ jest.mock(
'@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service', '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service',
() => { () => {
return { return {
// eslint-disable-next-line @typescript-eslint/naming-convention
PortfolioSnapshotService: jest.fn().mockImplementation(() => { PortfolioSnapshotService: jest.fn().mockImplementation(() => {
return PortfolioSnapshotServiceMock; return PortfolioSnapshotServiceMock;
}) })
@ -42,7 +40,6 @@ jest.mock(
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return { return {
// eslint-disable-next-line @typescript-eslint/naming-convention
RedisCacheService: jest.fn().mockImplementation(() => { RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock; return RedisCacheServiceMock;
}) })
@ -53,7 +50,6 @@ jest.mock(
'@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service', '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service',
() => { () => {
return { return {
// eslint-disable-next-line @typescript-eslint/naming-convention
ExchangeRateDataService: jest.fn().mockImplementation(() => { ExchangeRateDataService: jest.fn().mockImplementation(() => {
return ExchangeRateDataServiceMock; return ExchangeRateDataServiceMock;
}) })

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

@ -15,7 +15,7 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-
import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.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 { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock';
import { parseDate } from '@ghostfolio/common/helper'; import { parseDate } from '@ghostfolio/common/helper';
import { Export } from '@ghostfolio/common/interfaces'; import { ExportResponse } from '@ghostfolio/common/interfaces';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
import { Big } from 'big.js'; import { Big } from 'big.js';
@ -23,7 +23,6 @@ import { join } from 'node:path';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
return { return {
// eslint-disable-next-line @typescript-eslint/naming-convention
CurrentRateService: jest.fn().mockImplementation(() => { CurrentRateService: jest.fn().mockImplementation(() => {
return CurrentRateServiceMock; return CurrentRateServiceMock;
}) })
@ -34,7 +33,6 @@ jest.mock(
'@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service', '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service',
() => { () => {
return { return {
// eslint-disable-next-line @typescript-eslint/naming-convention
PortfolioSnapshotService: jest.fn().mockImplementation(() => { PortfolioSnapshotService: jest.fn().mockImplementation(() => {
return PortfolioSnapshotServiceMock; return PortfolioSnapshotServiceMock;
}) })
@ -44,7 +42,6 @@ jest.mock(
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return { return {
// eslint-disable-next-line @typescript-eslint/naming-convention
RedisCacheService: jest.fn().mockImplementation(() => { RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock; return RedisCacheServiceMock;
}) })
@ -52,7 +49,7 @@ jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
}); });
describe('PortfolioCalculator', () => { describe('PortfolioCalculator', () => {
let exportResponse: Export; let exportResponse: ExportResponse;
let configurationService: ConfigurationService; let configurationService: ConfigurationService;
let currentRateService: CurrentRateService; let currentRateService: CurrentRateService;

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

@ -15,7 +15,7 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-
import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.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 { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock';
import { parseDate } from '@ghostfolio/common/helper'; import { parseDate } from '@ghostfolio/common/helper';
import { Export } from '@ghostfolio/common/interfaces'; import { ExportResponse } from '@ghostfolio/common/interfaces';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
import { Big } from 'big.js'; import { Big } from 'big.js';
@ -23,7 +23,6 @@ import { join } from 'node:path';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
return { return {
// eslint-disable-next-line @typescript-eslint/naming-convention
CurrentRateService: jest.fn().mockImplementation(() => { CurrentRateService: jest.fn().mockImplementation(() => {
return CurrentRateServiceMock; return CurrentRateServiceMock;
}) })
@ -34,7 +33,6 @@ jest.mock(
'@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service', '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service',
() => { () => {
return { return {
// eslint-disable-next-line @typescript-eslint/naming-convention
PortfolioSnapshotService: jest.fn().mockImplementation(() => { PortfolioSnapshotService: jest.fn().mockImplementation(() => {
return PortfolioSnapshotServiceMock; return PortfolioSnapshotServiceMock;
}) })
@ -44,7 +42,6 @@ jest.mock(
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return { return {
// eslint-disable-next-line @typescript-eslint/naming-convention
RedisCacheService: jest.fn().mockImplementation(() => { RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock; return RedisCacheServiceMock;
}) })
@ -52,7 +49,7 @@ jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
}); });
describe('PortfolioCalculator', () => { describe('PortfolioCalculator', () => {
let exportResponse: Export; let exportResponse: ExportResponse;
let configurationService: ConfigurationService; let configurationService: ConfigurationService;
let currentRateService: CurrentRateService; let currentRateService: CurrentRateService;

3
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-fee.spec.ts

@ -20,7 +20,6 @@ import { Big } from 'big.js';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
return { return {
// eslint-disable-next-line @typescript-eslint/naming-convention
CurrentRateService: jest.fn().mockImplementation(() => { CurrentRateService: jest.fn().mockImplementation(() => {
return CurrentRateServiceMock; return CurrentRateServiceMock;
}) })
@ -31,7 +30,6 @@ jest.mock(
'@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service', '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service',
() => { () => {
return { return {
// eslint-disable-next-line @typescript-eslint/naming-convention
PortfolioSnapshotService: jest.fn().mockImplementation(() => { PortfolioSnapshotService: jest.fn().mockImplementation(() => {
return PortfolioSnapshotServiceMock; return PortfolioSnapshotServiceMock;
}) })
@ -41,7 +39,6 @@ jest.mock(
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return { return {
// eslint-disable-next-line @typescript-eslint/naming-convention
RedisCacheService: jest.fn().mockImplementation(() => { RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock; return RedisCacheServiceMock;
}) })

4
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-googl-buy.spec.ts

@ -21,7 +21,6 @@ import { Big } from 'big.js';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
return { return {
// eslint-disable-next-line @typescript-eslint/naming-convention
CurrentRateService: jest.fn().mockImplementation(() => { CurrentRateService: jest.fn().mockImplementation(() => {
return CurrentRateServiceMock; return CurrentRateServiceMock;
}) })
@ -32,7 +31,6 @@ jest.mock(
'@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service', '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service',
() => { () => {
return { return {
// eslint-disable-next-line @typescript-eslint/naming-convention
PortfolioSnapshotService: jest.fn().mockImplementation(() => { PortfolioSnapshotService: jest.fn().mockImplementation(() => {
return PortfolioSnapshotServiceMock; return PortfolioSnapshotServiceMock;
}) })
@ -42,7 +40,6 @@ jest.mock(
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return { return {
// eslint-disable-next-line @typescript-eslint/naming-convention
RedisCacheService: jest.fn().mockImplementation(() => { RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock; return RedisCacheServiceMock;
}) })
@ -53,7 +50,6 @@ jest.mock(
'@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service', '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service',
() => { () => {
return { return {
// eslint-disable-next-line @typescript-eslint/naming-convention
ExchangeRateDataService: jest.fn().mockImplementation(() => { ExchangeRateDataService: jest.fn().mockImplementation(() => {
return ExchangeRateDataServiceMock; return ExchangeRateDataServiceMock;
}) })

3
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-liability.spec.ts

@ -20,7 +20,6 @@ import { Big } from 'big.js';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
return { return {
// eslint-disable-next-line @typescript-eslint/naming-convention
CurrentRateService: jest.fn().mockImplementation(() => { CurrentRateService: jest.fn().mockImplementation(() => {
return CurrentRateServiceMock; return CurrentRateServiceMock;
}) })
@ -31,7 +30,6 @@ jest.mock(
'@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service', '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service',
() => { () => {
return { return {
// eslint-disable-next-line @typescript-eslint/naming-convention
PortfolioSnapshotService: jest.fn().mockImplementation(() => { PortfolioSnapshotService: jest.fn().mockImplementation(() => {
return PortfolioSnapshotServiceMock; return PortfolioSnapshotServiceMock;
}) })
@ -41,7 +39,6 @@ jest.mock(
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return { return {
// eslint-disable-next-line @typescript-eslint/naming-convention
RedisCacheService: jest.fn().mockImplementation(() => { RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock; return RedisCacheServiceMock;
}) })

2
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-and-sell.spec.ts

@ -28,7 +28,6 @@ jest.mock(
'@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service', '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service',
() => { () => {
return { return {
// eslint-disable-next-line @typescript-eslint/naming-convention
PortfolioSnapshotService: jest.fn().mockImplementation(() => { PortfolioSnapshotService: jest.fn().mockImplementation(() => {
return PortfolioSnapshotServiceMock; return PortfolioSnapshotServiceMock;
}) })
@ -38,7 +37,6 @@ jest.mock(
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return { return {
// eslint-disable-next-line @typescript-eslint/naming-convention
RedisCacheService: jest.fn().mockImplementation(() => { RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock; return RedisCacheServiceMock;
}) })

3
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-with-dividend.spec.ts

@ -20,7 +20,6 @@ import { Big } from 'big.js';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
return { return {
// eslint-disable-next-line @typescript-eslint/naming-convention
CurrentRateService: jest.fn().mockImplementation(() => { CurrentRateService: jest.fn().mockImplementation(() => {
return CurrentRateServiceMock; return CurrentRateServiceMock;
}) })
@ -31,7 +30,6 @@ jest.mock(
'@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service', '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service',
() => { () => {
return { return {
// eslint-disable-next-line @typescript-eslint/naming-convention
PortfolioSnapshotService: jest.fn().mockImplementation(() => { PortfolioSnapshotService: jest.fn().mockImplementation(() => {
return PortfolioSnapshotServiceMock; return PortfolioSnapshotServiceMock;
}) })
@ -41,7 +39,6 @@ jest.mock(
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return { return {
// eslint-disable-next-line @typescript-eslint/naming-convention
RedisCacheService: jest.fn().mockImplementation(() => { RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock; return RedisCacheServiceMock;
}) })

3
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-no-orders.spec.ts

@ -15,7 +15,6 @@ import { Big } from 'big.js';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
return { return {
// eslint-disable-next-line @typescript-eslint/naming-convention
CurrentRateService: jest.fn().mockImplementation(() => { CurrentRateService: jest.fn().mockImplementation(() => {
return CurrentRateServiceMock; return CurrentRateServiceMock;
}) })
@ -26,7 +25,6 @@ jest.mock(
'@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service', '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service',
() => { () => {
return { return {
// eslint-disable-next-line @typescript-eslint/naming-convention
PortfolioSnapshotService: jest.fn().mockImplementation(() => { PortfolioSnapshotService: jest.fn().mockImplementation(() => {
return PortfolioSnapshotServiceMock; return PortfolioSnapshotServiceMock;
}) })
@ -36,7 +34,6 @@ jest.mock(
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return { return {
// eslint-disable-next-line @typescript-eslint/naming-convention
RedisCacheService: jest.fn().mockImplementation(() => { RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock; return RedisCacheServiceMock;
}) })

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

@ -15,7 +15,7 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-
import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.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 { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock';
import { parseDate } from '@ghostfolio/common/helper'; import { parseDate } from '@ghostfolio/common/helper';
import { Export } from '@ghostfolio/common/interfaces'; import { ExportResponse } from '@ghostfolio/common/interfaces';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
import { Big } from 'big.js'; import { Big } from 'big.js';
@ -23,7 +23,6 @@ import { join } from 'node:path';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
return { return {
// eslint-disable-next-line @typescript-eslint/naming-convention
CurrentRateService: jest.fn().mockImplementation(() => { CurrentRateService: jest.fn().mockImplementation(() => {
return CurrentRateServiceMock; return CurrentRateServiceMock;
}) })
@ -34,7 +33,6 @@ jest.mock(
'@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service', '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service',
() => { () => {
return { return {
// eslint-disable-next-line @typescript-eslint/naming-convention
PortfolioSnapshotService: jest.fn().mockImplementation(() => { PortfolioSnapshotService: jest.fn().mockImplementation(() => {
return PortfolioSnapshotServiceMock; return PortfolioSnapshotServiceMock;
}) })
@ -44,7 +42,6 @@ jest.mock(
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return { return {
// eslint-disable-next-line @typescript-eslint/naming-convention
RedisCacheService: jest.fn().mockImplementation(() => { RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock; return RedisCacheServiceMock;
}) })
@ -52,7 +49,7 @@ jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
}); });
describe('PortfolioCalculator', () => { describe('PortfolioCalculator', () => {
let exportResponse: Export; let exportResponse: ExportResponse;
let configurationService: ConfigurationService; let configurationService: ConfigurationService;
let currentRateService: CurrentRateService; let currentRateService: CurrentRateService;

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

@ -15,7 +15,7 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-
import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.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 { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock';
import { parseDate } from '@ghostfolio/common/helper'; import { parseDate } from '@ghostfolio/common/helper';
import { Export } from '@ghostfolio/common/interfaces'; import { ExportResponse } from '@ghostfolio/common/interfaces';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
import { Big } from 'big.js'; import { Big } from 'big.js';
@ -23,7 +23,6 @@ import { join } from 'node:path';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
return { return {
// eslint-disable-next-line @typescript-eslint/naming-convention
CurrentRateService: jest.fn().mockImplementation(() => { CurrentRateService: jest.fn().mockImplementation(() => {
return CurrentRateServiceMock; return CurrentRateServiceMock;
}) })
@ -34,7 +33,6 @@ jest.mock(
'@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service', '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service',
() => { () => {
return { return {
// eslint-disable-next-line @typescript-eslint/naming-convention
PortfolioSnapshotService: jest.fn().mockImplementation(() => { PortfolioSnapshotService: jest.fn().mockImplementation(() => {
return PortfolioSnapshotServiceMock; return PortfolioSnapshotServiceMock;
}) })
@ -44,7 +42,6 @@ jest.mock(
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return { return {
// eslint-disable-next-line @typescript-eslint/naming-convention
RedisCacheService: jest.fn().mockImplementation(() => { RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock; return RedisCacheServiceMock;
}) })
@ -52,7 +49,7 @@ jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
}); });
describe('PortfolioCalculator', () => { describe('PortfolioCalculator', () => {
let exportResponse: Export; let exportResponse: ExportResponse;
let configurationService: ConfigurationService; let configurationService: ConfigurationService;
let currentRateService: CurrentRateService; let currentRateService: CurrentRateService;

3
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-valuable.spec.ts

@ -20,7 +20,6 @@ import { Big } from 'big.js';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
return { return {
// eslint-disable-next-line @typescript-eslint/naming-convention
CurrentRateService: jest.fn().mockImplementation(() => { CurrentRateService: jest.fn().mockImplementation(() => {
return CurrentRateServiceMock; return CurrentRateServiceMock;
}) })
@ -31,7 +30,6 @@ jest.mock(
'@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service', '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service',
() => { () => {
return { return {
// eslint-disable-next-line @typescript-eslint/naming-convention
PortfolioSnapshotService: jest.fn().mockImplementation(() => { PortfolioSnapshotService: jest.fn().mockImplementation(() => {
return PortfolioSnapshotServiceMock; return PortfolioSnapshotServiceMock;
}) })
@ -41,7 +39,6 @@ jest.mock(
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return { return {
// eslint-disable-next-line @typescript-eslint/naming-convention
RedisCacheService: jest.fn().mockImplementation(() => { RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock; return RedisCacheServiceMock;
}) })

4
apps/api/src/app/portfolio/interfaces/get-values-params.interface.ts

@ -1,8 +1,8 @@
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces'; import { DataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
import { DateQuery } from './date-query.interface'; import { DateQuery } from './date-query.interface';
export interface GetValuesParams { export interface GetValuesParams {
dataGatheringItems: IDataGatheringItem[]; dataGatheringItems: DataGatheringItem[];
dateQuery: DateQuery; dateQuery: DateQuery;
} }

11
apps/api/src/app/subscription/subscription.controller.ts

@ -5,7 +5,10 @@ import {
DEFAULT_LANGUAGE_CODE, DEFAULT_LANGUAGE_CODE,
PROPERTY_COUPONS PROPERTY_COUPONS
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { Coupon } from '@ghostfolio/common/interfaces'; import {
Coupon,
CreateStripeCheckoutSessionResponse
} from '@ghostfolio/common/interfaces';
import type { RequestWithUser } from '@ghostfolio/common/types'; import type { RequestWithUser } from '@ghostfolio/common/types';
import { import {
@ -111,11 +114,11 @@ export class SubscriptionController {
@Post('stripe/checkout-session') @Post('stripe/checkout-session')
@UseGuards(AuthGuard('jwt'), HasPermissionGuard) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async createCheckoutSession( public createStripeCheckoutSession(
@Body() { couponId, priceId }: { couponId?: string; priceId: string } @Body() { couponId, priceId }: { couponId?: string; priceId: string }
) { ): Promise<CreateStripeCheckoutSessionResponse> {
try { try {
return this.subscriptionService.createCheckoutSession({ return this.subscriptionService.createStripeCheckoutSession({
couponId, couponId,
priceId, priceId,
user: this.request.user user: this.request.user

62
apps/api/src/app/subscription/subscription.service.ts

@ -6,7 +6,10 @@ import {
PROPERTY_STRIPE_CONFIG PROPERTY_STRIPE_CONFIG
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { parseDate } from '@ghostfolio/common/helper'; import { parseDate } from '@ghostfolio/common/helper';
import { SubscriptionOffer } from '@ghostfolio/common/interfaces'; import {
CreateStripeCheckoutSessionResponse,
SubscriptionOffer
} from '@ghostfolio/common/interfaces';
import { import {
SubscriptionOfferKey, SubscriptionOfferKey,
UserWithSettings UserWithSettings
@ -38,7 +41,7 @@ export class SubscriptionService {
} }
} }
public async createCheckoutSession({ public async createStripeCheckoutSession({
couponId, couponId,
priceId, priceId,
user user
@ -46,7 +49,7 @@ export class SubscriptionService {
couponId?: string; couponId?: string;
priceId: string; priceId: string;
user: UserWithSettings; user: UserWithSettings;
}) { }): Promise<CreateStripeCheckoutSessionResponse> {
const subscriptionOffers: { const subscriptionOffers: {
[offer in SubscriptionOfferKey]: SubscriptionOffer; [offer in SubscriptionOfferKey]: SubscriptionOffer;
} = } =
@ -58,33 +61,34 @@ export class SubscriptionService {
} }
); );
const checkoutSessionCreateParams: Stripe.Checkout.SessionCreateParams = { const stripeCheckoutSessionCreateParams: Stripe.Checkout.SessionCreateParams =
cancel_url: `${this.configurationService.get('ROOT_URL')}/${ {
user.settings.settings.language cancel_url: `${this.configurationService.get('ROOT_URL')}/${
}/account`, user.settings.settings.language
client_reference_id: user.id, }/account`,
line_items: [ client_reference_id: user.id,
{ line_items: [
price: priceId, {
quantity: 1 price: priceId,
} quantity: 1
], }
locale: ],
(user.settings?.settings locale:
?.language as Stripe.Checkout.SessionCreateParams.Locale) ?? (user.settings?.settings
DEFAULT_LANGUAGE_CODE, ?.language as Stripe.Checkout.SessionCreateParams.Locale) ??
metadata: subscriptionOffer DEFAULT_LANGUAGE_CODE,
? { subscriptionOffer: JSON.stringify(subscriptionOffer) } metadata: subscriptionOffer
: {}, ? { subscriptionOffer: JSON.stringify(subscriptionOffer) }
mode: 'payment', : {},
payment_method_types: ['card'], mode: 'payment',
success_url: `${this.configurationService.get( payment_method_types: ['card'],
'ROOT_URL' success_url: `${this.configurationService.get(
)}/api/v1/subscription/stripe/callback?checkoutSessionId={CHECKOUT_SESSION_ID}` 'ROOT_URL'
}; )}/api/v1/subscription/stripe/callback?checkoutSessionId={CHECKOUT_SESSION_ID}`
};
if (couponId) { if (couponId) {
checkoutSessionCreateParams.discounts = [ stripeCheckoutSessionCreateParams.discounts = [
{ {
coupon: couponId coupon: couponId
} }
@ -92,7 +96,7 @@ export class SubscriptionService {
} }
const session = await this.stripe.checkout.sessions.create( const session = await this.stripe.checkout.sessions.create(
checkoutSessionCreateParams stripeCheckoutSessionCreateParams
); );
return { return {

4
apps/api/src/app/symbol/symbol.controller.ts

@ -1,7 +1,7 @@
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor'; import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor';
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor'; import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor';
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces'; import { DataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
import { LookupResponse } from '@ghostfolio/common/interfaces'; import { LookupResponse } from '@ghostfolio/common/interfaces';
import type { RequestWithUser } from '@ghostfolio/common/types'; import type { RequestWithUser } from '@ghostfolio/common/types';
@ -97,7 +97,7 @@ export class SymbolController {
@Param('dataSource') dataSource: DataSource, @Param('dataSource') dataSource: DataSource,
@Param('dateString') dateString: string, @Param('dateString') dateString: string,
@Param('symbol') symbol: string @Param('symbol') symbol: string
): Promise<IDataProviderHistoricalResponse> { ): Promise<DataProviderHistoricalResponse> {
const date = parseISO(dateString); const date = parseISO(dateString);
if (!isDate(date)) { if (!isDate(date)) {

10
apps/api/src/app/symbol/symbol.service.ts

@ -1,7 +1,7 @@
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { import {
IDataGatheringItem, DataGatheringItem,
IDataProviderHistoricalResponse DataProviderHistoricalResponse
} from '@ghostfolio/api/services/interfaces/interfaces'; } from '@ghostfolio/api/services/interfaces/interfaces';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { DATE_FORMAT } from '@ghostfolio/common/helper';
@ -27,7 +27,7 @@ export class SymbolService {
dataGatheringItem, dataGatheringItem,
includeHistoricalData includeHistoricalData
}: { }: {
dataGatheringItem: IDataGatheringItem; dataGatheringItem: DataGatheringItem;
includeHistoricalData?: number; includeHistoricalData?: number;
}): Promise<SymbolItem> { }): Promise<SymbolItem> {
const quotes = await this.dataProviderService.getQuotes({ const quotes = await this.dataProviderService.getQuotes({
@ -75,10 +75,10 @@ export class SymbolService {
dataSource, dataSource,
date = new Date(), date = new Date(),
symbol symbol
}: IDataGatheringItem): Promise<IDataProviderHistoricalResponse> { }: DataGatheringItem): Promise<DataProviderHistoricalResponse> {
let historicalData: { let historicalData: {
[symbol: string]: { [symbol: string]: {
[date: string]: IDataProviderHistoricalResponse; [date: string]: DataProviderHistoricalResponse;
}; };
} = { } = {
[symbol]: {} [symbol]: {}

3
apps/api/src/dependencies.ts

@ -0,0 +1,3 @@
// Dependencies required by .config/prisma.ts in Docker container
import 'dotenv';
import 'dotenv-expand';

1
apps/api/src/models/interfaces/rule-settings.interface.ts

@ -1,3 +1,4 @@
export interface RuleSettings { export interface RuleSettings {
isActive: boolean; isActive: boolean;
locale: string;
} }

7
apps/api/src/models/rules/account-cluster-risk/current-investment.ts

@ -121,9 +121,14 @@ export class AccountClusterRiskCurrentInvestment extends Rule<Settings> {
}); });
} }
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { public getSettings({
baseCurrency,
locale,
xRayRules
}: UserSettings): Settings {
return { return {
baseCurrency, baseCurrency,
locale,
isActive: xRayRules?.[this.getKey()]?.isActive ?? true, isActive: xRayRules?.[this.getKey()]?.isActive ?? true,
thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.5 thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.5
}; };

3
apps/api/src/models/rules/account-cluster-risk/single-account.ts

@ -72,8 +72,9 @@ export class AccountClusterRiskSingleAccount extends Rule<RuleSettings> {
}); });
} }
public getSettings({ xRayRules }: UserSettings): RuleSettings { public getSettings({ locale, xRayRules }: UserSettings): RuleSettings {
return { return {
locale,
isActive: xRayRules?.[this.getKey()]?.isActive ?? true isActive: xRayRules?.[this.getKey()]?.isActive ?? true
}; };
} }

7
apps/api/src/models/rules/asset-class-cluster-risk/equity.ts

@ -109,9 +109,14 @@ export class AssetClassClusterRiskEquity extends Rule<Settings> {
}); });
} }
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { public getSettings({
baseCurrency,
locale,
xRayRules
}: UserSettings): Settings {
return { return {
baseCurrency, baseCurrency,
locale,
isActive: xRayRules?.[this.getKey()]?.isActive ?? true, isActive: xRayRules?.[this.getKey()]?.isActive ?? true,
thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.82, thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.82,
thresholdMin: xRayRules?.[this.getKey()]?.thresholdMin ?? 0.78 thresholdMin: xRayRules?.[this.getKey()]?.thresholdMin ?? 0.78

7
apps/api/src/models/rules/asset-class-cluster-risk/fixed-income.ts

@ -109,9 +109,14 @@ export class AssetClassClusterRiskFixedIncome extends Rule<Settings> {
}); });
} }
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { public getSettings({
baseCurrency,
locale,
xRayRules
}: UserSettings): Settings {
return { return {
baseCurrency, baseCurrency,
locale,
isActive: xRayRules?.[this.getKey()]?.isActive ?? true, isActive: xRayRules?.[this.getKey()]?.isActive ?? true,
thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.22, thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.22,
thresholdMin: xRayRules?.[this.getKey()]?.thresholdMin ?? 0.18 thresholdMin: xRayRules?.[this.getKey()]?.thresholdMin ?? 0.18

7
apps/api/src/models/rules/currency-cluster-risk/base-currency-current-investment.ts

@ -97,9 +97,14 @@ export class CurrencyClusterRiskBaseCurrencyCurrentInvestment extends Rule<Setti
}); });
} }
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { public getSettings({
baseCurrency,
locale,
xRayRules
}: UserSettings): Settings {
return { return {
baseCurrency, baseCurrency,
locale,
isActive: xRayRules?.[this.getKey()]?.isActive ?? true isActive: xRayRules?.[this.getKey()]?.isActive ?? true
}; };
} }

7
apps/api/src/models/rules/currency-cluster-risk/current-investment.ts

@ -98,9 +98,14 @@ export class CurrencyClusterRiskCurrentInvestment extends Rule<Settings> {
}); });
} }
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { public getSettings({
baseCurrency,
locale,
xRayRules
}: UserSettings): Settings {
return { return {
baseCurrency, baseCurrency,
locale,
isActive: xRayRules?.[this.getKey()]?.isActive ?? true, isActive: xRayRules?.[this.getKey()]?.isActive ?? true,
thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.5 thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.5
}; };

7
apps/api/src/models/rules/economic-market-cluster-risk/developed-markets.ts

@ -104,9 +104,14 @@ export class EconomicMarketClusterRiskDevelopedMarkets extends Rule<Settings> {
}); });
} }
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { public getSettings({
baseCurrency,
locale,
xRayRules
}: UserSettings): Settings {
return { return {
baseCurrency, baseCurrency,
locale,
isActive: xRayRules?.[this.getKey()]?.isActive ?? true, isActive: xRayRules?.[this.getKey()]?.isActive ?? true,
thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.72, thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.72,
thresholdMin: xRayRules?.[this.getKey()]?.thresholdMin ?? 0.68 thresholdMin: xRayRules?.[this.getKey()]?.thresholdMin ?? 0.68

7
apps/api/src/models/rules/economic-market-cluster-risk/emerging-markets.ts

@ -104,9 +104,14 @@ export class EconomicMarketClusterRiskEmergingMarkets extends Rule<Settings> {
}); });
} }
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { public getSettings({
baseCurrency,
locale,
xRayRules
}: UserSettings): Settings {
return { return {
baseCurrency, baseCurrency,
locale,
isActive: xRayRules?.[this.getKey()]?.isActive ?? true, isActive: xRayRules?.[this.getKey()]?.isActive ?? true,
thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.32, thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.32,
thresholdMin: xRayRules?.[this.getKey()]?.thresholdMin ?? 0.28 thresholdMin: xRayRules?.[this.getKey()]?.thresholdMin ?? 0.28

7
apps/api/src/models/rules/emergency-fund/emergency-fund-setup.ts

@ -59,9 +59,14 @@ export class EmergencyFundSetup extends Rule<Settings> {
}); });
} }
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { public getSettings({
baseCurrency,
locale,
xRayRules
}: UserSettings): Settings {
return { return {
baseCurrency, baseCurrency,
locale,
isActive: xRayRules?.[this.getKey()]?.isActive ?? true isActive: xRayRules?.[this.getKey()]?.isActive ?? true
}; };
} }

7
apps/api/src/models/rules/fees/fee-ratio-initial-investment.ts

@ -82,9 +82,14 @@ export class FeeRatioInitialInvestment extends Rule<Settings> {
}); });
} }
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { public getSettings({
baseCurrency,
locale,
xRayRules
}: UserSettings): Settings {
return { return {
baseCurrency, baseCurrency,
locale,
isActive: xRayRules?.[this.getKey()]?.isActive ?? true, isActive: xRayRules?.[this.getKey()]?.isActive ?? true,
thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.01 thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.01
}; };

15
apps/api/src/models/rules/liquidity/buying-power.ts

@ -40,7 +40,9 @@ export class BuyingPower extends Rule<Settings> {
languageCode: this.getLanguageCode(), languageCode: this.getLanguageCode(),
placeholders: { placeholders: {
baseCurrency: ruleSettings.baseCurrency, baseCurrency: ruleSettings.baseCurrency,
thresholdMin: ruleSettings.thresholdMin thresholdMin: ruleSettings.thresholdMin.toLocaleString(
ruleSettings.locale
)
} }
}), }),
value: false value: false
@ -53,7 +55,9 @@ export class BuyingPower extends Rule<Settings> {
languageCode: this.getLanguageCode(), languageCode: this.getLanguageCode(),
placeholders: { placeholders: {
baseCurrency: ruleSettings.baseCurrency, baseCurrency: ruleSettings.baseCurrency,
thresholdMin: ruleSettings.thresholdMin thresholdMin: ruleSettings.thresholdMin.toLocaleString(
ruleSettings.locale
)
} }
}), }),
value: true value: true
@ -86,9 +90,14 @@ export class BuyingPower extends Rule<Settings> {
}); });
} }
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { public getSettings({
baseCurrency,
locale,
xRayRules
}: UserSettings): Settings {
return { return {
baseCurrency, baseCurrency,
locale,
isActive: xRayRules?.[this.getKey()]?.isActive ?? true, isActive: xRayRules?.[this.getKey()]?.isActive ?? true,
thresholdMin: xRayRules?.[this.getKey()]?.thresholdMin ?? 0 thresholdMin: xRayRules?.[this.getKey()]?.thresholdMin ?? 0
}; };

7
apps/api/src/models/rules/regional-market-cluster-risk/asia-pacific.ts

@ -94,9 +94,14 @@ export class RegionalMarketClusterRiskAsiaPacific extends Rule<Settings> {
}); });
} }
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { public getSettings({
baseCurrency,
locale,
xRayRules
}: UserSettings): Settings {
return { return {
baseCurrency, baseCurrency,
locale,
isActive: xRayRules?.[this.getKey()]?.isActive ?? true, isActive: xRayRules?.[this.getKey()]?.isActive ?? true,
thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.03, thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.03,
thresholdMin: xRayRules?.[this.getKey()]?.thresholdMin ?? 0.02 thresholdMin: xRayRules?.[this.getKey()]?.thresholdMin ?? 0.02

7
apps/api/src/models/rules/regional-market-cluster-risk/emerging-markets.ts

@ -96,9 +96,14 @@ export class RegionalMarketClusterRiskEmergingMarkets extends Rule<Settings> {
}); });
} }
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { public getSettings({
baseCurrency,
locale,
xRayRules
}: UserSettings): Settings {
return { return {
baseCurrency, baseCurrency,
locale,
isActive: xRayRules?.[this.getKey()]?.isActive ?? true, isActive: xRayRules?.[this.getKey()]?.isActive ?? true,
thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.12, thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.12,
thresholdMin: xRayRules?.[this.getKey()]?.thresholdMin ?? 0.08 thresholdMin: xRayRules?.[this.getKey()]?.thresholdMin ?? 0.08

7
apps/api/src/models/rules/regional-market-cluster-risk/europe.ts

@ -94,9 +94,14 @@ export class RegionalMarketClusterRiskEurope extends Rule<Settings> {
}); });
} }
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { public getSettings({
baseCurrency,
locale,
xRayRules
}: UserSettings): Settings {
return { return {
baseCurrency, baseCurrency,
locale,
isActive: xRayRules?.[this.getKey()]?.isActive ?? true, isActive: xRayRules?.[this.getKey()]?.isActive ?? true,
thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.15, thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.15,
thresholdMin: xRayRules?.[this.getKey()]?.thresholdMin ?? 0.11 thresholdMin: xRayRules?.[this.getKey()]?.thresholdMin ?? 0.11

7
apps/api/src/models/rules/regional-market-cluster-risk/japan.ts

@ -94,9 +94,14 @@ export class RegionalMarketClusterRiskJapan extends Rule<Settings> {
}); });
} }
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { public getSettings({
baseCurrency,
locale,
xRayRules
}: UserSettings): Settings {
return { return {
baseCurrency, baseCurrency,
locale,
isActive: xRayRules?.[this.getKey()]?.isActive ?? true, isActive: xRayRules?.[this.getKey()]?.isActive ?? true,
thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.06, thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.06,
thresholdMin: xRayRules?.[this.getKey()]?.thresholdMin ?? 0.04 thresholdMin: xRayRules?.[this.getKey()]?.thresholdMin ?? 0.04

7
apps/api/src/models/rules/regional-market-cluster-risk/north-america.ts

@ -94,9 +94,14 @@ export class RegionalMarketClusterRiskNorthAmerica extends Rule<Settings> {
}); });
} }
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { public getSettings({
baseCurrency,
locale,
xRayRules
}: UserSettings): Settings {
return { return {
baseCurrency, baseCurrency,
locale,
isActive: xRayRules?.[this.getKey()]?.isActive ?? true, isActive: xRayRules?.[this.getKey()]?.isActive ?? true,
thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.69, thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.69,
thresholdMin: xRayRules?.[this.getKey()]?.thresholdMin ?? 0.65 thresholdMin: xRayRules?.[this.getKey()]?.thresholdMin ?? 0.65

14
apps/api/src/services/data-provider/alpha-vantage/alpha-vantage.service.ts

@ -8,8 +8,8 @@ import {
GetSearchParams GetSearchParams
} from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface'; } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
import { import {
IDataProviderHistoricalResponse, DataProviderHistoricalResponse,
IDataProviderResponse DataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces'; } from '@ghostfolio/api/services/interfaces/interfaces';
import { DEFAULT_CURRENCY } from '@ghostfolio/common/config'; import { DEFAULT_CURRENCY } from '@ghostfolio/common/config';
import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { DATE_FORMAT } from '@ghostfolio/common/helper';
@ -23,7 +23,7 @@ import { DataSource, SymbolProfile } from '@prisma/client';
import * as Alphavantage from 'alphavantage'; import * as Alphavantage from 'alphavantage';
import { format, isAfter, isBefore, parse } from 'date-fns'; import { format, isAfter, isBefore, parse } from 'date-fns';
import { IAlphaVantageHistoricalResponse } from './interfaces/interfaces'; import { AlphaVantageHistoricalResponse } from './interfaces/interfaces';
@Injectable() @Injectable()
export class AlphaVantageService implements DataProviderInterface { export class AlphaVantageService implements DataProviderInterface {
@ -68,11 +68,11 @@ export class AlphaVantageService implements DataProviderInterface {
symbol, symbol,
to to
}: GetHistoricalParams): Promise<{ }: GetHistoricalParams): Promise<{
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; [symbol: string]: { [date: string]: DataProviderHistoricalResponse };
}> { }> {
try { try {
const historicalData: { const historicalData: {
[symbol: string]: IAlphaVantageHistoricalResponse[]; [symbol: string]: AlphaVantageHistoricalResponse[];
} = await this.alphaVantage.crypto.daily( } = await this.alphaVantage.crypto.daily(
symbol symbol
.substring(0, symbol.length - DEFAULT_CURRENCY.length) .substring(0, symbol.length - DEFAULT_CURRENCY.length)
@ -81,7 +81,7 @@ export class AlphaVantageService implements DataProviderInterface {
); );
const response: { const response: {
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; [symbol: string]: { [date: string]: DataProviderHistoricalResponse };
} = {}; } = {};
response[symbol] = {}; response[symbol] = {};
@ -115,7 +115,7 @@ export class AlphaVantageService implements DataProviderInterface {
} }
public async getQuotes({}: GetQuotesParams): Promise<{ public async getQuotes({}: GetQuotesParams): Promise<{
[symbol: string]: IDataProviderResponse; [symbol: string]: DataProviderResponse;
}> { }> {
return {}; return {};
} }

2
apps/api/src/services/data-provider/alpha-vantage/interfaces/interfaces.ts

@ -1 +1 @@
export interface IAlphaVantageHistoricalResponse {} export interface AlphaVantageHistoricalResponse {}

12
apps/api/src/services/data-provider/coingecko/coingecko.service.ts

@ -8,8 +8,8 @@ import {
GetSearchParams GetSearchParams
} from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface'; } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
import { import {
IDataProviderHistoricalResponse, DataProviderHistoricalResponse,
IDataProviderResponse DataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces'; } from '@ghostfolio/api/services/interfaces/interfaces';
import { DEFAULT_CURRENCY } from '@ghostfolio/common/config'; import { DEFAULT_CURRENCY } from '@ghostfolio/common/config';
import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { DATE_FORMAT } from '@ghostfolio/common/helper';
@ -109,7 +109,7 @@ export class CoinGeckoService implements DataProviderInterface {
symbol, symbol,
to to
}: GetHistoricalParams): Promise<{ }: GetHistoricalParams): Promise<{
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; [symbol: string]: { [date: string]: DataProviderHistoricalResponse };
}> { }> {
try { try {
const { error, prices, status } = await fetch( const { error, prices, status } = await fetch(
@ -133,7 +133,7 @@ export class CoinGeckoService implements DataProviderInterface {
} }
const result: { const result: {
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; [symbol: string]: { [date: string]: DataProviderHistoricalResponse };
} = { } = {
[symbol]: {} [symbol]: {}
}; };
@ -166,8 +166,8 @@ export class CoinGeckoService implements DataProviderInterface {
public async getQuotes({ public async getQuotes({
requestTimeout = this.configurationService.get('REQUEST_TIMEOUT'), requestTimeout = this.configurationService.get('REQUEST_TIMEOUT'),
symbols symbols
}: GetQuotesParams): Promise<{ [symbol: string]: IDataProviderResponse }> { }: GetQuotesParams): Promise<{ [symbol: string]: DataProviderResponse }> {
const response: { [symbol: string]: IDataProviderResponse } = {}; const response: { [symbol: string]: DataProviderResponse } = {};
if (symbols.length <= 0) { if (symbols.length <= 0) {
return response; return response;

24
apps/api/src/services/data-provider/data-provider.service.ts

@ -2,8 +2,8 @@ import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.s
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { DataProviderInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface'; import { DataProviderInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
import { import {
IDataProviderHistoricalResponse, DataProviderHistoricalResponse,
IDataProviderResponse DataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces'; } from '@ghostfolio/api/services/interfaces/interfaces';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
@ -215,10 +215,10 @@ export class DataProviderService implements OnModuleInit {
from: Date, from: Date,
to: Date to: Date
): Promise<{ ): Promise<{
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; [symbol: string]: { [date: string]: DataProviderHistoricalResponse };
}> { }> {
let response: { let response: {
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; [symbol: string]: { [date: string]: DataProviderHistoricalResponse };
} = {}; } = {};
if (isEmpty(aItems) || !isValid(from) || !isValid(to)) { if (isEmpty(aItems) || !isValid(from) || !isValid(to)) {
@ -284,7 +284,7 @@ export class DataProviderService implements OnModuleInit {
from: Date; from: Date;
to: Date; to: Date;
}): Promise<{ }): Promise<{
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; [symbol: string]: { [date: string]: DataProviderHistoricalResponse };
}> { }> {
for (const { currency, rootCurrency } of DERIVED_CURRENCIES) { for (const { currency, rootCurrency } of DERIVED_CURRENCIES) {
if ( if (
@ -317,11 +317,11 @@ export class DataProviderService implements OnModuleInit {
); );
const result: { const result: {
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; [symbol: string]: { [date: string]: DataProviderHistoricalResponse };
} = {}; } = {};
const promises: Promise<{ const promises: Promise<{
data: { [date: string]: IDataProviderHistoricalResponse }; data: { [date: string]: DataProviderHistoricalResponse };
symbol: string; symbol: string;
}>[] = []; }>[] = [];
for (const { dataSource, symbol } of assetProfileIdentifiers) { for (const { dataSource, symbol } of assetProfileIdentifiers) {
@ -329,7 +329,7 @@ export class DataProviderService implements OnModuleInit {
if (dataProvider.canHandle(symbol)) { if (dataProvider.canHandle(symbol)) {
if (symbol === `${DEFAULT_CURRENCY}USX`) { if (symbol === `${DEFAULT_CURRENCY}USX`) {
const data: { const data: {
[date: string]: IDataProviderHistoricalResponse; [date: string]: DataProviderHistoricalResponse;
} = {}; } = {};
for (const date of eachDayOfInterval({ end: to, start: from })) { for (const date of eachDayOfInterval({ end: to, start: from })) {
@ -399,10 +399,10 @@ export class DataProviderService implements OnModuleInit {
useCache?: boolean; useCache?: boolean;
user?: UserWithSettings; user?: UserWithSettings;
}): Promise<{ }): Promise<{
[symbol: string]: IDataProviderResponse; [symbol: string]: DataProviderResponse;
}> { }> {
const response: { const response: {
[symbol: string]: IDataProviderResponse; [symbol: string]: DataProviderResponse;
} = {}; } = {};
const startTimeTotal = performance.now(); const startTimeTotal = performance.now();
@ -716,7 +716,7 @@ export class DataProviderService implements OnModuleInit {
}: { }: {
allData: { allData: {
data: { data: {
[date: string]: IDataProviderHistoricalResponse; [date: string]: DataProviderHistoricalResponse;
}; };
symbol: string; symbol: string;
}[]; }[];
@ -728,7 +728,7 @@ export class DataProviderService implements OnModuleInit {
})?.data; })?.data;
const data: { const data: {
[date: string]: IDataProviderHistoricalResponse; [date: string]: DataProviderHistoricalResponse;
} = {}; } = {};
for (const date in rootData) { for (const date in rootData) {

14
apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts

@ -8,8 +8,8 @@ import {
GetSearchParams GetSearchParams
} from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface'; } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
import { import {
IDataProviderHistoricalResponse, DataProviderHistoricalResponse,
IDataProviderResponse DataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces'; } from '@ghostfolio/api/services/interfaces/interfaces';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import { import {
@ -89,7 +89,7 @@ export class EodHistoricalDataService implements DataProviderInterface {
symbol, symbol,
to to
}: GetDividendsParams): Promise<{ }: GetDividendsParams): Promise<{
[date: string]: IDataProviderHistoricalResponse; [date: string]: DataProviderHistoricalResponse;
}> { }> {
symbol = this.convertToEodSymbol(symbol); symbol = this.convertToEodSymbol(symbol);
@ -99,7 +99,7 @@ export class EodHistoricalDataService implements DataProviderInterface {
try { try {
const response: { const response: {
[date: string]: IDataProviderHistoricalResponse; [date: string]: DataProviderHistoricalResponse;
} = {}; } = {};
const historicalResult = await fetch( const historicalResult = await fetch(
@ -141,7 +141,7 @@ export class EodHistoricalDataService implements DataProviderInterface {
symbol, symbol,
to to
}: GetHistoricalParams): Promise<{ }: GetHistoricalParams): Promise<{
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; [symbol: string]: { [date: string]: DataProviderHistoricalResponse };
}> { }> {
symbol = this.convertToEodSymbol(symbol); symbol = this.convertToEodSymbol(symbol);
@ -198,8 +198,8 @@ export class EodHistoricalDataService implements DataProviderInterface {
public async getQuotes({ public async getQuotes({
requestTimeout = this.configurationService.get('REQUEST_TIMEOUT'), requestTimeout = this.configurationService.get('REQUEST_TIMEOUT'),
symbols symbols
}: GetQuotesParams): Promise<{ [symbol: string]: IDataProviderResponse }> { }: GetQuotesParams): Promise<{ [symbol: string]: DataProviderResponse }> {
const response: { [symbol: string]: IDataProviderResponse } = {}; const response: { [symbol: string]: DataProviderResponse } = {};
if (symbols.length <= 0) { if (symbols.length <= 0) {
return response; return response;

14
apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts

@ -9,8 +9,8 @@ import {
GetSearchParams GetSearchParams
} from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface'; } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
import { import {
IDataProviderHistoricalResponse, DataProviderHistoricalResponse,
IDataProviderResponse DataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces'; } from '@ghostfolio/api/services/interfaces/interfaces';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { import {
@ -245,7 +245,7 @@ export class FinancialModelingPrepService implements DataProviderInterface {
try { try {
const response: { const response: {
[date: string]: IDataProviderHistoricalResponse; [date: string]: DataProviderHistoricalResponse;
} = {}; } = {};
const dividends = await fetch( const dividends = await fetch(
@ -289,11 +289,11 @@ export class FinancialModelingPrepService implements DataProviderInterface {
symbol, symbol,
to to
}: GetHistoricalParams): Promise<{ }: GetHistoricalParams): Promise<{
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; [symbol: string]: { [date: string]: DataProviderHistoricalResponse };
}> { }> {
const MAX_YEARS_PER_REQUEST = 5; const MAX_YEARS_PER_REQUEST = 5;
const result: { const result: {
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; [symbol: string]: { [date: string]: DataProviderHistoricalResponse };
} = { } = {
[symbol]: {} [symbol]: {}
}; };
@ -353,8 +353,8 @@ export class FinancialModelingPrepService implements DataProviderInterface {
public async getQuotes({ public async getQuotes({
requestTimeout = this.configurationService.get('REQUEST_TIMEOUT'), requestTimeout = this.configurationService.get('REQUEST_TIMEOUT'),
symbols symbols
}: GetQuotesParams): Promise<{ [symbol: string]: IDataProviderResponse }> { }: GetQuotesParams): Promise<{ [symbol: string]: DataProviderResponse }> {
const response: { [symbol: string]: IDataProviderResponse } = {}; const response: { [symbol: string]: DataProviderResponse } = {};
if (symbols.length <= 0) { if (symbols.length <= 0) {
return response; return response;

14
apps/api/src/services/data-provider/ghostfolio/ghostfolio.service.ts

@ -9,8 +9,8 @@ import {
GetSearchParams GetSearchParams
} from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface'; } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
import { import {
IDataProviderHistoricalResponse, DataProviderHistoricalResponse,
IDataProviderResponse DataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces'; } from '@ghostfolio/api/services/interfaces/interfaces';
import { PropertyService } from '@ghostfolio/api/services/property/property.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import { import {
@ -111,10 +111,10 @@ export class GhostfolioService implements DataProviderInterface {
symbol, symbol,
to to
}: GetDividendsParams): Promise<{ }: GetDividendsParams): Promise<{
[date: string]: IDataProviderHistoricalResponse; [date: string]: DataProviderHistoricalResponse;
}> { }> {
let dividends: { let dividends: {
[date: string]: IDataProviderHistoricalResponse; [date: string]: DataProviderHistoricalResponse;
} = {}; } = {};
try { try {
@ -164,7 +164,7 @@ export class GhostfolioService implements DataProviderInterface {
symbol, symbol,
to to
}: GetHistoricalParams): Promise<{ }: GetHistoricalParams): Promise<{
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; [symbol: string]: { [date: string]: DataProviderHistoricalResponse };
}> { }> {
try { try {
const response = await fetch( const response = await fetch(
@ -228,9 +228,9 @@ export class GhostfolioService implements DataProviderInterface {
requestTimeout = this.configurationService.get('REQUEST_TIMEOUT'), requestTimeout = this.configurationService.get('REQUEST_TIMEOUT'),
symbols symbols
}: GetQuotesParams): Promise<{ }: GetQuotesParams): Promise<{
[symbol: string]: IDataProviderResponse; [symbol: string]: DataProviderResponse;
}> { }> {
let quotes: { [symbol: string]: IDataProviderResponse } = {}; let quotes: { [symbol: string]: DataProviderResponse } = {};
if (symbols.length <= 0) { if (symbols.length <= 0) {
return quotes; return quotes;

12
apps/api/src/services/data-provider/google-sheets/google-sheets.service.ts

@ -8,8 +8,8 @@ import {
GetSearchParams GetSearchParams
} from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface'; } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
import { import {
IDataProviderHistoricalResponse, DataProviderHistoricalResponse,
IDataProviderResponse DataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces'; } from '@ghostfolio/api/services/interfaces/interfaces';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
@ -60,7 +60,7 @@ export class GoogleSheetsService implements DataProviderInterface {
symbol, symbol,
to to
}: GetHistoricalParams): Promise<{ }: GetHistoricalParams): Promise<{
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; [symbol: string]: { [date: string]: DataProviderHistoricalResponse };
}> { }> {
try { try {
const sheet = await this.getSheet({ const sheet = await this.getSheet({
@ -71,7 +71,7 @@ export class GoogleSheetsService implements DataProviderInterface {
const rows = await sheet.getRows(); const rows = await sheet.getRows();
const historicalData: { const historicalData: {
[date: string]: IDataProviderHistoricalResponse; [date: string]: DataProviderHistoricalResponse;
} = {}; } = {};
rows rows
@ -104,8 +104,8 @@ export class GoogleSheetsService implements DataProviderInterface {
public async getQuotes({ public async getQuotes({
symbols symbols
}: GetQuotesParams): Promise<{ [symbol: string]: IDataProviderResponse }> { }: GetQuotesParams): Promise<{ [symbol: string]: DataProviderResponse }> {
const response: { [symbol: string]: IDataProviderResponse } = {}; const response: { [symbol: string]: DataProviderResponse } = {};
if (symbols.length <= 0) { if (symbols.length <= 0) {
return response; return response;

10
apps/api/src/services/data-provider/interfaces/data-provider.interface.ts

@ -1,6 +1,6 @@
import { import {
IDataProviderHistoricalResponse, DataProviderHistoricalResponse,
IDataProviderResponse DataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces'; } from '@ghostfolio/api/services/interfaces/interfaces';
import { import {
DataProviderInfo, DataProviderInfo,
@ -26,7 +26,7 @@ export interface DataProviderInterface {
symbol, symbol,
to to
}: GetDividendsParams): Promise<{ }: GetDividendsParams): Promise<{
[date: string]: IDataProviderHistoricalResponse; [date: string]: DataProviderHistoricalResponse;
}>; }>;
getHistorical({ getHistorical({
@ -36,7 +36,7 @@ export interface DataProviderInterface {
symbol, symbol,
to to
}: GetHistoricalParams): Promise<{ }: GetHistoricalParams): Promise<{
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; [symbol: string]: { [date: string]: DataProviderHistoricalResponse };
}>; // TODO: Return only one symbol }>; // TODO: Return only one symbol
getMaxNumberOfSymbolsPerRequest?(): number; getMaxNumberOfSymbolsPerRequest?(): number;
@ -46,7 +46,7 @@ export interface DataProviderInterface {
getQuotes({ getQuotes({
requestTimeout, requestTimeout,
symbols symbols
}: GetQuotesParams): Promise<{ [symbol: string]: IDataProviderResponse }>; }: GetQuotesParams): Promise<{ [symbol: string]: DataProviderResponse }>;
getTestSymbol(): string; getTestSymbol(): string;

12
apps/api/src/services/data-provider/manual/manual.service.ts

@ -8,8 +8,8 @@ import {
GetSearchParams GetSearchParams
} from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface'; } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
import { import {
IDataProviderHistoricalResponse, DataProviderHistoricalResponse,
IDataProviderResponse DataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces'; } from '@ghostfolio/api/services/interfaces/interfaces';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
@ -77,7 +77,7 @@ export class ManualService implements DataProviderInterface {
symbol, symbol,
to to
}: GetHistoricalParams): Promise<{ }: GetHistoricalParams): Promise<{
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; [symbol: string]: { [date: string]: DataProviderHistoricalResponse };
}> { }> {
try { try {
const [symbolProfile] = await this.symbolProfileService.getSymbolProfiles( const [symbolProfile] = await this.symbolProfileService.getSymbolProfiles(
@ -88,7 +88,7 @@ export class ManualService implements DataProviderInterface {
if (defaultMarketPrice) { if (defaultMarketPrice) {
const historical: { const historical: {
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; [symbol: string]: { [date: string]: DataProviderHistoricalResponse };
} = { } = {
[symbol]: {} [symbol]: {}
}; };
@ -132,8 +132,8 @@ export class ManualService implements DataProviderInterface {
public async getQuotes({ public async getQuotes({
symbols symbols
}: GetQuotesParams): Promise<{ [symbol: string]: IDataProviderResponse }> { }: GetQuotesParams): Promise<{ [symbol: string]: DataProviderResponse }> {
const response: { [symbol: string]: IDataProviderResponse } = {}; const response: { [symbol: string]: DataProviderResponse } = {};
if (symbols.length <= 0) { if (symbols.length <= 0) {
return response; return response;

2
apps/api/src/services/data-provider/rapid-api/interfaces/interfaces.ts

@ -1 +1 @@
export interface IRapidApiResponse {} export interface RapidApiResponse {}

8
apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts

@ -8,8 +8,8 @@ import {
GetSearchParams GetSearchParams
} from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface'; } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
import { import {
IDataProviderHistoricalResponse, DataProviderHistoricalResponse,
IDataProviderResponse DataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces'; } from '@ghostfolio/api/services/interfaces/interfaces';
import { import {
ghostfolioFearAndGreedIndexSymbol, ghostfolioFearAndGreedIndexSymbol,
@ -59,7 +59,7 @@ export class RapidApiService implements DataProviderInterface {
symbol, symbol,
to to
}: GetHistoricalParams): Promise<{ }: GetHistoricalParams): Promise<{
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; [symbol: string]: { [date: string]: DataProviderHistoricalResponse };
}> { }> {
try { try {
if ( if (
@ -96,7 +96,7 @@ export class RapidApiService implements DataProviderInterface {
public async getQuotes({ public async getQuotes({
symbols symbols
}: GetQuotesParams): Promise<{ [symbol: string]: IDataProviderResponse }> { }: GetQuotesParams): Promise<{ [symbol: string]: DataProviderResponse }> {
if (symbols.length <= 0) { if (symbols.length <= 0) {
return {}; return {};
} }

14
apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts

@ -10,8 +10,8 @@ import {
GetSearchParams GetSearchParams
} from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface'; } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
import { import {
IDataProviderHistoricalResponse, DataProviderHistoricalResponse,
IDataProviderResponse DataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces'; } from '@ghostfolio/api/services/interfaces/interfaces';
import { DEFAULT_CURRENCY } from '@ghostfolio/common/config'; import { DEFAULT_CURRENCY } from '@ghostfolio/common/config';
import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { DATE_FORMAT } from '@ghostfolio/common/helper';
@ -96,7 +96,7 @@ export class YahooFinanceService implements DataProviderInterface {
) )
); );
const response: { const response: {
[date: string]: IDataProviderHistoricalResponse; [date: string]: DataProviderHistoricalResponse;
} = {}; } = {};
for (const historicalItem of historicalResult) { for (const historicalItem of historicalResult) {
@ -124,7 +124,7 @@ export class YahooFinanceService implements DataProviderInterface {
symbol, symbol,
to to
}: GetHistoricalParams): Promise<{ }: GetHistoricalParams): Promise<{
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; [symbol: string]: { [date: string]: DataProviderHistoricalResponse };
}> { }> {
if (isSameDay(from, to)) { if (isSameDay(from, to)) {
to = addDays(to, 1); to = addDays(to, 1);
@ -145,7 +145,7 @@ export class YahooFinanceService implements DataProviderInterface {
); );
const response: { const response: {
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; [symbol: string]: { [date: string]: DataProviderHistoricalResponse };
} = {}; } = {};
response[symbol] = {}; response[symbol] = {};
@ -183,8 +183,8 @@ export class YahooFinanceService implements DataProviderInterface {
public async getQuotes({ public async getQuotes({
symbols symbols
}: GetQuotesParams): Promise<{ [symbol: string]: IDataProviderResponse }> { }: GetQuotesParams): Promise<{ [symbol: string]: DataProviderResponse }> {
const response: { [symbol: string]: IDataProviderResponse } = {}; const response: { [symbol: string]: DataProviderResponse } = {};
if (symbols.length <= 0) { if (symbols.length <= 0) {
return response; return response;

4
apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts

@ -1,6 +1,6 @@
import { LogPerformance } from '@ghostfolio/api/interceptors/performance-logging/performance-logging.interceptor'; import { LogPerformance } from '@ghostfolio/api/interceptors/performance-logging/performance-logging.interceptor';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces'; import { DataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service';
@ -29,7 +29,7 @@ import ms from 'ms';
@Injectable() @Injectable()
export class ExchangeRateDataService { export class ExchangeRateDataService {
private currencies: string[] = []; private currencies: string[] = [];
private currencyPairs: IDataGatheringItem[] = []; private currencyPairs: DataGatheringItem[] = [];
private exchangeRates: { [currencyPair: string]: number } = {}; private exchangeRates: { [currencyPair: string]: number } = {};
public constructor( public constructor(

6
apps/api/src/services/interfaces/interfaces.ts

@ -6,11 +6,11 @@ import { MarketState } from '@ghostfolio/common/types';
import { DataSource } from '@prisma/client'; import { DataSource } from '@prisma/client';
export interface IDataProviderHistoricalResponse { export interface DataProviderHistoricalResponse {
marketPrice: number; marketPrice: number;
} }
export interface IDataProviderResponse { export interface DataProviderResponse {
currency: string; currency: string;
dataProviderInfo?: DataProviderInfo; dataProviderInfo?: DataProviderInfo;
dataSource: DataSource; dataSource: DataSource;
@ -18,6 +18,6 @@ export interface IDataProviderResponse {
marketState: MarketState; marketState: MarketState;
} }
export interface IDataGatheringItem extends AssetProfileIdentifier { export interface DataGatheringItem extends AssetProfileIdentifier {
date?: Date; date?: Date;
} }

4
apps/api/src/services/market-data/market-data.service.ts

@ -1,6 +1,6 @@
import { UpdateMarketDataDto } from '@ghostfolio/api/app/admin/update-market-data.dto'; import { UpdateMarketDataDto } from '@ghostfolio/api/app/admin/update-market-data.dto';
import { DateQuery } from '@ghostfolio/api/app/portfolio/interfaces/date-query.interface'; import { DateQuery } from '@ghostfolio/api/app/portfolio/interfaces/date-query.interface';
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces'; import { DataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { resetHours } from '@ghostfolio/common/helper'; import { resetHours } from '@ghostfolio/common/helper';
import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces'; import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
@ -30,7 +30,7 @@ export class MarketDataService {
dataSource, dataSource,
date = new Date(), date = new Date(),
symbol symbol
}: IDataGatheringItem): Promise<MarketData> { }: DataGatheringItem): Promise<MarketData> {
return await this.prismaService.marketData.findFirst({ return await this.prismaService.marketData.findFirst({
where: { where: {
dataSource, dataSource,

4
apps/api/src/services/queues/data-gathering/data-gathering.processor.ts

@ -1,6 +1,6 @@
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { AssetProfileDelistedError } from '@ghostfolio/api/services/data-provider/errors/asset-profile-delisted.error'; import { AssetProfileDelistedError } from '@ghostfolio/api/services/data-provider/errors/asset-profile-delisted.error';
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces'; import { DataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import { import {
@ -99,7 +99,7 @@ export class DataGatheringProcessor {
), ),
name: GATHER_HISTORICAL_MARKET_DATA_PROCESS_JOB_NAME name: GATHER_HISTORICAL_MARKET_DATA_PROCESS_JOB_NAME
}) })
public async gatherHistoricalMarketData(job: Job<IDataGatheringItem>) { public async gatherHistoricalMarketData(job: Job<DataGatheringItem>) {
const { dataSource, date, symbol } = job.data; const { dataSource, date, symbol } = job.data;
try { try {

12
apps/api/src/services/queues/data-gathering/data-gathering.service.ts

@ -1,7 +1,7 @@
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface'; 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 { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces'; import { DataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service';
@ -94,7 +94,7 @@ export class DataGatheringService {
}); });
} }
public async gatherSymbol({ dataSource, date, symbol }: IDataGatheringItem) { public async gatherSymbol({ dataSource, date, symbol }: DataGatheringItem) {
await this.marketDataService.deleteMany({ dataSource, symbol }); await this.marketDataService.deleteMany({ dataSource, symbol });
const dataGatheringItems = (await this.getSymbolsMax()) const dataGatheringItems = (await this.getSymbolsMax())
@ -276,7 +276,7 @@ export class DataGatheringService {
dataGatheringItems, dataGatheringItems,
priority priority
}: { }: {
dataGatheringItems: IDataGatheringItem[]; dataGatheringItems: DataGatheringItem[];
priority: number; priority: number;
}) { }) {
await this.addJobsToQueue( await this.addJobsToQueue(
@ -348,7 +348,7 @@ export class DataGatheringService {
}); });
} }
private async getCurrencies7D(): Promise<IDataGatheringItem[]> { private async getCurrencies7D(): Promise<DataGatheringItem[]> {
const assetProfileIdentifiersWithCompleteMarketData = const assetProfileIdentifiersWithCompleteMarketData =
await this.getAssetProfileIdentifiersWithCompleteMarketData(); await this.getAssetProfileIdentifiersWithCompleteMarketData();
@ -376,7 +376,7 @@ export class DataGatheringService {
withUserSubscription = false withUserSubscription = false
}: { }: {
withUserSubscription?: boolean; withUserSubscription?: boolean;
}): Promise<IDataGatheringItem[]> { }): Promise<DataGatheringItem[]> {
const symbolProfiles = const symbolProfiles =
await this.symbolProfileService.getActiveSymbolProfilesByUserSubscription( await this.symbolProfileService.getActiveSymbolProfilesByUserSubscription(
{ {
@ -407,7 +407,7 @@ export class DataGatheringService {
}); });
} }
private async getSymbolsMax(): Promise<IDataGatheringItem[]> { private async getSymbolsMax(): Promise<DataGatheringItem[]> {
const benchmarkAssetProfileIdMap: { [key: string]: boolean } = {}; const benchmarkAssetProfileIdMap: { [key: string]: boolean } = {};
( (
(await this.propertyService.getByKey<BenchmarkProperty[]>( (await this.propertyService.getByKey<BenchmarkProperty[]>(

2
apps/api/src/services/queues/portfolio-snapshot/interfaces/portfolio-snapshot-queue-job.interface.ts

@ -1,7 +1,7 @@
import { Filter } from '@ghostfolio/common/interfaces'; import { Filter } from '@ghostfolio/common/interfaces';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
export interface IPortfolioSnapshotQueueJob { export interface PortfolioSnapshotQueueJob {
calculationType: PerformanceCalculationType; calculationType: PerformanceCalculationType;
filters: Filter[]; filters: Filter[];
userCurrency: string; userCurrency: string;

6
apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.processor.ts

@ -16,7 +16,7 @@ import { Injectable, Logger } from '@nestjs/common';
import { Job } from 'bull'; import { Job } from 'bull';
import { addMilliseconds } from 'date-fns'; import { addMilliseconds } from 'date-fns';
import { IPortfolioSnapshotQueueJob } from './interfaces/portfolio-snapshot-queue-job.interface'; import { PortfolioSnapshotQueueJob } from './interfaces/portfolio-snapshot-queue-job.interface';
@Injectable() @Injectable()
@Processor(PORTFOLIO_SNAPSHOT_COMPUTATION_QUEUE) @Processor(PORTFOLIO_SNAPSHOT_COMPUTATION_QUEUE)
@ -37,9 +37,7 @@ export class PortfolioSnapshotProcessor {
), ),
name: PORTFOLIO_SNAPSHOT_PROCESS_JOB_NAME name: PORTFOLIO_SNAPSHOT_PROCESS_JOB_NAME
}) })
public async calculatePortfolioSnapshot( public async calculatePortfolioSnapshot(job: Job<PortfolioSnapshotQueueJob>) {
job: Job<IPortfolioSnapshotQueueJob>
) {
try { try {
const startTime = performance.now(); const startTime = performance.now();

4
apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock.ts

@ -1,13 +1,13 @@
import { Job, JobOptions } from 'bull'; import { Job, JobOptions } from 'bull';
import { setTimeout } from 'timers/promises'; import { setTimeout } from 'timers/promises';
import { IPortfolioSnapshotQueueJob } from './interfaces/portfolio-snapshot-queue-job.interface'; import { PortfolioSnapshotQueueJob } from './interfaces/portfolio-snapshot-queue-job.interface';
export const PortfolioSnapshotServiceMock = { export const PortfolioSnapshotServiceMock = {
addJobToQueue({ addJobToQueue({
opts opts
}: { }: {
data: IPortfolioSnapshotQueueJob; data: PortfolioSnapshotQueueJob;
name: string; name: string;
opts?: JobOptions; opts?: JobOptions;
}): Promise<Job<any>> { }): Promise<Job<any>> {

4
apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.service.ts

@ -4,7 +4,7 @@ import { InjectQueue } from '@nestjs/bull';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { JobOptions, Queue } from 'bull'; import { JobOptions, Queue } from 'bull';
import { IPortfolioSnapshotQueueJob } from './interfaces/portfolio-snapshot-queue-job.interface'; import { PortfolioSnapshotQueueJob } from './interfaces/portfolio-snapshot-queue-job.interface';
@Injectable() @Injectable()
export class PortfolioSnapshotService { export class PortfolioSnapshotService {
@ -18,7 +18,7 @@ export class PortfolioSnapshotService {
name, name,
opts opts
}: { }: {
data: IPortfolioSnapshotQueueJob; data: PortfolioSnapshotQueueJob;
name: string; name: string;
opts?: JobOptions; opts?: JobOptions;
}) { }) {

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

@ -18,6 +18,7 @@ import {
ScraperConfiguration, ScraperConfiguration,
User User
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { DateRange } from '@ghostfolio/common/types';
import { GfCurrencySelectorComponent } from '@ghostfolio/ui/currency-selector'; import { GfCurrencySelectorComponent } from '@ghostfolio/ui/currency-selector';
import { GfEntityLogoComponent } from '@ghostfolio/ui/entity-logo'; import { GfEntityLogoComponent } from '@ghostfolio/ui/entity-logo';
import { GfHistoricalMarketDataEditorComponent } from '@ghostfolio/ui/historical-market-data-editor'; import { GfHistoricalMarketDataEditorComponent } from '@ghostfolio/ui/historical-market-data-editor';
@ -190,6 +191,32 @@ export class GfAssetProfileDialogComponent implements OnDestroy, OnInit {
}; };
public currencies: string[] = []; public currencies: string[] = [];
public dateRangeOptions = [
{
label: $localize`Current week` + ' (' + $localize`WTD` + ')',
value: 'wtd'
},
{
label: $localize`Current month` + ' (' + $localize`MTD` + ')',
value: 'mtd'
},
{
label: $localize`Current year` + ' (' + $localize`YTD` + ')',
value: 'ytd'
},
{
label: '1 ' + $localize`year` + ' (' + $localize`1Y` + ')',
value: '1y'
},
{
label: '5 ' + $localize`years` + ' (' + $localize`5Y` + ')',
value: '5y'
},
{
label: $localize`Max`,
value: 'max'
}
];
public historicalDataItems: LineChartItem[]; public historicalDataItems: LineChartItem[];
public isBenchmark = false; public isBenchmark = false;
public isDataGatheringEnabled: boolean; public isDataGatheringEnabled: boolean;
@ -405,9 +432,15 @@ export class GfAssetProfileDialogComponent implements OnDestroy, OnInit {
.subscribe(); .subscribe();
} }
public onGatherSymbol({ dataSource, symbol }: AssetProfileIdentifier) { public onGatherSymbol({
dataSource,
range,
symbol
}: {
range?: DateRange;
} & AssetProfileIdentifier) {
this.adminService this.adminService
.gatherSymbol({ dataSource, symbol }) .gatherSymbol({ dataSource, range, symbol })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe(); .subscribe();
} }

18
apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html

@ -26,12 +26,30 @@
[disabled]=" [disabled]="
assetProfileForm.dirty || !assetProfileForm.controls.isActive.value assetProfileForm.dirty || !assetProfileForm.controls.isActive.value
" "
[matMenuTriggerFor]="gatherHistoricalMarketDataMenu"
(click)=" (click)="
onGatherSymbol({ dataSource: data.dataSource, symbol: data.symbol }) onGatherSymbol({ dataSource: data.dataSource, symbol: data.symbol })
" "
> >
<ng-container i18n>Gather Historical Market Data</ng-container> <ng-container i18n>Gather Historical Market Data</ng-container>
</button> </button>
<mat-menu #gatherHistoricalMarketDataMenu="matMenu">
@for (dateRange of dateRangeOptions; track dateRange.value) {
<button
mat-menu-item
type="button"
(click)="
onGatherSymbol({
dataSource: data.dataSource,
range: dateRange.value,
symbol: data.symbol
})
"
>
{{ dateRange.label }}
</button>
}
</mat-menu>
<button <button
mat-menu-item mat-menu-item
type="button" type="button"

70
apps/client/src/app/components/admin-users/admin-users.component.ts

@ -19,6 +19,7 @@ import {
ViewChild ViewChild
} from '@angular/core'; } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatDialog } from '@angular/material/dialog';
import { MatMenuModule } from '@angular/material/menu'; import { MatMenuModule } from '@angular/material/menu';
import { import {
MatPaginator, MatPaginator,
@ -26,6 +27,7 @@ import {
PageEvent PageEvent
} from '@angular/material/paginator'; } from '@angular/material/paginator';
import { MatTableDataSource, MatTableModule } from '@angular/material/table'; import { MatTableDataSource, MatTableModule } from '@angular/material/table';
import { ActivatedRoute, Router } from '@angular/router';
import { IonIcon } from '@ionic/angular/standalone'; import { IonIcon } from '@ionic/angular/standalone';
import { import {
differenceInSeconds, differenceInSeconds,
@ -37,8 +39,10 @@ import {
contractOutline, contractOutline,
ellipsisHorizontal, ellipsisHorizontal,
keyOutline, keyOutline,
personOutline,
trashOutline trashOutline
} from 'ionicons/icons'; } from 'ionicons/icons';
import { DeviceDetectorService } from 'ngx-device-detector';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
@ -49,6 +53,8 @@ import { AdminService } from '../../services/admin.service';
import { DataService } from '../../services/data.service'; import { DataService } from '../../services/data.service';
import { ImpersonationStorageService } from '../../services/impersonation-storage.service'; import { ImpersonationStorageService } from '../../services/impersonation-storage.service';
import { UserService } from '../../services/user/user.service'; import { UserService } from '../../services/user/user.service';
import { UserDetailDialogParams } from '../user-detail-dialog/interfaces/interfaces';
import { GfUserDetailDialogComponent } from '../user-detail-dialog/user-detail-dialog.component';
@Component({ @Component({
imports: [ imports: [
@ -71,6 +77,7 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit {
public dataSource = new MatTableDataSource<AdminUsers['users'][0]>(); public dataSource = new MatTableDataSource<AdminUsers['users'][0]>();
public defaultDateFormat: string; public defaultDateFormat: string;
public deviceType: string;
public displayedColumns: string[] = []; public displayedColumns: string[] = [];
public getEmojiFlag = getEmojiFlag; public getEmojiFlag = getEmojiFlag;
public hasPermissionForSubscription: boolean; public hasPermissionForSubscription: boolean;
@ -87,11 +94,16 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit {
private adminService: AdminService, private adminService: AdminService,
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService, private dataService: DataService,
private deviceService: DeviceDetectorService,
private dialog: MatDialog,
private impersonationStorageService: ImpersonationStorageService, private impersonationStorageService: ImpersonationStorageService,
private notificationService: NotificationService, private notificationService: NotificationService,
private route: ActivatedRoute,
private router: Router,
private tokenStorageService: TokenStorageService, private tokenStorageService: TokenStorageService,
private userService: UserService private userService: UserService
) { ) {
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
this.info = this.dataService.fetchInfo(); this.info = this.dataService.fetchInfo();
this.hasPermissionForSubscription = hasPermission( this.hasPermissionForSubscription = hasPermission(
@ -121,6 +133,14 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit {
]; ];
} }
this.route.queryParams
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((params) => {
if (params['userDetailDialog'] && params['userId']) {
this.openUserDetailDialog(params['userId']);
}
});
this.userService.stateChanged this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe((state) => { .subscribe((state) => {
@ -138,7 +158,13 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit {
} }
}); });
addIcons({ contractOutline, ellipsisHorizontal, keyOutline, trashOutline }); addIcons({
contractOutline,
ellipsisHorizontal,
keyOutline,
personOutline,
trashOutline
});
} }
public ngOnInit() { public ngOnInit() {
@ -161,6 +187,12 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit {
return ''; return '';
} }
public onChangePage(page: PageEvent) {
this.fetchUsers({
pageIndex: page.pageIndex
});
}
public onDeleteUser(aId: string) { public onDeleteUser(aId: string) {
this.notificationService.confirm({ this.notificationService.confirm({
confirmFn: () => { confirmFn: () => {
@ -212,9 +244,9 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit {
window.location.reload(); window.location.reload();
} }
public onChangePage(page: PageEvent) { public onOpenUserDetailDialog(userId: string) {
this.fetchUsers({ this.router.navigate([], {
pageIndex: page.pageIndex queryParams: { userId, userDetailDialog: true }
}); });
} }
@ -245,4 +277,34 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit {
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
}); });
} }
private openUserDetailDialog(userId: string) {
const userData = this.dataSource.data.find(({ id }) => {
return id === userId;
});
if (!userData) {
this.router.navigate(['.'], { relativeTo: this.route });
return;
}
const dialogRef = this.dialog.open(GfUserDetailDialogComponent, {
autoFocus: false,
data: {
userData,
deviceType: this.deviceType,
locale: this.user?.settings?.locale
} as UserDetailDialogParams,
height: this.deviceType === 'mobile' ? '98vh' : '60vh',
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
});
dialogRef
.afterClosed()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
this.fetchUsers();
this.router.navigate(['.'], { relativeTo: this.route });
});
}
} }

12
apps/client/src/app/components/admin-users/admin-users.html

@ -216,6 +216,15 @@
<ion-icon name="ellipsis-horizontal" /> <ion-icon name="ellipsis-horizontal" />
</button> </button>
<mat-menu #userMenu="matMenu" xPosition="before"> <mat-menu #userMenu="matMenu" xPosition="before">
<button
mat-menu-item
(click)="onOpenUserDetailDialog(element.id)"
>
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="person-outline" />
<span i18n>View Details</span>
</span>
</button>
@if (hasPermissionToImpersonateAllUsers) { @if (hasPermissionToImpersonateAllUsers) {
<button mat-menu-item (click)="onImpersonateUser(element.id)"> <button mat-menu-item (click)="onImpersonateUser(element.id)">
<span class="align-items-center d-flex"> <span class="align-items-center d-flex">
@ -255,8 +264,9 @@
></tr> ></tr>
<tr <tr
*matRowDef="let row; columns: displayedColumns" *matRowDef="let row; columns: displayedColumns"
class="mat-mdc-row" class="cursor-pointer mat-mdc-row"
mat-row mat-row
(click)="onOpenUserDetailDialog(row.id)"
></tr> ></tr>
</table> </table>
</div> </div>

57
apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html

@ -2,13 +2,14 @@
<div class="py-3" mat-dialog-content> <div class="py-3" mat-dialog-content>
<div class="align-items-center d-flex flex-column"> <div class="align-items-center d-flex flex-column">
<form class="w-100" (ngSubmit)="onLoginWithAccessToken()"> <form class="w-100">
<mat-form-field appearance="outline" class="without-hint w-100"> <mat-form-field appearance="outline" class="without-hint w-100">
<mat-label i18n>Security Token</mat-label> <mat-label i18n>Security Token</mat-label>
<input <input
matInput matInput
[formControl]="accessTokenFormControl" [formControl]="accessTokenFormControl"
[type]="isAccessTokenHidden ? 'password' : 'text'" [type]="isAccessTokenHidden ? 'password' : 'text'"
(keydown.enter)="onLoginWithAccessToken(); $event.preventDefault()"
/> />
<button <button
mat-button mat-button
@ -21,35 +22,37 @@
/> />
</button> </button>
</mat-form-field> </mat-form-field>
@if (data.hasPermissionToUseSocialLogin) {
<div class="my-3 text-center text-muted" i18n>or</div>
<div class="d-flex flex-column">
<button
class="mb-2 px-4 rounded-pill"
mat-stroked-button
(click)="onLoginWithInternetIdentity()"
>
<img
class="mr-2"
src="../assets/icons/internet-computer.svg"
style="height: 0.75rem"
/><span i18n>Sign in with Internet Identity</span>
</button>
<a
class="px-4 rounded-pill"
href="../api/v1/auth/google"
mat-stroked-button
><img
class="mr-2"
src="../assets/icons/google.svg"
style="height: 1rem"
/><span i18n>Sign in with Google</span></a
>
</div>
}
</form> </form>
@if (data.hasPermissionToUseSocialLogin) {
<div class="my-3 text-center text-muted" i18n>or</div>
<div class="d-flex flex-column">
<button
class="mb-2 px-4 rounded-pill"
mat-stroked-button
(click)="onLoginWithInternetIdentity()"
>
<img
class="mr-2"
src="../assets/icons/internet-computer.svg"
style="height: 0.75rem"
/><span i18n>Sign in with Internet Identity</span>
</button>
<a
class="px-4 rounded-pill"
href="../api/v1/auth/google"
mat-stroked-button
><img
class="mr-2"
src="../assets/icons/google.svg"
style="height: 1rem"
/><span i18n>Sign in with Google</span></a
>
</div>
}
</div> </div>
</div> </div>
<div mat-dialog-actions> <div mat-dialog-actions>
<div class="flex-grow-1"> <div class="flex-grow-1">
<mat-checkbox color="primary" i18n (change)="onChangeStaySignedIn($event)" <mat-checkbox color="primary" i18n (change)="onChangeStaySignedIn($event)"

2
apps/client/src/app/components/rule/rule-settings-dialog/interfaces/interfaces.ts

@ -3,7 +3,7 @@ import {
XRayRulesSettings XRayRulesSettings
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
export interface IRuleSettingsDialogParams { export interface RuleSettingsDialogParams {
categoryName: string; categoryName: string;
locale: string; locale: string;
rule: PortfolioReportRule; rule: PortfolioReportRule;

4
apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.component.ts

@ -12,7 +12,7 @@ import {
} from '@angular/material/dialog'; } from '@angular/material/dialog';
import { MatSliderModule } from '@angular/material/slider'; import { MatSliderModule } from '@angular/material/slider';
import { IRuleSettingsDialogParams } from './interfaces/interfaces'; import { RuleSettingsDialogParams } from './interfaces/interfaces';
@Component({ @Component({
imports: [ imports: [
@ -31,7 +31,7 @@ export class GfRuleSettingsDialogComponent {
public settings: XRayRulesSettings['AccountClusterRiskCurrentInvestment']; public settings: XRayRulesSettings['AccountClusterRiskCurrentInvestment'];
public constructor( public constructor(
@Inject(MAT_DIALOG_DATA) public data: IRuleSettingsDialogParams, @Inject(MAT_DIALOG_DATA) public data: RuleSettingsDialogParams,
public dialogRef: MatDialogRef<GfRuleSettingsDialogComponent> public dialogRef: MatDialogRef<GfRuleSettingsDialogComponent>
) {} ) {}
} }

4
apps/client/src/app/components/rule/rule.component.ts

@ -31,7 +31,7 @@ import { DeviceDetectorService } from 'ngx-device-detector';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { Subject, takeUntil } from 'rxjs'; import { Subject, takeUntil } from 'rxjs';
import { IRuleSettingsDialogParams } from './rule-settings-dialog/interfaces/interfaces'; import { RuleSettingsDialogParams } from './rule-settings-dialog/interfaces/interfaces';
import { GfRuleSettingsDialogComponent } from './rule-settings-dialog/rule-settings-dialog.component'; import { GfRuleSettingsDialogComponent } from './rule-settings-dialog/rule-settings-dialog.component';
@Component({ @Component({
@ -83,7 +83,7 @@ export class GfRuleComponent implements OnInit {
rule, rule,
categoryName: this.categoryName, categoryName: this.categoryName,
settings: this.settings settings: this.settings
} as IRuleSettingsDialogParams, } as RuleSettingsDialogParams,
width: this.deviceType === 'mobile' ? '100vw' : '50rem' width: this.deviceType === 'mobile' ? '100vw' : '50rem'
}); });

7
apps/client/src/app/components/user-account-membership/user-account-membership.component.ts

@ -108,7 +108,10 @@ export class GfUserAccountMembershipComponent implements OnDestroy {
public onCheckout() { public onCheckout() {
this.dataService this.dataService
.createCheckoutSession({ couponId: this.couponId, priceId: this.priceId }) .createStripeCheckoutSession({
couponId: this.couponId,
priceId: this.priceId
})
.pipe( .pipe(
catchError((error) => { catchError((error) => {
this.notificationService.alert({ this.notificationService.alert({
@ -117,7 +120,7 @@ export class GfUserAccountMembershipComponent implements OnDestroy {
throw error; throw error;
}), }),
switchMap(({ sessionId }: { sessionId: string }) => { switchMap(({ sessionId }) => {
return this.stripeService.redirectToCheckout({ sessionId }); return this.stripeService.redirectToCheckout({ sessionId });
}) })
) )

7
apps/client/src/app/components/user-detail-dialog/interfaces/interfaces.ts

@ -0,0 +1,7 @@
import { AdminUsers } from '@ghostfolio/common/interfaces';
export interface UserDetailDialogParams {
deviceType: string;
locale: string;
userData: AdminUsers['users'][0];
}

7
apps/client/src/app/components/user-detail-dialog/user-detail-dialog.component.scss

@ -0,0 +1,7 @@
:host {
display: block;
.mat-mdc-dialog-content {
max-height: unset;
}
}

52
apps/client/src/app/components/user-detail-dialog/user-detail-dialog.component.ts

@ -0,0 +1,52 @@
import { GfDialogFooterComponent } from '@ghostfolio/client/components/dialog-footer/dialog-footer.component';
import { GfDialogHeaderComponent } from '@ghostfolio/client/components/dialog-header/dialog-header.component';
import { GfValueComponent } from '@ghostfolio/ui/value';
import { CommonModule } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
CUSTOM_ELEMENTS_SCHEMA,
Inject,
OnDestroy
} 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 { Subject } from 'rxjs';
import { UserDetailDialogParams } from './interfaces/interfaces';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'd-flex flex-column h-100' },
imports: [
CommonModule,
GfDialogFooterComponent,
GfDialogHeaderComponent,
GfValueComponent,
MatButtonModule,
MatDialogModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
selector: 'gf-user-detail-dialog',
styleUrls: ['./user-detail-dialog.component.scss'],
templateUrl: './user-detail-dialog.html'
})
export class GfUserDetailDialogComponent implements OnDestroy {
private unsubscribeSubject = new Subject<void>();
public constructor(
@Inject(MAT_DIALOG_DATA) public data: UserDetailDialogParams,
public dialogRef: MatDialogRef<GfUserDetailDialogComponent>
) {}
public onClose() {
this.dialogRef.close();
}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
}

32
apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html

@ -0,0 +1,32 @@
<gf-dialog-header
position="center"
[deviceType]="data.deviceType"
(closeButtonClicked)="onClose()"
/>
<div class="flex-grow-1" mat-dialog-content>
<div class="container p-0">
<div class="mb-3 row">
<div class="col-6 mb-3">
<gf-value i18n size="medium" [value]="data.userData.id"
>User ID</gf-value
>
</div>
<div class="col-6 mb-3">
<gf-value
i18n
size="medium"
[isDate]="true"
[locale]="data.locale"
[value]="data.userData.createdAt"
>Registration Date</gf-value
>
</div>
</div>
</div>
</div>
<gf-dialog-footer
[deviceType]="data.deviceType"
(closeButtonClicked)="onClose()"
/>

4
apps/client/src/app/core/notification/alert-dialog/alert-dialog.component.ts

@ -2,7 +2,7 @@ import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule, MatDialogRef } from '@angular/material/dialog'; import { MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { IAlertDialogParams } from './interfaces/interfaces'; import { AlertDialogParams } from './interfaces/interfaces';
@Component({ @Component({
imports: [MatButtonModule, MatDialogModule], imports: [MatButtonModule, MatDialogModule],
@ -17,7 +17,7 @@ export class GfAlertDialogComponent {
public constructor(public dialogRef: MatDialogRef<GfAlertDialogComponent>) {} public constructor(public dialogRef: MatDialogRef<GfAlertDialogComponent>) {}
public initialize(aParams: IAlertDialogParams) { public initialize(aParams: AlertDialogParams) {
this.discardLabel = aParams.discardLabel; this.discardLabel = aParams.discardLabel;
this.message = aParams.message; this.message = aParams.message;
this.title = aParams.title; this.title = aParams.title;

2
apps/client/src/app/core/notification/alert-dialog/interfaces/interfaces.ts

@ -1,4 +1,4 @@
export interface IAlertDialogParams { export interface AlertDialogParams {
confirmLabel?: string; confirmLabel?: string;
discardLabel?: string; discardLabel?: string;
message?: string; message?: string;

4
apps/client/src/app/core/notification/confirmation-dialog/confirmation-dialog.component.ts

@ -3,7 +3,7 @@ import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule, MatDialogRef } from '@angular/material/dialog'; import { MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { ConfirmationDialogType } from './confirmation-dialog.type'; import { ConfirmationDialogType } from './confirmation-dialog.type';
import { IConfirmDialogParams } from './interfaces/interfaces'; import { ConfirmDialogParams } from './interfaces/interfaces';
@Component({ @Component({
imports: [MatButtonModule, MatDialogModule], imports: [MatButtonModule, MatDialogModule],
@ -29,7 +29,7 @@ export class GfConfirmationDialogComponent {
} }
} }
public initialize(aParams: IConfirmDialogParams) { public initialize(aParams: ConfirmDialogParams) {
this.confirmLabel = aParams.confirmLabel; this.confirmLabel = aParams.confirmLabel;
this.confirmType = aParams.confirmType; this.confirmType = aParams.confirmType;
this.discardLabel = aParams.discardLabel; this.discardLabel = aParams.discardLabel;

2
apps/client/src/app/core/notification/confirmation-dialog/interfaces/interfaces.ts

@ -1,6 +1,6 @@
import { ConfirmationDialogType } from '../confirmation-dialog.type'; import { ConfirmationDialogType } from '../confirmation-dialog.type';
export interface IConfirmDialogParams { export interface ConfirmDialogParams {
confirmLabel?: string; confirmLabel?: string;
confirmType: ConfirmationDialogType; confirmType: ConfirmationDialogType;
discardLabel?: string; discardLabel?: string;

6
apps/client/src/app/core/notification/interfaces/interfaces.ts

@ -1,13 +1,13 @@
import { ConfirmationDialogType } from '../confirmation-dialog/confirmation-dialog.type'; import { ConfirmationDialogType } from '../confirmation-dialog/confirmation-dialog.type';
export interface IAlertParams { export interface AlertParams {
discardFn?: () => void; discardFn?: () => void;
discardLabel?: string; discardLabel?: string;
message?: string; message?: string;
title: string; title: string;
} }
export interface IConfirmParams { export interface ConfirmParams {
confirmFn: () => void; confirmFn: () => void;
confirmLabel?: string; confirmLabel?: string;
confirmType?: ConfirmationDialogType; confirmType?: ConfirmationDialogType;
@ -18,7 +18,7 @@ export interface IConfirmParams {
title: string; title: string;
} }
export interface IPromptParams { export interface PromptParams {
confirmFn: (value: string) => void; confirmFn: (value: string) => void;
confirmLabel?: string; confirmLabel?: string;
defaultValue?: string; defaultValue?: string;

12
apps/client/src/app/core/notification/notification.service.ts

@ -8,9 +8,9 @@ import { GfAlertDialogComponent } from './alert-dialog/alert-dialog.component';
import { GfConfirmationDialogComponent } from './confirmation-dialog/confirmation-dialog.component'; import { GfConfirmationDialogComponent } from './confirmation-dialog/confirmation-dialog.component';
import { ConfirmationDialogType } from './confirmation-dialog/confirmation-dialog.type'; import { ConfirmationDialogType } from './confirmation-dialog/confirmation-dialog.type';
import { import {
IAlertParams, AlertParams,
IConfirmParams, ConfirmParams,
IPromptParams PromptParams
} from './interfaces/interfaces'; } from './interfaces/interfaces';
import { GfPromptDialogComponent } from './prompt-dialog/prompt-dialog.component'; import { GfPromptDialogComponent } from './prompt-dialog/prompt-dialog.component';
@ -21,7 +21,7 @@ export class NotificationService {
public constructor(private matDialog: MatDialog) {} public constructor(private matDialog: MatDialog) {}
public alert(aParams: IAlertParams) { public alert(aParams: AlertParams) {
if (!aParams.discardLabel) { if (!aParams.discardLabel) {
aParams.discardLabel = translate('CLOSE'); aParams.discardLabel = translate('CLOSE');
} }
@ -45,7 +45,7 @@ export class NotificationService {
}); });
} }
public confirm(aParams: IConfirmParams) { public confirm(aParams: ConfirmParams) {
if (!aParams.confirmLabel) { if (!aParams.confirmLabel) {
aParams.confirmLabel = translate('YES'); aParams.confirmLabel = translate('YES');
} }
@ -78,7 +78,7 @@ export class NotificationService {
}); });
} }
public prompt(aParams: IPromptParams) { public prompt(aParams: PromptParams) {
if (!aParams.confirmLabel) { if (!aParams.confirmLabel) {
aParams.confirmLabel = translate('OK'); aParams.confirmLabel = translate('OK');
} }

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save