Browse Source

Merge branch 'main' into pr/5786

pull/5786/head
Thomas Kaul 1 week 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/),
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
### 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 `PUT api/v1/portfolio/position/:dataSource/:symbol/tags`
- 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

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
# layers when files (package.json etc.) have not changed
COPY ./.config .config/
COPY ./CHANGELOG.md CHANGELOG.md
COPY ./LICENSE LICENSE
COPY ./package.json package.json
COPY ./package-lock.json package-lock.json
COPY ./prisma.config.ts prisma.config.ts
COPY ./prisma/schema.prisma prisma/
RUN npm install
@ -44,7 +44,7 @@ WORKDIR /ghostfolio/dist/apps/api
COPY ./package-lock.json /ghostfolio/dist/apps/api/
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/
# 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;
if (dateRange) {
const { startDate } = getIntervalFromDateRange(dateRange, new Date());
const { startDate } = getIntervalFromDateRange(dateRange);
date = startDate;
}

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

@ -71,7 +71,6 @@ import { UserModule } from './user/user.module';
BullModule.forRoot({
redis: {
db: parseInt(process.env.REDIS_DB ?? '0', 10),
family: 0,
host: process.env.REDIS_HOST,
password: process.env.REDIS_PASSWORD,
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 { 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 type { AdminMarketDataDetails } from '@ghostfolio/common/interfaces';
import type { AssetResponse } from '@ghostfolio/common/interfaces';
import { Controller, Get, Param, UseInterceptors } from '@nestjs/common';
import { DataSource } from '@prisma/client';
@ -17,7 +17,7 @@ export class AssetController {
public async getAsset(
@Param('dataSource') dataSource: DataSource,
@Param('symbol') symbol: string
): Promise<AdminMarketDataDetails> {
): Promise<AssetResponse> {
const { assetProfile, marketData } =
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 { createOpenRouter } from '@openrouter/ai-sdk-provider';
import { generateText } from 'ai';
import type { ColumnDescriptor } from 'tablemark';
@Injectable()
export class AiService {
@ -58,34 +59,57 @@ export class AiService {
userId
});
const holdingsTable = [
'| Name | Symbol | Currency | Asset Class | Asset Sub Class | Allocation in Percentage |',
'| --- | --- | --- | --- | --- | --- |',
...Object.values(holdings)
.sort((a, b) => {
return b.allocationInPercentage - a.allocationInPercentage;
})
.map(
({
allocationInPercentage,
assetClass,
assetSubClass,
currency,
name,
symbol
}) => {
return `| ${name} | ${symbol} | ${currency} | ${assetClass} | ${assetSubClass} | ${(allocationInPercentage * 100).toFixed(3)}% |`;
}
)
const holdingsTableColumns: ColumnDescriptor[] = [
{ name: 'Name' },
{ name: 'Symbol' },
{ name: 'Currency' },
{ name: 'Asset Class' },
{ name: 'Asset Sub Class' },
{ align: 'right', name: 'Allocation in Percentage' }
];
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') {
return holdingsTable.join('\n');
return holdingsTableString;
}
return [
`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:',
'Overview: Briefly summarize the portfolio’s composition and allocation rationale.',
'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,
GetSearchParams
} 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 { PropertyService } from '@ghostfolio/api/services/property/property.service';
import {
@ -114,7 +114,7 @@ export class GhostfolioService {
try {
const promises: Promise<{
[date: string]: IDataProviderHistoricalResponse;
[date: string]: DataProviderHistoricalResponse;
}>[] = [];
for (const dataProviderService of this.getDataProviderServices()) {
@ -156,7 +156,7 @@ export class GhostfolioService {
try {
const promises: Promise<{
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
[symbol: string]: { [date: string]: DataProviderHistoricalResponse };
}>[] = [];
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 { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
import { DataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
import {
Controller,
@ -25,7 +25,7 @@ export class ExchangeRateController {
public async getExchangeRate(
@Param('dateString') dateString: string,
@Param('symbol') symbol: string
): Promise<IDataProviderHistoricalResponse> {
): Promise<DataProviderHistoricalResponse> {
const date = parseISO(dateString);
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 { 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 { Export } from '@ghostfolio/common/interfaces';
import { ExportResponse } from '@ghostfolio/common/interfaces';
import type { RequestWithUser } from '@ghostfolio/common/types';
import {
@ -35,7 +35,7 @@ export class ExportController {
@Query('dataSource') filterByDataSource?: string,
@Query('symbol') filterBySymbol?: string,
@Query('tags') filterByTags?: string
): Promise<Export> {
): Promise<ExportResponse> {
const activityIds = filterByActivityIds?.split(',') ?? [];
const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts,
@ -48,8 +48,8 @@ export class ExportController {
return this.exportService.export({
activityIds,
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 { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.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 { Platform, Prisma } from '@prisma/client';
@ -21,14 +25,14 @@ export class ExportService {
public async export({
activityIds,
filters,
userCurrency,
userId
userId,
userSettings
}: {
activityIds?: string[];
filters?: Filter[];
userCurrency: string;
userId: string;
}): Promise<Export> {
userSettings: UserSettings;
}): Promise<ExportResponse> {
const { ACCOUNT: filtersByAccount } = groupBy(filters, ({ type }) => {
return type;
});
@ -36,11 +40,11 @@ export class ExportService {
let { activities } = await this.orderService.getOrders({
filters,
userCurrency,
userId,
includeDrafts: true,
sortColumn: 'date',
sortDirection: 'asc',
userCurrency: userSettings?.baseCurrency,
withExcludedAccountsAndActivities: true
});
@ -244,7 +248,10 @@ export class ExportService {
}
),
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: {
create: {
dataSource,
name,
symbol,
currency: assetProfile.currency,
userId: dataSource === 'MANUAL' ? user.id : undefined
@ -746,10 +747,19 @@ export class ImportService {
if (['FEE', 'INTEREST', 'LIABILITY'].includes(type)) {
// Skip asset profile validation for FEE, INTEREST, and LIABILITY
// 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 })] = {
currency,
dataSource,
symbol
symbol,
name: assetProfileInImport?.name
};
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 { InfoItem } from '@ghostfolio/common/interfaces';
import { InfoResponse } from '@ghostfolio/common/interfaces';
import { Controller, Get, UseInterceptors } from '@nestjs/common';
@ -11,7 +11,7 @@ export class InfoController {
@Get()
@UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getInfo(): Promise<InfoItem> {
public async getInfo(): Promise<InfoResponse> {
return this.infoService.get();
}
}

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

@ -44,7 +44,8 @@ export class CreateOrderDto {
customCurrency?: string;
@IsEnum(DataSource)
dataSource: DataSource;
@IsOptional() // Optional for type FEE, INTEREST, and LIABILITY (default data source is resolved in the backend)
dataSource?: DataSource;
@IsISO8601()
@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';
export interface Activities {
activities: Activity[];
count: number;
}
export interface Activity extends Order {
account?: AccountWithPlatform;
error?: ActivityError;

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

@ -11,6 +11,10 @@ import {
DATA_GATHERING_QUEUE_PRIORITY_HIGH,
HEADER_KEY_IMPERSONATION
} from '@ghostfolio/common/config';
import {
ActivitiesResponse,
ActivityResponse
} from '@ghostfolio/common/interfaces';
import { permissions } from '@ghostfolio/common/permissions';
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 { CreateOrderDto } from './create-order.dto';
import { Activities, Activity } from './interfaces/activities.interface';
import { OrderService } from './order.service';
import { UpdateOrderDto } from './update-order.dto';
@ -113,7 +116,7 @@ export class OrderController {
@Query('symbol') filterBySymbol?: string,
@Query('tags') filterByTags?: string,
@Query('take') take?: number
): Promise<Activities> {
): Promise<ActivitiesResponse> {
let endDate: Date;
let startDate: Date;
@ -157,7 +160,7 @@ export class OrderController {
public async getOrderById(
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string,
@Param('id') id: string
): Promise<Activity> {
): Promise<ActivityResponse> {
const impersonationUserId =
await this.impersonationService.validateImpersonationId(impersonationId);
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';
import { getAssetProfileIdentifier } from '@ghostfolio/common/helper';
import {
ActivitiesResponse,
AssetProfileIdentifier,
EnhancedSymbolProfile,
Filter
@ -37,8 +38,6 @@ import { endOfToday, isAfter } from 'date-fns';
import { groupBy, uniqBy } from 'lodash';
import { v4 as uuidv4 } from 'uuid';
import { Activities } from './interfaces/activities.interface';
@Injectable()
export class OrderService {
public constructor(
@ -129,7 +128,7 @@ export class OrderService {
const assetSubClass = data.assetSubClass;
const dataSource: DataSource = 'MANUAL';
let name: string;
let name = data.SymbolProfile.connectOrCreate.create.name;
let symbol: string;
if (
@ -142,7 +141,7 @@ export class OrderService {
symbol = data.SymbolProfile.connectOrCreate.create.symbol;
} else {
// Create custom asset profile
name = data.SymbolProfile.connectOrCreate.create.symbol;
name = name ?? data.SymbolProfile.connectOrCreate.create.symbol;
symbol = uuidv4();
}
@ -345,7 +344,7 @@ export class OrderService {
userCurrency: string;
userId: string;
withExcludedAccountsAndActivities?: boolean;
}): Promise<Activities> {
}): Promise<ActivitiesResponse> {
let orderBy: Prisma.Enumerable<Prisma.OrderOrderByWithRelationInput> = [
{ 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';
@ -39,6 +39,6 @@ export const userDummyData = {
id: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
};
export function loadExportFile(filePath: string): Export {
export function loadExportFile(filePath: string): ExportResponse {
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 { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.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 { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper';
import {
@ -193,7 +193,7 @@ export abstract class PortfolioCalculator {
}
const currencies: { [symbol: string]: string } = {};
const dataGatheringItems: IDataGatheringItem[] = [];
const dataGatheringItems: DataGatheringItem[] = [];
let firstIndex = transactionPoints.length;
let firstTransactionPoint: TransactionPoint = null;
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', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
CurrentRateService: jest.fn().mockImplementation(() => {
return CurrentRateServiceMock;
})
@ -31,7 +30,6 @@ jest.mock(
'@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service',
() => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
PortfolioSnapshotService: jest.fn().mockImplementation(() => {
return PortfolioSnapshotServiceMock;
})
@ -41,7 +39,6 @@ jest.mock(
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock;
})

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', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
CurrentRateService: jest.fn().mockImplementation(() => {
return CurrentRateServiceMock;
})
@ -31,7 +30,6 @@ jest.mock(
'@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service',
() => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
PortfolioSnapshotService: jest.fn().mockImplementation(() => {
return PortfolioSnapshotServiceMock;
})
@ -41,7 +39,6 @@ jest.mock(
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock;
})

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', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
CurrentRateService: jest.fn().mockImplementation(() => {
return CurrentRateServiceMock;
})
@ -31,7 +30,6 @@ jest.mock(
'@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service',
() => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
PortfolioSnapshotService: jest.fn().mockImplementation(() => {
return PortfolioSnapshotServiceMock;
})
@ -41,7 +39,6 @@ jest.mock(
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock;
})

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', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
CurrentRateService: jest.fn().mockImplementation(() => {
return CurrentRateServiceMock;
})
@ -31,7 +30,6 @@ jest.mock(
'@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service',
() => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
PortfolioSnapshotService: jest.fn().mockImplementation(() => {
return PortfolioSnapshotServiceMock;
})
@ -41,7 +39,6 @@ jest.mock(
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock;
})

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 { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock';
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 { Big } from 'big.js';
@ -23,7 +23,6 @@ import { join } from 'node:path';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
CurrentRateService: jest.fn().mockImplementation(() => {
return CurrentRateServiceMock;
})
@ -34,7 +33,6 @@ jest.mock(
'@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service',
() => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
PortfolioSnapshotService: jest.fn().mockImplementation(() => {
return PortfolioSnapshotServiceMock;
})
@ -44,7 +42,6 @@ jest.mock(
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock;
})
@ -52,7 +49,7 @@ jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
});
describe('PortfolioCalculator', () => {
let exportResponse: Export;
let exportResponse: ExportResponse;
let configurationService: ConfigurationService;
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', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
CurrentRateService: jest.fn().mockImplementation(() => {
return CurrentRateServiceMock;
})
@ -32,7 +31,6 @@ jest.mock(
'@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service',
() => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
PortfolioSnapshotService: jest.fn().mockImplementation(() => {
return PortfolioSnapshotServiceMock;
})
@ -42,7 +40,6 @@ jest.mock(
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock;
})
@ -53,7 +50,6 @@ jest.mock(
'@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service',
() => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
ExchangeRateDataService: jest.fn().mockImplementation(() => {
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 { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock';
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 { Big } from 'big.js';
@ -23,7 +23,6 @@ import { join } from 'node:path';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
CurrentRateService: jest.fn().mockImplementation(() => {
return CurrentRateServiceMock;
})
@ -34,7 +33,6 @@ jest.mock(
'@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service',
() => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
PortfolioSnapshotService: jest.fn().mockImplementation(() => {
return PortfolioSnapshotServiceMock;
})
@ -44,7 +42,6 @@ jest.mock(
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock;
})
@ -52,7 +49,7 @@ jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
});
describe('PortfolioCalculator', () => {
let exportResponse: Export;
let exportResponse: ExportResponse;
let configurationService: ConfigurationService;
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 { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock';
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 { Big } from 'big.js';
@ -23,7 +23,6 @@ import { join } from 'node:path';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
CurrentRateService: jest.fn().mockImplementation(() => {
return CurrentRateServiceMock;
})
@ -34,7 +33,6 @@ jest.mock(
'@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service',
() => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
PortfolioSnapshotService: jest.fn().mockImplementation(() => {
return PortfolioSnapshotServiceMock;
})
@ -44,7 +42,6 @@ jest.mock(
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock;
})
@ -52,7 +49,7 @@ jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
});
describe('PortfolioCalculator', () => {
let exportResponse: Export;
let exportResponse: ExportResponse;
let configurationService: ConfigurationService;
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', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
CurrentRateService: jest.fn().mockImplementation(() => {
return CurrentRateServiceMock;
})
@ -31,7 +30,6 @@ jest.mock(
'@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service',
() => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
PortfolioSnapshotService: jest.fn().mockImplementation(() => {
return PortfolioSnapshotServiceMock;
})
@ -41,7 +39,6 @@ jest.mock(
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock;
})

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', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
CurrentRateService: jest.fn().mockImplementation(() => {
return CurrentRateServiceMock;
})
@ -32,7 +31,6 @@ jest.mock(
'@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service',
() => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
PortfolioSnapshotService: jest.fn().mockImplementation(() => {
return PortfolioSnapshotServiceMock;
})
@ -42,7 +40,6 @@ jest.mock(
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock;
})
@ -53,7 +50,6 @@ jest.mock(
'@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service',
() => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
ExchangeRateDataService: jest.fn().mockImplementation(() => {
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', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
CurrentRateService: jest.fn().mockImplementation(() => {
return CurrentRateServiceMock;
})
@ -31,7 +30,6 @@ jest.mock(
'@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service',
() => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
PortfolioSnapshotService: jest.fn().mockImplementation(() => {
return PortfolioSnapshotServiceMock;
})
@ -41,7 +39,6 @@ jest.mock(
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock;
})

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

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', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
CurrentRateService: jest.fn().mockImplementation(() => {
return CurrentRateServiceMock;
})
@ -31,7 +30,6 @@ jest.mock(
'@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service',
() => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
PortfolioSnapshotService: jest.fn().mockImplementation(() => {
return PortfolioSnapshotServiceMock;
})
@ -41,7 +39,6 @@ jest.mock(
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock;
})

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', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
CurrentRateService: jest.fn().mockImplementation(() => {
return CurrentRateServiceMock;
})
@ -26,7 +25,6 @@ jest.mock(
'@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service',
() => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
PortfolioSnapshotService: jest.fn().mockImplementation(() => {
return PortfolioSnapshotServiceMock;
})
@ -36,7 +34,6 @@ jest.mock(
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock;
})

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

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';
export interface GetValuesParams {
dataGatheringItems: IDataGatheringItem[];
dataGatheringItems: DataGatheringItem[];
dateQuery: DateQuery;
}

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

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

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

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

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

@ -1,7 +1,7 @@
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 { 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 type { RequestWithUser } from '@ghostfolio/common/types';
@ -97,7 +97,7 @@ export class SymbolController {
@Param('dataSource') dataSource: DataSource,
@Param('dateString') dateString: string,
@Param('symbol') symbol: string
): Promise<IDataProviderHistoricalResponse> {
): Promise<DataProviderHistoricalResponse> {
const date = parseISO(dateString);
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 {
IDataGatheringItem,
IDataProviderHistoricalResponse
DataGatheringItem,
DataProviderHistoricalResponse
} from '@ghostfolio/api/services/interfaces/interfaces';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { DATE_FORMAT } from '@ghostfolio/common/helper';
@ -27,7 +27,7 @@ export class SymbolService {
dataGatheringItem,
includeHistoricalData
}: {
dataGatheringItem: IDataGatheringItem;
dataGatheringItem: DataGatheringItem;
includeHistoricalData?: number;
}): Promise<SymbolItem> {
const quotes = await this.dataProviderService.getQuotes({
@ -75,10 +75,10 @@ export class SymbolService {
dataSource,
date = new Date(),
symbol
}: IDataGatheringItem): Promise<IDataProviderHistoricalResponse> {
}: DataGatheringItem): Promise<DataProviderHistoricalResponse> {
let historicalData: {
[symbol: string]: {
[date: string]: IDataProviderHistoricalResponse;
[date: string]: DataProviderHistoricalResponse;
};
} = {
[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 {
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 {
baseCurrency,
locale,
isActive: xRayRules?.[this.getKey()]?.isActive ?? true,
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 {
locale,
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 {
baseCurrency,
locale,
isActive: xRayRules?.[this.getKey()]?.isActive ?? true,
thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.82,
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 {
baseCurrency,
locale,
isActive: xRayRules?.[this.getKey()]?.isActive ?? true,
thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.22,
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 {
baseCurrency,
locale,
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 {
baseCurrency,
locale,
isActive: xRayRules?.[this.getKey()]?.isActive ?? true,
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 {
baseCurrency,
locale,
isActive: xRayRules?.[this.getKey()]?.isActive ?? true,
thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.72,
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 {
baseCurrency,
locale,
isActive: xRayRules?.[this.getKey()]?.isActive ?? true,
thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.32,
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 {
baseCurrency,
locale,
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 {
baseCurrency,
locale,
isActive: xRayRules?.[this.getKey()]?.isActive ?? true,
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(),
placeholders: {
baseCurrency: ruleSettings.baseCurrency,
thresholdMin: ruleSettings.thresholdMin
thresholdMin: ruleSettings.thresholdMin.toLocaleString(
ruleSettings.locale
)
}
}),
value: false
@ -53,7 +55,9 @@ export class BuyingPower extends Rule<Settings> {
languageCode: this.getLanguageCode(),
placeholders: {
baseCurrency: ruleSettings.baseCurrency,
thresholdMin: ruleSettings.thresholdMin
thresholdMin: ruleSettings.thresholdMin.toLocaleString(
ruleSettings.locale
)
}
}),
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 {
baseCurrency,
locale,
isActive: xRayRules?.[this.getKey()]?.isActive ?? true,
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 {
baseCurrency,
locale,
isActive: xRayRules?.[this.getKey()]?.isActive ?? true,
thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.03,
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 {
baseCurrency,
locale,
isActive: xRayRules?.[this.getKey()]?.isActive ?? true,
thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.12,
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 {
baseCurrency,
locale,
isActive: xRayRules?.[this.getKey()]?.isActive ?? true,
thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.15,
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 {
baseCurrency,
locale,
isActive: xRayRules?.[this.getKey()]?.isActive ?? true,
thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.06,
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 {
baseCurrency,
locale,
isActive: xRayRules?.[this.getKey()]?.isActive ?? true,
thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.69,
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
} from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
import {
IDataProviderHistoricalResponse,
IDataProviderResponse
DataProviderHistoricalResponse,
DataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces';
import { DEFAULT_CURRENCY } from '@ghostfolio/common/config';
import { DATE_FORMAT } from '@ghostfolio/common/helper';
@ -23,7 +23,7 @@ import { DataSource, SymbolProfile } from '@prisma/client';
import * as Alphavantage from 'alphavantage';
import { format, isAfter, isBefore, parse } from 'date-fns';
import { IAlphaVantageHistoricalResponse } from './interfaces/interfaces';
import { AlphaVantageHistoricalResponse } from './interfaces/interfaces';
@Injectable()
export class AlphaVantageService implements DataProviderInterface {
@ -68,11 +68,11 @@ export class AlphaVantageService implements DataProviderInterface {
symbol,
to
}: GetHistoricalParams): Promise<{
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
[symbol: string]: { [date: string]: DataProviderHistoricalResponse };
}> {
try {
const historicalData: {
[symbol: string]: IAlphaVantageHistoricalResponse[];
[symbol: string]: AlphaVantageHistoricalResponse[];
} = await this.alphaVantage.crypto.daily(
symbol
.substring(0, symbol.length - DEFAULT_CURRENCY.length)
@ -81,7 +81,7 @@ export class AlphaVantageService implements DataProviderInterface {
);
const response: {
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
[symbol: string]: { [date: string]: DataProviderHistoricalResponse };
} = {};
response[symbol] = {};
@ -115,7 +115,7 @@ export class AlphaVantageService implements DataProviderInterface {
}
public async getQuotes({}: GetQuotesParams): Promise<{
[symbol: string]: IDataProviderResponse;
[symbol: string]: DataProviderResponse;
}> {
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
} from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
import {
IDataProviderHistoricalResponse,
IDataProviderResponse
DataProviderHistoricalResponse,
DataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces';
import { DEFAULT_CURRENCY } from '@ghostfolio/common/config';
import { DATE_FORMAT } from '@ghostfolio/common/helper';
@ -109,7 +109,7 @@ export class CoinGeckoService implements DataProviderInterface {
symbol,
to
}: GetHistoricalParams): Promise<{
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
[symbol: string]: { [date: string]: DataProviderHistoricalResponse };
}> {
try {
const { error, prices, status } = await fetch(
@ -133,7 +133,7 @@ export class CoinGeckoService implements DataProviderInterface {
}
const result: {
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
[symbol: string]: { [date: string]: DataProviderHistoricalResponse };
} = {
[symbol]: {}
};
@ -166,8 +166,8 @@ export class CoinGeckoService implements DataProviderInterface {
public async getQuotes({
requestTimeout = this.configurationService.get('REQUEST_TIMEOUT'),
symbols
}: GetQuotesParams): Promise<{ [symbol: string]: IDataProviderResponse }> {
const response: { [symbol: string]: IDataProviderResponse } = {};
}: GetQuotesParams): Promise<{ [symbol: string]: DataProviderResponse }> {
const response: { [symbol: string]: DataProviderResponse } = {};
if (symbols.length <= 0) {
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 { DataProviderInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
import {
IDataProviderHistoricalResponse,
IDataProviderResponse
DataProviderHistoricalResponse,
DataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
@ -215,10 +215,10 @@ export class DataProviderService implements OnModuleInit {
from: Date,
to: Date
): Promise<{
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
[symbol: string]: { [date: string]: DataProviderHistoricalResponse };
}> {
let response: {
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
[symbol: string]: { [date: string]: DataProviderHistoricalResponse };
} = {};
if (isEmpty(aItems) || !isValid(from) || !isValid(to)) {
@ -284,7 +284,7 @@ export class DataProviderService implements OnModuleInit {
from: Date;
to: Date;
}): Promise<{
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
[symbol: string]: { [date: string]: DataProviderHistoricalResponse };
}> {
for (const { currency, rootCurrency } of DERIVED_CURRENCIES) {
if (
@ -317,11 +317,11 @@ export class DataProviderService implements OnModuleInit {
);
const result: {
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
[symbol: string]: { [date: string]: DataProviderHistoricalResponse };
} = {};
const promises: Promise<{
data: { [date: string]: IDataProviderHistoricalResponse };
data: { [date: string]: DataProviderHistoricalResponse };
symbol: string;
}>[] = [];
for (const { dataSource, symbol } of assetProfileIdentifiers) {
@ -329,7 +329,7 @@ export class DataProviderService implements OnModuleInit {
if (dataProvider.canHandle(symbol)) {
if (symbol === `${DEFAULT_CURRENCY}USX`) {
const data: {
[date: string]: IDataProviderHistoricalResponse;
[date: string]: DataProviderHistoricalResponse;
} = {};
for (const date of eachDayOfInterval({ end: to, start: from })) {
@ -399,10 +399,10 @@ export class DataProviderService implements OnModuleInit {
useCache?: boolean;
user?: UserWithSettings;
}): Promise<{
[symbol: string]: IDataProviderResponse;
[symbol: string]: DataProviderResponse;
}> {
const response: {
[symbol: string]: IDataProviderResponse;
[symbol: string]: DataProviderResponse;
} = {};
const startTimeTotal = performance.now();
@ -716,7 +716,7 @@ export class DataProviderService implements OnModuleInit {
}: {
allData: {
data: {
[date: string]: IDataProviderHistoricalResponse;
[date: string]: DataProviderHistoricalResponse;
};
symbol: string;
}[];
@ -728,7 +728,7 @@ export class DataProviderService implements OnModuleInit {
})?.data;
const data: {
[date: string]: IDataProviderHistoricalResponse;
[date: string]: DataProviderHistoricalResponse;
} = {};
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
} from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
import {
IDataProviderHistoricalResponse,
IDataProviderResponse
DataProviderHistoricalResponse,
DataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import {
@ -89,7 +89,7 @@ export class EodHistoricalDataService implements DataProviderInterface {
symbol,
to
}: GetDividendsParams): Promise<{
[date: string]: IDataProviderHistoricalResponse;
[date: string]: DataProviderHistoricalResponse;
}> {
symbol = this.convertToEodSymbol(symbol);
@ -99,7 +99,7 @@ export class EodHistoricalDataService implements DataProviderInterface {
try {
const response: {
[date: string]: IDataProviderHistoricalResponse;
[date: string]: DataProviderHistoricalResponse;
} = {};
const historicalResult = await fetch(
@ -141,7 +141,7 @@ export class EodHistoricalDataService implements DataProviderInterface {
symbol,
to
}: GetHistoricalParams): Promise<{
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
[symbol: string]: { [date: string]: DataProviderHistoricalResponse };
}> {
symbol = this.convertToEodSymbol(symbol);
@ -198,8 +198,8 @@ export class EodHistoricalDataService implements DataProviderInterface {
public async getQuotes({
requestTimeout = this.configurationService.get('REQUEST_TIMEOUT'),
symbols
}: GetQuotesParams): Promise<{ [symbol: string]: IDataProviderResponse }> {
const response: { [symbol: string]: IDataProviderResponse } = {};
}: GetQuotesParams): Promise<{ [symbol: string]: DataProviderResponse }> {
const response: { [symbol: string]: DataProviderResponse } = {};
if (symbols.length <= 0) {
return response;

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

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

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

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

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

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

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

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

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

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

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

@ -10,8 +10,8 @@ import {
GetSearchParams
} from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
import {
IDataProviderHistoricalResponse,
IDataProviderResponse
DataProviderHistoricalResponse,
DataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces';
import { DEFAULT_CURRENCY } from '@ghostfolio/common/config';
import { DATE_FORMAT } from '@ghostfolio/common/helper';
@ -96,7 +96,7 @@ export class YahooFinanceService implements DataProviderInterface {
)
);
const response: {
[date: string]: IDataProviderHistoricalResponse;
[date: string]: DataProviderHistoricalResponse;
} = {};
for (const historicalItem of historicalResult) {
@ -124,7 +124,7 @@ export class YahooFinanceService implements DataProviderInterface {
symbol,
to
}: GetHistoricalParams): Promise<{
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
[symbol: string]: { [date: string]: DataProviderHistoricalResponse };
}> {
if (isSameDay(from, to)) {
to = addDays(to, 1);
@ -145,7 +145,7 @@ export class YahooFinanceService implements DataProviderInterface {
);
const response: {
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
[symbol: string]: { [date: string]: DataProviderHistoricalResponse };
} = {};
response[symbol] = {};
@ -183,8 +183,8 @@ export class YahooFinanceService implements DataProviderInterface {
public async getQuotes({
symbols
}: GetQuotesParams): Promise<{ [symbol: string]: IDataProviderResponse }> {
const response: { [symbol: string]: IDataProviderResponse } = {};
}: GetQuotesParams): Promise<{ [symbol: string]: DataProviderResponse }> {
const response: { [symbol: string]: DataProviderResponse } = {};
if (symbols.length <= 0) {
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 { 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 { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
@ -29,7 +29,7 @@ import ms from 'ms';
@Injectable()
export class ExchangeRateDataService {
private currencies: string[] = [];
private currencyPairs: IDataGatheringItem[] = [];
private currencyPairs: DataGatheringItem[] = [];
private exchangeRates: { [currencyPair: string]: number } = {};
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';
export interface IDataProviderHistoricalResponse {
export interface DataProviderHistoricalResponse {
marketPrice: number;
}
export interface IDataProviderResponse {
export interface DataProviderResponse {
currency: string;
dataProviderInfo?: DataProviderInfo;
dataSource: DataSource;
@ -18,6 +18,6 @@ export interface IDataProviderResponse {
marketState: MarketState;
}
export interface IDataGatheringItem extends AssetProfileIdentifier {
export interface DataGatheringItem extends AssetProfileIdentifier {
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 { 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 { resetHours } from '@ghostfolio/common/helper';
import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
@ -30,7 +30,7 @@ export class MarketDataService {
dataSource,
date = new Date(),
symbol
}: IDataGatheringItem): Promise<MarketData> {
}: DataGatheringItem): Promise<MarketData> {
return await this.prismaService.marketData.findFirst({
where: {
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 { 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 { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import {
@ -99,7 +99,7 @@ export class DataGatheringProcessor {
),
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;
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 { 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 { 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 { PrismaService } from '@ghostfolio/api/services/prisma/prisma.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 });
const dataGatheringItems = (await this.getSymbolsMax())
@ -276,7 +276,7 @@ export class DataGatheringService {
dataGatheringItems,
priority
}: {
dataGatheringItems: IDataGatheringItem[];
dataGatheringItems: DataGatheringItem[];
priority: number;
}) {
await this.addJobsToQueue(
@ -348,7 +348,7 @@ export class DataGatheringService {
});
}
private async getCurrencies7D(): Promise<IDataGatheringItem[]> {
private async getCurrencies7D(): Promise<DataGatheringItem[]> {
const assetProfileIdentifiersWithCompleteMarketData =
await this.getAssetProfileIdentifiersWithCompleteMarketData();
@ -376,7 +376,7 @@ export class DataGatheringService {
withUserSubscription = false
}: {
withUserSubscription?: boolean;
}): Promise<IDataGatheringItem[]> {
}): Promise<DataGatheringItem[]> {
const symbolProfiles =
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 } = {};
(
(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 { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
export interface IPortfolioSnapshotQueueJob {
export interface PortfolioSnapshotQueueJob {
calculationType: PerformanceCalculationType;
filters: Filter[];
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 { addMilliseconds } from 'date-fns';
import { IPortfolioSnapshotQueueJob } from './interfaces/portfolio-snapshot-queue-job.interface';
import { PortfolioSnapshotQueueJob } from './interfaces/portfolio-snapshot-queue-job.interface';
@Injectable()
@Processor(PORTFOLIO_SNAPSHOT_COMPUTATION_QUEUE)
@ -37,9 +37,7 @@ export class PortfolioSnapshotProcessor {
),
name: PORTFOLIO_SNAPSHOT_PROCESS_JOB_NAME
})
public async calculatePortfolioSnapshot(
job: Job<IPortfolioSnapshotQueueJob>
) {
public async calculatePortfolioSnapshot(job: Job<PortfolioSnapshotQueueJob>) {
try {
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 { 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 = {
addJobToQueue({
opts
}: {
data: IPortfolioSnapshotQueueJob;
data: PortfolioSnapshotQueueJob;
name: string;
opts?: JobOptions;
}): 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 { JobOptions, Queue } from 'bull';
import { IPortfolioSnapshotQueueJob } from './interfaces/portfolio-snapshot-queue-job.interface';
import { PortfolioSnapshotQueueJob } from './interfaces/portfolio-snapshot-queue-job.interface';
@Injectable()
export class PortfolioSnapshotService {
@ -18,7 +18,7 @@ export class PortfolioSnapshotService {
name,
opts
}: {
data: IPortfolioSnapshotQueueJob;
data: PortfolioSnapshotQueueJob;
name: string;
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,
User
} from '@ghostfolio/common/interfaces';
import { DateRange } from '@ghostfolio/common/types';
import { GfCurrencySelectorComponent } from '@ghostfolio/ui/currency-selector';
import { GfEntityLogoComponent } from '@ghostfolio/ui/entity-logo';
import { GfHistoricalMarketDataEditorComponent } from '@ghostfolio/ui/historical-market-data-editor';
@ -190,6 +191,32 @@ export class GfAssetProfileDialogComponent implements OnDestroy, OnInit {
};
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 isBenchmark = false;
public isDataGatheringEnabled: boolean;
@ -405,9 +432,15 @@ export class GfAssetProfileDialogComponent implements OnDestroy, OnInit {
.subscribe();
}
public onGatherSymbol({ dataSource, symbol }: AssetProfileIdentifier) {
public onGatherSymbol({
dataSource,
range,
symbol
}: {
range?: DateRange;
} & AssetProfileIdentifier) {
this.adminService
.gatherSymbol({ dataSource, symbol })
.gatherSymbol({ dataSource, range, symbol })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe();
}

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

@ -26,12 +26,30 @@
[disabled]="
assetProfileForm.dirty || !assetProfileForm.controls.isActive.value
"
[matMenuTriggerFor]="gatherHistoricalMarketDataMenu"
(click)="
onGatherSymbol({ dataSource: data.dataSource, symbol: data.symbol })
"
>
<ng-container i18n>Gather Historical Market Data</ng-container>
</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
mat-menu-item
type="button"

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

@ -19,6 +19,7 @@ import {
ViewChild
} from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatDialog } from '@angular/material/dialog';
import { MatMenuModule } from '@angular/material/menu';
import {
MatPaginator,
@ -26,6 +27,7 @@ import {
PageEvent
} from '@angular/material/paginator';
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
import { ActivatedRoute, Router } from '@angular/router';
import { IonIcon } from '@ionic/angular/standalone';
import {
differenceInSeconds,
@ -37,8 +39,10 @@ import {
contractOutline,
ellipsisHorizontal,
keyOutline,
personOutline,
trashOutline
} from 'ionicons/icons';
import { DeviceDetectorService } from 'ngx-device-detector';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@ -49,6 +53,8 @@ import { AdminService } from '../../services/admin.service';
import { DataService } from '../../services/data.service';
import { ImpersonationStorageService } from '../../services/impersonation-storage.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({
imports: [
@ -71,6 +77,7 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit {
public dataSource = new MatTableDataSource<AdminUsers['users'][0]>();
public defaultDateFormat: string;
public deviceType: string;
public displayedColumns: string[] = [];
public getEmojiFlag = getEmojiFlag;
public hasPermissionForSubscription: boolean;
@ -87,11 +94,16 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit {
private adminService: AdminService,
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
private deviceService: DeviceDetectorService,
private dialog: MatDialog,
private impersonationStorageService: ImpersonationStorageService,
private notificationService: NotificationService,
private route: ActivatedRoute,
private router: Router,
private tokenStorageService: TokenStorageService,
private userService: UserService
) {
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
this.info = this.dataService.fetchInfo();
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
.pipe(takeUntil(this.unsubscribeSubject))
.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() {
@ -161,6 +187,12 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit {
return '';
}
public onChangePage(page: PageEvent) {
this.fetchUsers({
pageIndex: page.pageIndex
});
}
public onDeleteUser(aId: string) {
this.notificationService.confirm({
confirmFn: () => {
@ -212,9 +244,9 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit {
window.location.reload();
}
public onChangePage(page: PageEvent) {
this.fetchUsers({
pageIndex: page.pageIndex
public onOpenUserDetailDialog(userId: string) {
this.router.navigate([], {
queryParams: { userId, userDetailDialog: true }
});
}
@ -245,4 +277,34 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit {
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" />
</button>
<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) {
<button mat-menu-item (click)="onImpersonateUser(element.id)">
<span class="align-items-center d-flex">
@ -255,8 +264,9 @@
></tr>
<tr
*matRowDef="let row; columns: displayedColumns"
class="mat-mdc-row"
class="cursor-pointer mat-mdc-row"
mat-row
(click)="onOpenUserDetailDialog(row.id)"
></tr>
</table>
</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="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-label i18n>Security Token</mat-label>
<input
matInput
[formControl]="accessTokenFormControl"
[type]="isAccessTokenHidden ? 'password' : 'text'"
(keydown.enter)="onLoginWithAccessToken(); $event.preventDefault()"
/>
<button
mat-button
@ -21,35 +22,37 @@
/>
</button>
</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>
@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 mat-dialog-actions>
<div class="flex-grow-1">
<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
} from '@ghostfolio/common/interfaces';
export interface IRuleSettingsDialogParams {
export interface RuleSettingsDialogParams {
categoryName: string;
locale: string;
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';
import { MatSliderModule } from '@angular/material/slider';
import { IRuleSettingsDialogParams } from './interfaces/interfaces';
import { RuleSettingsDialogParams } from './interfaces/interfaces';
@Component({
imports: [
@ -31,7 +31,7 @@ export class GfRuleSettingsDialogComponent {
public settings: XRayRulesSettings['AccountClusterRiskCurrentInvestment'];
public constructor(
@Inject(MAT_DIALOG_DATA) public data: IRuleSettingsDialogParams,
@Inject(MAT_DIALOG_DATA) public data: RuleSettingsDialogParams,
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 { 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';
@Component({
@ -83,7 +83,7 @@ export class GfRuleComponent implements OnInit {
rule,
categoryName: this.categoryName,
settings: this.settings
} as IRuleSettingsDialogParams,
} as RuleSettingsDialogParams,
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() {
this.dataService
.createCheckoutSession({ couponId: this.couponId, priceId: this.priceId })
.createStripeCheckoutSession({
couponId: this.couponId,
priceId: this.priceId
})
.pipe(
catchError((error) => {
this.notificationService.alert({
@ -117,7 +120,7 @@ export class GfUserAccountMembershipComponent implements OnDestroy {
throw error;
}),
switchMap(({ sessionId }: { sessionId: string }) => {
switchMap(({ 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 { MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { IAlertDialogParams } from './interfaces/interfaces';
import { AlertDialogParams } from './interfaces/interfaces';
@Component({
imports: [MatButtonModule, MatDialogModule],
@ -17,7 +17,7 @@ export class GfAlertDialogComponent {
public constructor(public dialogRef: MatDialogRef<GfAlertDialogComponent>) {}
public initialize(aParams: IAlertDialogParams) {
public initialize(aParams: AlertDialogParams) {
this.discardLabel = aParams.discardLabel;
this.message = aParams.message;
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;
discardLabel?: 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 { ConfirmationDialogType } from './confirmation-dialog.type';
import { IConfirmDialogParams } from './interfaces/interfaces';
import { ConfirmDialogParams } from './interfaces/interfaces';
@Component({
imports: [MatButtonModule, MatDialogModule],
@ -29,7 +29,7 @@ export class GfConfirmationDialogComponent {
}
}
public initialize(aParams: IConfirmDialogParams) {
public initialize(aParams: ConfirmDialogParams) {
this.confirmLabel = aParams.confirmLabel;
this.confirmType = aParams.confirmType;
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';
export interface IConfirmDialogParams {
export interface ConfirmDialogParams {
confirmLabel?: string;
confirmType: ConfirmationDialogType;
discardLabel?: string;

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

@ -1,13 +1,13 @@
import { ConfirmationDialogType } from '../confirmation-dialog/confirmation-dialog.type';
export interface IAlertParams {
export interface AlertParams {
discardFn?: () => void;
discardLabel?: string;
message?: string;
title: string;
}
export interface IConfirmParams {
export interface ConfirmParams {
confirmFn: () => void;
confirmLabel?: string;
confirmType?: ConfirmationDialogType;
@ -18,7 +18,7 @@ export interface IConfirmParams {
title: string;
}
export interface IPromptParams {
export interface PromptParams {
confirmFn: (value: string) => void;
confirmLabel?: 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 { ConfirmationDialogType } from './confirmation-dialog/confirmation-dialog.type';
import {
IAlertParams,
IConfirmParams,
IPromptParams
AlertParams,
ConfirmParams,
PromptParams
} from './interfaces/interfaces';
import { GfPromptDialogComponent } from './prompt-dialog/prompt-dialog.component';
@ -21,7 +21,7 @@ export class NotificationService {
public constructor(private matDialog: MatDialog) {}
public alert(aParams: IAlertParams) {
public alert(aParams: AlertParams) {
if (!aParams.discardLabel) {
aParams.discardLabel = translate('CLOSE');
}
@ -45,7 +45,7 @@ export class NotificationService {
});
}
public confirm(aParams: IConfirmParams) {
public confirm(aParams: ConfirmParams) {
if (!aParams.confirmLabel) {
aParams.confirmLabel = translate('YES');
}
@ -78,7 +78,7 @@ export class NotificationService {
});
}
public prompt(aParams: IPromptParams) {
public prompt(aParams: PromptParams) {
if (!aParams.confirmLabel) {
aParams.confirmLabel = translate('OK');
}

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

Loading…
Cancel
Save