diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5bdd653a9..33f1064ba 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -94,6 +94,129 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## 2.115.0 - 2024-10-14
+### Changed
+
+- Extended the assistant by a holding selector
+- Separated the _FIRE_ / _X-ray_ page
+- Improved the language localization for Italian (`it`)
+- Upgraded `ngx-skeleton-loader` from version `7.0.0` to `9.0.0`
+
+## 2.122.0 - 2024-11-07
+
+### Changed
+
+- Upgraded `countries-list` from version `3.1.0` to `3.1.1`
+
+### Fixed
+
+- Fixed an issue with the algebraic sign in the chart of the holdings tab on the home page (experimental)
+- Improved the exception handling in the user authorization service
+- Disabled the caching of the benchmarks in the markets overview if sharing the _Fear & Greed Index_ (market mood) is enabled
+
+## 2.121.1 - 2024-11-02
+
+### Added
+
+- Set the stack and container names in the `docker-compose` files (`docker-compose.yml`, `docker-compose.build.yml` and `docker-compose.dev.yml`)
+
+### Changed
+
+- Reverted the permissions (`chmod 0700`) on `entrypoint.sh` in the `Dockerfile`
+- Upgraded the _Stripe_ dependencies
+
+## 2.120.0 - 2024-10-30
+
+### Added
+
+- Added support for log levels (`LOG_LEVELS`) to conditionally log `prisma` query events (`debug` or `verbose`)
+
+### Changed
+
+- Restructured the resources page
+- Renamed the static portfolio analysis rule from _Allocation Cluster Risk_ to _Economic Market Cluster Risk_ (Developed Markets and Emerging Markets)
+- Improved the language localization for German (`de`)
+- Switched the `consistent-generic-constructors` rule from `warn` to `error` in the `eslint` configuration
+- Switched the `consistent-indexed-object-style` rule from `warn` to `off` in the `eslint` configuration
+- Switched the `consistent-type-assertions` rule from `warn` to `error` in the `eslint` configuration
+- Switched the `prefer-optional-chain` rule from `warn` to `error` in the `eslint` configuration
+- Upgraded `Nx` from version `20.0.3` to `20.0.6`
+
+## 2.119.0 - 2024-10-26
+
+### Changed
+
+- Switched the `consistent-type-definitions` rule from `warn` to `error` in the `eslint` configuration
+- Switched the `no-empty-function` rule from `warn` to `error` in the `eslint` configuration
+- Switched the `prefer-function-type` rule from `warn` to `error` in the `eslint` configuration
+- Upgraded `prisma` from version `5.20.0` to `5.21.1`
+
+### Fixed
+
+- Fixed an issue with the X-axis scale of the dividend timeline on the analysis page
+- Fixed an issue with the X-axis scale of the investment timeline on the analysis page
+- Fixed an issue with the X-axis scale of the portfolio evolution chart on the analysis page
+- Fixed an issue in the calculation of the static portfolio analysis rule: _Allocation Cluster Risk_ (Developed Markets)
+- Fixed an issue in the calculation of the static portfolio analysis rule: _Allocation Cluster Risk_ (Emerging Markets)
+
+## 2.118.0 - 2024-10-23
+
+### Added
+
+- Added a new static portfolio analysis rule: _Allocation Cluster Risk_ (Developed Markets)
+- Added a new static portfolio analysis rule: _Allocation Cluster Risk_ (Emerging Markets)
+- Added support for mutual funds in the _EOD Historical Data_ service
+
+### Changed
+
+- Improved the font colors of the chart of the holdings tab on the home page (experimental)
+- Optimized the dialog sizes for mobile (full screen)
+- Optimized the git-hook via `husky` to lint only affected projects before a commit
+- Upgraded `angular` from version `18.1.1` to `18.2.8`
+- Upgraded `Nx` from version `19.5.6` to `20.0.3`
+
+### Fixed
+
+- Fixed the warning `export was not found` in connection with `GetValuesParams`
+- Quoted the password for the _Redis_ service `healthcheck` in the `docker-compose` files (`docker-compose.yml` and `docker-compose.build.yml`)
+
+## 2.117.0 - 2024-10-19
+
+### Added
+
+- Added the logotype to the footer
+- Added the data providers management to the admin control panel
+
+### Changed
+
+- Improved the backgrounds of the chart of the holdings tab on the home page (experimental)
+- Improved the language localization for German (`de`)
+
+### Fixed
+
+- Fixed an issue in the carousel component for the testimonial section on the landing page
+
+## 2.116.0 - 2024-10-17
+
+### Added
+
+- Extended the content of the _Self-Hosting_ section by the benchmarks concept for _Compare with..._ on the Frequently Asked Questions (FAQ) page
+- Extended the content of the _Self-Hosting_ section by the benchmarks concept for _Markets_ on the Frequently Asked Questions (FAQ) page
+- Set the permissions (`chmod 0700`) on `entrypoint.sh` in the `Dockerfile`
+
+### Changed
+
+- Improved the empty state in the benchmarks of the markets overview
+- Disabled the text hover effect in the chart of the holdings tab on the home page (experimental)
+- Improved the usability to customize the rule thresholds in the _X-ray_ section by introducing units (experimental)
+- Switched to adjusted market prices (splits and dividends) in the get historical functionality of the _EOD Historical Data_ service
+- Improved the language localization for German (`de`)
+
+### Fixed
+
+- Fixed the usage of the environment variable `PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_CONCURRENCY`
+
+## 2.115.0 - 2024-10-14
+
### Added
- Added the name to the tooltip of the chart of the holdings tab on the home page (experimental)
diff --git a/apps/api/src/app/admin/admin.service.ts b/apps/api/src/app/admin/admin.service.ts
index 18441dda7..112d774e7 100644
--- a/apps/api/src/app/admin/admin.service.ts
+++ b/apps/api/src/app/admin/admin.service.ts
@@ -648,7 +648,7 @@ export class AdminService {
}
private async getUsersWithAnalytics(): Promise {
- let orderBy: any = {
+ let orderBy: Prisma.UserOrderByWithRelationInput = {
createdAt: 'desc'
};
let where: Prisma.UserWhereInput;
@@ -656,7 +656,7 @@ export class AdminService {
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
orderBy = {
Analytics: {
- updatedAt: 'desc'
+ lastRequestAt: 'desc'
}
};
where = {
diff --git a/apps/api/src/app/auth/jwt.strategy.ts b/apps/api/src/app/auth/jwt.strategy.ts
index a8ad8fd08..7a3fb224b 100644
--- a/apps/api/src/app/auth/jwt.strategy.ts
+++ b/apps/api/src/app/auth/jwt.strategy.ts
@@ -46,7 +46,7 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
update: {
country,
activityCount: { increment: 1 },
- updatedAt: new Date()
+ lastRequestAt: new Date()
},
where: { userId: user.id }
});
@@ -60,7 +60,7 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
);
}
} catch (error) {
- if (error?.getStatus() === StatusCodes.TOO_MANY_REQUESTS) {
+ if (error?.getStatus?.() === StatusCodes.TOO_MANY_REQUESTS) {
throw error;
} else {
throw new HttpException(
diff --git a/apps/api/src/app/benchmark/benchmark.service.ts b/apps/api/src/app/benchmark/benchmark.service.ts
index 36f196842..a659281d7 100644
--- a/apps/api/src/app/benchmark/benchmark.service.ts
+++ b/apps/api/src/app/benchmark/benchmark.service.ts
@@ -437,7 +437,7 @@ export class BenchmarkService {
};
});
- if (storeInCache) {
+ if (!enableSharing && storeInCache) {
const expiration = addHours(new Date(), 2);
await this.redisCacheService.set(
diff --git a/apps/api/src/app/info/info.service.ts b/apps/api/src/app/info/info.service.ts
index bd291c511..62a78d1d8 100644
--- a/apps/api/src/app/info/info.service.ts
+++ b/apps/api/src/app/info/info.service.ts
@@ -23,10 +23,10 @@ import {
import {
InfoItem,
Statistics,
- Subscription
+ SubscriptionOffer
} from '@ghostfolio/common/interfaces';
import { permissions } from '@ghostfolio/common/permissions';
-import { SubscriptionOffer } from '@ghostfolio/common/types';
+import { SubscriptionOfferKey } from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
@@ -101,7 +101,7 @@ export class InfoService {
isUserSignupEnabled,
platforms,
statistics,
- subscriptions
+ subscriptionOffers
] = await Promise.all([
this.benchmarkService.getBenchmarkAssetProfiles(),
this.getDemoAuthToken(),
@@ -110,7 +110,7 @@ export class InfoService {
orderBy: { name: 'asc' }
}),
this.getStatistics(),
- this.getSubscriptions()
+ this.getSubscriptionOffers()
]);
if (isUserSignupEnabled) {
@@ -125,7 +125,7 @@ export class InfoService {
isReadOnlyMode,
platforms,
statistics,
- subscriptions,
+ subscriptionOffers,
baseCurrency: DEFAULT_CURRENCY,
currencies: this.exchangeRateDataService.getCurrencies()
};
@@ -142,7 +142,7 @@ export class InfoService {
},
{
Analytics: {
- updatedAt: {
+ lastRequestAt: {
gt: subDays(new Date(), aDays)
}
}
@@ -314,8 +314,8 @@ export class InfoService {
return statistics;
}
- private async getSubscriptions(): Promise<{
- [offer in SubscriptionOffer]: Subscription;
+ private async getSubscriptionOffers(): Promise<{
+ [offer in SubscriptionOfferKey]: SubscriptionOffer;
}> {
if (!this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
return undefined;
diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
index 30f6ec264..bb83b98a8 100644
--- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
+++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
@@ -162,10 +162,6 @@ export abstract class PortfolioCalculator {
this.snapshotPromise = this.initialize();
}
- protected abstract calculateOverallPerformance(
- positions: TimelinePosition[]
- ): PortfolioSnapshot;
-
@LogPerformance
public async computeSnapshot(): Promise {
const lastTransactionPoint = last(this.transactionPoints);
@@ -769,62 +765,6 @@ export abstract class PortfolioCalculator {
return { chart };
}
- @LogPerformance
- public async getSnapshot() {
- await this.snapshotPromise;
-
- return this.snapshot;
- }
-
- public getStartDate() {
- let firstAccountBalanceDate: Date;
- let firstActivityDate: Date;
-
- try {
- const firstAccountBalanceDateString = first(
- this.accountBalanceItems
- )?.date;
- firstAccountBalanceDate = firstAccountBalanceDateString
- ? parseDate(firstAccountBalanceDateString)
- : new Date();
- } catch (error) {
- firstAccountBalanceDate = new Date();
- }
-
- try {
- const firstActivityDateString = this.transactionPoints[0].date;
- firstActivityDate = firstActivityDateString
- ? parseDate(firstActivityDateString)
- : new Date();
- } catch (error) {
- firstActivityDate = new Date();
- }
-
- return min([firstAccountBalanceDate, firstActivityDate]);
- }
-
- protected abstract getSymbolMetrics({
- chartDateMap,
- dataSource,
- end,
- exchangeRates,
- marketSymbolMap,
- start,
- symbol
- }: {
- chartDateMap: { [date: string]: boolean };
- end: Date;
- exchangeRates: { [dateString: string]: number };
- marketSymbolMap: {
- [date: string]: { [symbol: string]: Big };
- };
- start: Date;
- } & AssetProfileIdentifier): SymbolMetrics;
-
- public getTransactionPoints() {
- return this.transactionPoints;
- }
-
@LogPerformance
public async getValuablesInBaseCurrency() {
await this.snapshotPromise;
@@ -832,72 +772,11 @@ export abstract class PortfolioCalculator {
return this.snapshot.totalValuablesWithCurrencyEffect;
}
- private getChartDateMap({
- endDate,
- startDate,
- step
- }: {
- endDate: Date;
- startDate: Date;
- step: number;
- }): { [date: string]: true } {
- // Create a map of all relevant chart dates:
- // 1. Add transaction point dates
- const chartDateMap = this.transactionPoints.reduce((result, { date }) => {
- result[date] = true;
- return result;
- }, {});
-
- // 2. Add dates between transactions respecting the specified step size
- for (const date of eachDayOfInterval(
- { end: endDate, start: startDate },
- { step }
- )) {
- chartDateMap[format(date, DATE_FORMAT)] = true;
- }
-
- if (step > 1) {
- // Reduce the step size of last 90 days
- for (const date of eachDayOfInterval(
- { end: endDate, start: subDays(endDate, 90) },
- { step: 3 }
- )) {
- chartDateMap[format(date, DATE_FORMAT)] = true;
- }
-
- // Reduce the step size of last 30 days
- for (const date of eachDayOfInterval(
- { end: endDate, start: subDays(endDate, 30) },
- { step: 1 }
- )) {
- chartDateMap[format(date, DATE_FORMAT)] = true;
- }
- }
-
- // Make sure the end date is present
- chartDateMap[format(endDate, DATE_FORMAT)] = true;
-
- // Make sure some key dates are present
- for (const dateRange of ['1d', '1y', '5y', 'max', 'mtd', 'wtd', 'ytd']) {
- const { endDate: dateRangeEnd, startDate: dateRangeStart } =
- getIntervalFromDateRange(dateRange);
-
- if (
- !isBefore(dateRangeStart, startDate) &&
- !isAfter(dateRangeStart, endDate)
- ) {
- chartDateMap[format(dateRangeStart, DATE_FORMAT)] = true;
- }
-
- if (
- !isBefore(dateRangeEnd, startDate) &&
- !isAfter(dateRangeEnd, endDate)
- ) {
- chartDateMap[format(dateRangeEnd, DATE_FORMAT)] = true;
- }
- }
+ @LogPerformance
+ public async getSnapshot() {
+ await this.snapshotPromise;
- return chartDateMap;
+ return this.snapshot;
}
@LogPerformance
@@ -1120,4 +999,125 @@ export abstract class PortfolioCalculator {
await this.initialize();
}
}
+
+ public getStartDate() {
+ let firstAccountBalanceDate: Date;
+ let firstActivityDate: Date;
+
+ try {
+ const firstAccountBalanceDateString = first(
+ this.accountBalanceItems
+ )?.date;
+ firstAccountBalanceDate = firstAccountBalanceDateString
+ ? parseDate(firstAccountBalanceDateString)
+ : new Date();
+ } catch (error) {
+ firstAccountBalanceDate = new Date();
+ }
+
+ try {
+ const firstActivityDateString = this.transactionPoints[0].date;
+ firstActivityDate = firstActivityDateString
+ ? parseDate(firstActivityDateString)
+ : new Date();
+ } catch (error) {
+ firstActivityDate = new Date();
+ }
+
+ return min([firstAccountBalanceDate, firstActivityDate]);
+ }
+
+ public getTransactionPoints() {
+ return this.transactionPoints;
+ }
+
+ private getChartDateMap({
+ endDate,
+ startDate,
+ step
+ }: {
+ endDate: Date;
+ startDate: Date;
+ step: number;
+ }): { [date: string]: true } {
+ // Create a map of all relevant chart dates:
+ // 1. Add transaction point dates
+ const chartDateMap = this.transactionPoints.reduce((result, { date }) => {
+ result[date] = true;
+ return result;
+ }, {});
+
+ // 2. Add dates between transactions respecting the specified step size
+ for (const date of eachDayOfInterval(
+ { end: endDate, start: startDate },
+ { step }
+ )) {
+ chartDateMap[format(date, DATE_FORMAT)] = true;
+ }
+
+ if (step > 1) {
+ // Reduce the step size of last 90 days
+ for (const date of eachDayOfInterval(
+ { end: endDate, start: subDays(endDate, 90) },
+ { step: 3 }
+ )) {
+ chartDateMap[format(date, DATE_FORMAT)] = true;
+ }
+
+ // Reduce the step size of last 30 days
+ for (const date of eachDayOfInterval(
+ { end: endDate, start: subDays(endDate, 30) },
+ { step: 1 }
+ )) {
+ chartDateMap[format(date, DATE_FORMAT)] = true;
+ }
+ }
+
+ // Make sure the end date is present
+ chartDateMap[format(endDate, DATE_FORMAT)] = true;
+
+ // Make sure some key dates are present
+ for (const dateRange of ['1d', '1y', '5y', 'max', 'mtd', 'wtd', 'ytd']) {
+ const { endDate: dateRangeEnd, startDate: dateRangeStart } =
+ getIntervalFromDateRange(dateRange);
+
+ if (
+ !isBefore(dateRangeStart, startDate) &&
+ !isAfter(dateRangeStart, endDate)
+ ) {
+ chartDateMap[format(dateRangeStart, DATE_FORMAT)] = true;
+ }
+
+ if (
+ !isBefore(dateRangeEnd, startDate) &&
+ !isAfter(dateRangeEnd, endDate)
+ ) {
+ chartDateMap[format(dateRangeEnd, DATE_FORMAT)] = true;
+ }
+ }
+
+ return chartDateMap;
+ }
+
+ protected abstract getSymbolMetrics({
+ chartDateMap,
+ dataSource,
+ end,
+ exchangeRates,
+ marketSymbolMap,
+ start,
+ symbol
+ }: {
+ chartDateMap: { [date: string]: boolean };
+ end: Date;
+ exchangeRates: { [dateString: string]: number };
+ marketSymbolMap: {
+ [date: string]: { [symbol: string]: Big };
+ };
+ start: Date;
+ } & AssetProfileIdentifier): SymbolMetrics;
+
+ protected abstract calculateOverallPerformance(
+ positions: TimelinePosition[]
+ ): PortfolioSnapshot;
}
diff --git a/apps/api/src/app/portfolio/current-rate.service.ts b/apps/api/src/app/portfolio/current-rate.service.ts
index cd1994826..ab7bf2ebf 100644
--- a/apps/api/src/app/portfolio/current-rate.service.ts
+++ b/apps/api/src/app/portfolio/current-rate.service.ts
@@ -52,27 +52,24 @@ export class CurrentRateService {
.then((dataResultProvider) => {
const result: GetValueObject[] = [];
- for (const dataGatheringItem of dataGatheringItems) {
- if (
- dataResultProvider?.[dataGatheringItem.symbol]?.dataProviderInfo
- ) {
+ for (const { dataSource, symbol } of dataGatheringItems) {
+ if (dataResultProvider?.[symbol]?.dataProviderInfo) {
dataProviderInfos.push(
- dataResultProvider[dataGatheringItem.symbol].dataProviderInfo
+ dataResultProvider[symbol].dataProviderInfo
);
}
- if (dataResultProvider?.[dataGatheringItem.symbol]?.marketPrice) {
+ if (dataResultProvider?.[symbol]?.marketPrice) {
result.push({
- dataSource: dataGatheringItem.dataSource,
+ dataSource,
+ symbol,
date: today,
- marketPrice:
- dataResultProvider?.[dataGatheringItem.symbol]?.marketPrice,
- symbol: dataGatheringItem.symbol
+ marketPrice: dataResultProvider?.[symbol]?.marketPrice
});
} else {
quoteErrors.push({
- dataSource: dataGatheringItem.dataSource,
- symbol: dataGatheringItem.symbol
+ dataSource,
+ symbol
});
}
}
diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts
index fe42c9838..e33027fb6 100644
--- a/apps/api/src/app/portfolio/portfolio.controller.ts
+++ b/apps/api/src/app/portfolio/portfolio.controller.ts
@@ -77,12 +77,15 @@ export class PortfolioController {
@Get('details')
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@UseInterceptors(RedactValuesInResponseInterceptor)
+ @UseInterceptors(TransformDataSourceInRequestInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getDetails(
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string,
@Query('accounts') filterByAccounts?: string,
@Query('assetClasses') filterByAssetClasses?: string,
+ @Query('dataSource') filterByDataSource?: string,
@Query('range') dateRange: DateRange = 'max',
+ @Query('symbol') filterBySymbol?: string,
@Query('tags') filterByTags?: string,
@Query('withMarkets') withMarketsParam = 'false'
): Promise {
@@ -98,6 +101,8 @@ export class PortfolioController {
const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts,
filterByAssetClasses,
+ filterByDataSource,
+ filterBySymbol,
filterByTags
});
@@ -310,17 +315,22 @@ export class PortfolioController {
@Get('dividends')
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
+ @UseInterceptors(TransformDataSourceInRequestInterceptor)
public async getDividends(
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string,
@Query('accounts') filterByAccounts?: string,
@Query('assetClasses') filterByAssetClasses?: string,
+ @Query('dataSource') filterByDataSource?: string,
@Query('groupBy') groupBy?: GroupBy,
@Query('range') dateRange: DateRange = 'max',
+ @Query('symbol') filterBySymbol?: string,
@Query('tags') filterByTags?: string
): Promise {
const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts,
filterByAssetClasses,
+ filterByDataSource,
+ filterBySymbol,
filterByTags
});
@@ -377,21 +387,26 @@ export class PortfolioController {
@Get('holdings')
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@UseInterceptors(RedactValuesInResponseInterceptor)
+ @UseInterceptors(TransformDataSourceInRequestInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getHoldings(
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string,
@Query('accounts') filterByAccounts?: string,
@Query('assetClasses') filterByAssetClasses?: string,
+ @Query('dataSource') filterByDataSource?: string,
@Query('holdingType') filterByHoldingType?: string,
@Query('query') filterBySearchQuery?: string,
@Query('range') dateRange: DateRange = 'max',
+ @Query('symbol') filterBySymbol?: string,
@Query('tags') filterByTags?: string
): Promise {
const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts,
filterByAssetClasses,
+ filterByDataSource,
filterByHoldingType,
filterBySearchQuery,
+ filterBySymbol,
filterByTags
});
@@ -407,17 +422,22 @@ export class PortfolioController {
@Get('investments')
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
+ @UseInterceptors(TransformDataSourceInRequestInterceptor)
public async getInvestments(
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string,
@Query('accounts') filterByAccounts?: string,
@Query('assetClasses') filterByAssetClasses?: string,
+ @Query('dataSource') filterByDataSource?: string,
@Query('groupBy') groupBy?: GroupBy,
@Query('range') dateRange: DateRange = 'max',
+ @Query('symbol') filterBySymbol?: string,
@Query('tags') filterByTags?: string
): Promise {
const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts,
filterByAssetClasses,
+ filterByDataSource,
+ filterBySymbol,
filterByTags
});
@@ -472,6 +492,7 @@ export class PortfolioController {
@Get('performance')
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@UseInterceptors(PerformanceLoggingInterceptor)
+ @UseInterceptors(TransformDataSourceInRequestInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor)
@Version('2')
@LogPerformance
@@ -479,7 +500,9 @@ export class PortfolioController {
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string,
@Query('accounts') filterByAccounts?: string,
@Query('assetClasses') filterByAssetClasses?: string,
+ @Query('dataSource') filterByDataSource?: string,
@Query('range') dateRange: DateRange = 'max',
+ @Query('symbol') filterBySymbol?: string,
@Query('tags') filterByTags?: string,
@Query('withExcludedAccounts') withExcludedAccounts = false,
@Query('timeWeightedPerformance') calculateTimeWeightedPerformance = false
@@ -487,6 +510,8 @@ export class PortfolioController {
const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts,
filterByAssetClasses,
+ filterByDataSource,
+ filterBySymbol,
filterByTags
});
diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts
index 5827d63b6..bae845fab 100644
--- a/apps/api/src/app/portfolio/portfolio.service.ts
+++ b/apps/api/src/app/portfolio/portfolio.service.ts
@@ -420,15 +420,16 @@ export class PortfolioService {
);
}
- const dataGatheringItems = positions.map(({ dataSource, symbol }) => {
+ const assetProfileIdentifiers = positions.map(({ dataSource, symbol }) => {
return {
dataSource,
symbol
};
});
- const symbolProfiles =
- await this.symbolProfileService.getSymbolProfiles(dataGatheringItems);
+ const symbolProfiles = await this.symbolProfileService.getSymbolProfiles(
+ assetProfileIdentifiers
+ );
const symbolProfileMap: { [symbol: string]: EnhancedSymbolProfile } = {};
for (const symbolProfile of symbolProfiles) {
@@ -868,7 +869,7 @@ export class PortfolioService {
if (isEmpty(historicalData)) {
try {
historicalData = await this.dataProviderService.getHistoricalRaw({
- dataGatheringItems: [
+ assetProfileIdentifiers: [
{ dataSource: DataSource.YAHOO, symbol: aSymbol }
],
from: portfolioStart,
@@ -975,7 +976,7 @@ export class PortfolioService {
return !quantity.eq(0);
});
- const dataGatheringItems = positions.map(({ dataSource, symbol }) => {
+ const assetProfileIdentifiers = positions.map(({ dataSource, symbol }) => {
return {
dataSource,
symbol
@@ -983,7 +984,10 @@ export class PortfolioService {
});
const [dataProviderResponses, symbolProfiles] = await Promise.all([
- this.dataProviderService.getQuotes({ user, items: dataGatheringItems }),
+ this.dataProviderService.getQuotes({
+ user,
+ items: assetProfileIdentifiers
+ }),
this.symbolProfileService.getSymbolProfiles(
positions.map(({ dataSource, symbol }) => {
return { dataSource, symbol };
diff --git a/apps/api/src/app/subscription/subscription.service.ts b/apps/api/src/app/subscription/subscription.service.ts
index 7c1df023c..ae0260d8c 100644
--- a/apps/api/src/app/subscription/subscription.service.ts
+++ b/apps/api/src/app/subscription/subscription.service.ts
@@ -1,8 +1,16 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
-import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config';
+import { PropertyService } from '@ghostfolio/api/services/property/property.service';
+import {
+ DEFAULT_LANGUAGE_CODE,
+ PROPERTY_STRIPE_CONFIG
+} from '@ghostfolio/common/config';
import { parseDate } from '@ghostfolio/common/helper';
-import { SubscriptionOffer, UserWithSettings } from '@ghostfolio/common/types';
+import { SubscriptionOffer } from '@ghostfolio/common/interfaces';
+import {
+ SubscriptionOfferKey,
+ UserWithSettings
+} from '@ghostfolio/common/types';
import { SubscriptionType } from '@ghostfolio/common/types/subscription-type.type';
import { Injectable, Logger } from '@nestjs/common';
@@ -17,14 +25,17 @@ export class SubscriptionService {
public constructor(
private readonly configurationService: ConfigurationService,
- private readonly prismaService: PrismaService
+ private readonly prismaService: PrismaService,
+ private readonly propertyService: PropertyService
) {
- this.stripe = new Stripe(
- this.configurationService.get('STRIPE_SECRET_KEY'),
- {
- apiVersion: '2024-04-10'
- }
- );
+ if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
+ this.stripe = new Stripe(
+ this.configurationService.get('STRIPE_SECRET_KEY'),
+ {
+ apiVersion: '2024-09-30.acacia'
+ }
+ );
+ }
}
public async createCheckoutSession({
@@ -36,6 +47,18 @@ export class SubscriptionService {
priceId: string;
user: UserWithSettings;
}) {
+ const subscriptionOffers: {
+ [offer in SubscriptionOfferKey]: SubscriptionOffer;
+ } =
+ ((await this.propertyService.getByKey(PROPERTY_STRIPE_CONFIG)) as any) ??
+ {};
+
+ const subscriptionOffer = Object.values(subscriptionOffers).find(
+ (subscriptionOffer) => {
+ return subscriptionOffer.priceId === priceId;
+ }
+ );
+
const checkoutSessionCreateParams: Stripe.Checkout.SessionCreateParams = {
cancel_url: `${this.configurationService.get('ROOT_URL')}/${
user.Settings?.settings?.language ?? DEFAULT_LANGUAGE_CODE
@@ -47,6 +70,13 @@ export class SubscriptionService {
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(
@@ -73,17 +103,25 @@ export class SubscriptionService {
public async createSubscription({
duration = '1 year',
+ durationExtension,
price,
userId
}: {
duration?: StringValue;
+ durationExtension?: StringValue;
price: number;
userId: string;
}) {
+ let expiresAt = addMilliseconds(new Date(), ms(duration));
+
+ if (durationExtension) {
+ expiresAt = addMilliseconds(expiresAt, ms(durationExtension));
+ }
+
await this.prismaService.subscription.create({
data: {
+ expiresAt,
price,
- expiresAt: addMilliseconds(new Date(), ms(duration)),
User: {
connect: {
id: userId
@@ -95,10 +133,21 @@ export class SubscriptionService {
public async createSubscriptionViaStripe(aCheckoutSessionId: string) {
try {
+ let durationExtension: StringValue;
+
const session =
await this.stripe.checkout.sessions.retrieve(aCheckoutSessionId);
+ const subscriptionOffer: SubscriptionOffer = JSON.parse(
+ session.metadata.subscriptionOffer ?? '{}'
+ );
+
+ if (subscriptionOffer) {
+ durationExtension = subscriptionOffer.durationExtension;
+ }
+
await this.createSubscription({
+ durationExtension,
price: session.amount_total / 100,
userId: session.client_reference_id
});
@@ -121,7 +170,7 @@ export class SubscriptionService {
return new Date(a.expiresAt) > new Date(b.expiresAt) ? a : b;
});
- let offer: SubscriptionOffer = price ? 'renewal' : 'default';
+ let offer: SubscriptionOfferKey = price ? 'renewal' : 'default';
if (isBefore(createdAt, parseDate('2023-01-01'))) {
offer = 'renewal-early-bird-2023';
diff --git a/apps/api/src/app/symbol/symbol.controller.ts b/apps/api/src/app/symbol/symbol.controller.ts
index b3b9dc109..89cdd416c 100644
--- a/apps/api/src/app/symbol/symbol.controller.ts
+++ b/apps/api/src/app/symbol/symbol.controller.ts
@@ -2,6 +2,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 { LookupResponse } from '@ghostfolio/common/interfaces';
import type { RequestWithUser } from '@ghostfolio/common/types';
import {
@@ -21,7 +22,6 @@ import { parseISO } from 'date-fns';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { isDate, isEmpty } from 'lodash';
-import { LookupItem } from './interfaces/lookup-item.interface';
import { SymbolItem } from './interfaces/symbol-item.interface';
import { SymbolService } from './symbol.service';
@@ -41,7 +41,7 @@ export class SymbolController {
public async lookupSymbol(
@Query('includeIndices') includeIndicesParam = 'false',
@Query('query') query = ''
- ): Promise<{ items: LookupItem[] }> {
+ ): Promise {
const includeIndices = includeIndicesParam === 'true';
try {
diff --git a/apps/api/src/app/symbol/symbol.service.ts b/apps/api/src/app/symbol/symbol.service.ts
index 2baca18dd..56befb9b6 100644
--- a/apps/api/src/app/symbol/symbol.service.ts
+++ b/apps/api/src/app/symbol/symbol.service.ts
@@ -5,13 +5,15 @@ import {
} from '@ghostfolio/api/services/interfaces/interfaces';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { DATE_FORMAT } from '@ghostfolio/common/helper';
-import { HistoricalDataItem } from '@ghostfolio/common/interfaces';
+import {
+ HistoricalDataItem,
+ LookupResponse
+} from '@ghostfolio/common/interfaces';
import { UserWithSettings } from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common';
import { format, subDays } from 'date-fns';
-import { LookupItem } from './interfaces/lookup-item.interface';
import { SymbolItem } from './interfaces/symbol-item.interface';
@Injectable()
@@ -84,7 +86,7 @@ export class SymbolService {
try {
historicalData = await this.dataProviderService.getHistoricalRaw({
- dataGatheringItems: [{ dataSource, symbol }],
+ assetProfileIdentifiers: [{ dataSource, symbol }],
from: date,
to: date
});
@@ -104,8 +106,8 @@ export class SymbolService {
includeIndices?: boolean;
query: string;
user: UserWithSettings;
- }): Promise<{ items: LookupItem[] }> {
- const results: { items: LookupItem[] } = { items: [] };
+ }): Promise {
+ const results: LookupResponse = { items: [] };
if (!query) {
return results;
diff --git a/apps/api/src/app/user/update-user-setting.dto.ts b/apps/api/src/app/user/update-user-setting.dto.ts
index 317b4d689..d8e06790b 100644
--- a/apps/api/src/app/user/update-user-setting.dto.ts
+++ b/apps/api/src/app/user/update-user-setting.dto.ts
@@ -67,6 +67,14 @@ export class UpdateUserSettingDto {
@IsOptional()
'filters.assetClasses'?: string[];
+ @IsString()
+ @IsOptional()
+ 'filters.dataSource'?: string;
+
+ @IsString()
+ @IsOptional()
+ 'filters.symbol'?: string;
+
@IsArray()
@IsOptional()
'filters.tags'?: string[];
diff --git a/apps/api/src/app/user/user.service.ts b/apps/api/src/app/user/user.service.ts
index 288e2aba2..443a2a052 100644
--- a/apps/api/src/app/user/user.service.ts
+++ b/apps/api/src/app/user/user.service.ts
@@ -37,7 +37,7 @@ import { UserWithSettings } from '@ghostfolio/common/types';
import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { Prisma, Role, User } from '@prisma/client';
-import { differenceInDays } from 'date-fns';
+import { differenceInDays, subDays } from 'date-fns';
import { sortBy, without } from 'lodash';
const crypto = require('crypto');
@@ -60,6 +60,13 @@ export class UserService {
return this.prismaService.user.count(args);
}
+ public createAccessToken(password: string, salt: string): string {
+ const hash = crypto.createHmac('sha512', salt);
+ hash.update(password);
+
+ return hash.digest('hex');
+ }
+
public async getUser(
{ Account, id, permissions, Settings, subscription }: UserWithSettings,
aLocale = locale
@@ -358,13 +365,6 @@ export class UserService {
});
}
- public createAccessToken(password: string, salt: string): string {
- const hash = crypto.createHmac('sha512', salt);
- hash.update(password);
-
- return hash.digest('hex');
- }
-
public async createUser({
data
}: {
@@ -426,17 +426,6 @@ export class UserService {
return user;
}
- public async updateUser(params: {
- where: Prisma.UserWhereUniqueInput;
- data: Prisma.UserUpdateInput;
- }): Promise {
- const { where, data } = params;
- return this.prismaService.user.update({
- data,
- where
- });
- }
-
public async deleteUser(where: Prisma.UserWhereUniqueInput): Promise {
try {
await this.prismaService.access.deleteMany({
@@ -473,6 +462,32 @@ export class UserService {
});
}
+ public async resetAnalytics() {
+ return this.prismaService.analytics.updateMany({
+ data: {
+ dataProviderGhostfolioDailyRequests: 0
+ },
+ where: {
+ updatedAt: {
+ gte: subDays(new Date(), 1)
+ }
+ }
+ });
+ }
+
+ public async updateUser({
+ data,
+ where
+ }: {
+ data: Prisma.UserUpdateInput;
+ where: Prisma.UserWhereUniqueInput;
+ }): Promise {
+ return this.prismaService.user.update({
+ data,
+ where
+ });
+ }
+
public async updateUserSetting({
emitPortfolioChangedEvent,
userId,
diff --git a/apps/api/src/services/cron.service.ts b/apps/api/src/services/cron.service.ts
index 17e970c1b..7a1b30b5f 100644
--- a/apps/api/src/services/cron.service.ts
+++ b/apps/api/src/services/cron.service.ts
@@ -1,3 +1,4 @@
+import { UserService } from '@ghostfolio/api/app/user/user.service';
import {
DATA_GATHERING_QUEUE_PRIORITY_LOW,
GATHER_ASSET_PROFILE_PROCESS,
@@ -9,6 +10,7 @@ import { getAssetProfileIdentifier } from '@ghostfolio/common/helper';
import { Injectable } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
+import { ConfigurationService } from './configuration/configuration.service';
import { ExchangeRateDataService } from './exchange-rate-data/exchange-rate-data.service';
import { PropertyService } from './property/property.service';
import { DataGatheringService } from './queues/data-gathering/data-gathering.service';
@@ -19,10 +21,12 @@ export class CronService {
private static readonly EVERY_SUNDAY_AT_LUNCH_TIME = '0 12 * * 0';
public constructor(
+ private readonly configurationService: ConfigurationService,
private readonly dataGatheringService: DataGatheringService,
private readonly exchangeRateDataService: ExchangeRateDataService,
private readonly propertyService: PropertyService,
- private readonly twitterBotService: TwitterBotService
+ private readonly twitterBotService: TwitterBotService,
+ private readonly userService: UserService
) {}
@Cron(CronExpression.EVERY_HOUR)
@@ -42,6 +46,13 @@ export class CronService {
this.twitterBotService.tweetFearAndGreedIndex();
}
+ @Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT)
+ public async runEveryDayAtMidnight() {
+ if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
+ this.userService.resetAnalytics();
+ }
+ }
+
@Cron(CronService.EVERY_SUNDAY_AT_LUNCH_TIME)
public async runEverySundayAtTwelvePm() {
if (await this.isDataGatheringEnabled()) {
diff --git a/apps/api/src/services/data-provider/alpha-vantage/alpha-vantage.service.ts b/apps/api/src/services/data-provider/alpha-vantage/alpha-vantage.service.ts
index 016584949..5c9eee127 100644
--- a/apps/api/src/services/data-provider/alpha-vantage/alpha-vantage.service.ts
+++ b/apps/api/src/services/data-provider/alpha-vantage/alpha-vantage.service.ts
@@ -1,4 +1,3 @@
-import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import {
DataProviderInterface,
@@ -12,7 +11,10 @@ import {
IDataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces';
import { DATE_FORMAT } from '@ghostfolio/common/helper';
-import { DataProviderInfo } from '@ghostfolio/common/interfaces';
+import {
+ DataProviderInfo,
+ LookupResponse
+} from '@ghostfolio/common/interfaces';
import { Injectable } from '@nestjs/common';
import { DataSource, SymbolProfile } from '@prisma/client';
@@ -119,9 +121,7 @@ export class AlphaVantageService implements DataProviderInterface {
return undefined;
}
- public async search({
- query
- }: GetSearchParams): Promise<{ items: LookupItem[] }> {
+ public async search({ query }: GetSearchParams): Promise {
const result = await this.alphaVantage.data.search(query);
return {
diff --git a/apps/api/src/services/data-provider/coingecko/coingecko.service.ts b/apps/api/src/services/data-provider/coingecko/coingecko.service.ts
index d420c51fd..7d6f22c60 100644
--- a/apps/api/src/services/data-provider/coingecko/coingecko.service.ts
+++ b/apps/api/src/services/data-provider/coingecko/coingecko.service.ts
@@ -1,4 +1,3 @@
-import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import {
DataProviderInterface,
@@ -13,7 +12,11 @@ import {
} from '@ghostfolio/api/services/interfaces/interfaces';
import { DEFAULT_CURRENCY } from '@ghostfolio/common/config';
import { DATE_FORMAT } from '@ghostfolio/common/helper';
-import { DataProviderInfo } from '@ghostfolio/common/interfaces';
+import {
+ DataProviderInfo,
+ LookupItem,
+ LookupResponse
+} from '@ghostfolio/common/interfaces';
import { Injectable, Logger } from '@nestjs/common';
import {
@@ -221,9 +224,7 @@ export class CoinGeckoService implements DataProviderInterface {
return 'bitcoin';
}
- public async search({
- query
- }: GetSearchParams): Promise<{ items: LookupItem[] }> {
+ public async search({ query }: GetSearchParams): Promise {
let items: LookupItem[] = [];
try {
diff --git a/apps/api/src/services/data-provider/data-provider.service.ts b/apps/api/src/services/data-provider/data-provider.service.ts
index 9853b0186..2c7b2e08e 100644
--- a/apps/api/src/services/data-provider/data-provider.service.ts
+++ b/apps/api/src/services/data-provider/data-provider.service.ts
@@ -1,5 +1,4 @@
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
-import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
import { LogPerformance } from '@ghostfolio/api/interceptors/performance-logging/performance-logging.interceptor';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { DataProviderInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
@@ -21,7 +20,11 @@ import {
getStartOfUtcDate,
isDerivedCurrency
} from '@ghostfolio/common/helper';
-import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
+import {
+ AssetProfileIdentifier,
+ LookupItem,
+ LookupResponse
+} from '@ghostfolio/common/interfaces';
import type { Granularity, UserWithSettings } from '@ghostfolio/common/types';
import { Inject, Injectable, Logger } from '@nestjs/common';
@@ -89,11 +92,11 @@ export class DataProviderService {
const promises = [];
- for (const [dataSource, dataGatheringItems] of Object.entries(
+ for (const [dataSource, assetProfileIdentifiers] of Object.entries(
itemsGroupedByDataSource
)) {
- const symbols = dataGatheringItems.map((dataGatheringItem) => {
- return dataGatheringItem.symbol;
+ const symbols = assetProfileIdentifiers.map(({ symbol }) => {
+ return symbol;
});
for (const symbol of symbols) {
@@ -240,11 +243,11 @@ export class DataProviderService {
}
public async getHistoricalRaw({
- dataGatheringItems,
+ assetProfileIdentifiers,
from,
to
}: {
- dataGatheringItems: AssetProfileIdentifier[];
+ assetProfileIdentifiers: AssetProfileIdentifier[];
from: Date;
to: Date;
}): Promise<{
@@ -253,25 +256,32 @@ export class DataProviderService {
for (const { currency, rootCurrency } of DERIVED_CURRENCIES) {
if (
this.hasCurrency({
- dataGatheringItems,
+ assetProfileIdentifiers,
currency: `${DEFAULT_CURRENCY}${currency}`
})
) {
// Skip derived currency
- dataGatheringItems = dataGatheringItems.filter(({ symbol }) => {
- return symbol !== `${DEFAULT_CURRENCY}${currency}`;
- });
+ assetProfileIdentifiers = assetProfileIdentifiers.filter(
+ ({ symbol }) => {
+ return symbol !== `${DEFAULT_CURRENCY}${currency}`;
+ }
+ );
// Add root currency
- dataGatheringItems.push({
+ assetProfileIdentifiers.push({
dataSource: this.getDataSourceForExchangeRates(),
symbol: `${DEFAULT_CURRENCY}${rootCurrency}`
});
}
}
- dataGatheringItems = uniqWith(dataGatheringItems, (obj1, obj2) => {
- return obj1.dataSource === obj2.dataSource && obj1.symbol === obj2.symbol;
- });
+ assetProfileIdentifiers = uniqWith(
+ assetProfileIdentifiers,
+ (obj1, obj2) => {
+ return (
+ obj1.dataSource === obj2.dataSource && obj1.symbol === obj2.symbol
+ );
+ }
+ );
const result: {
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
@@ -281,7 +291,7 @@ export class DataProviderService {
data: { [date: string]: IDataProviderHistoricalResponse };
symbol: string;
}>[] = [];
- for (const { dataSource, symbol } of dataGatheringItems) {
+ for (const { dataSource, symbol } of assetProfileIdentifiers) {
const dataProvider = this.getDataProvider(dataSource);
if (dataProvider.canHandle(symbol)) {
if (symbol === `${DEFAULT_CURRENCY}USX`) {
@@ -417,7 +427,7 @@ export class DataProviderService {
const promises: Promise[] = [];
- for (const [dataSource, dataGatheringItems] of Object.entries(
+ for (const [dataSource, assetProfileIdentifiers] of Object.entries(
itemsGroupedByDataSource
)) {
const dataProvider = this.getDataProvider(DataSource[dataSource]);
@@ -430,7 +440,7 @@ export class DataProviderService {
continue;
}
- const symbols = dataGatheringItems
+ const symbols = assetProfileIdentifiers
.filter(({ symbol }) => {
return !isDerivedCurrency(getCurrencyFromSymbol(symbol));
})
@@ -594,9 +604,9 @@ export class DataProviderService {
includeIndices?: boolean;
query: string;
user: UserWithSettings;
- }): Promise<{ items: LookupItem[] }> {
- const promises: Promise<{ items: LookupItem[] }>[] = [];
+ }): Promise {
let lookupItems: LookupItem[] = [];
+ const promises: Promise[] = [];
if (query?.length < 2) {
return { items: lookupItems };
@@ -626,9 +636,9 @@ export class DataProviderService {
});
const filteredItems = lookupItems
- .filter((lookupItem) => {
+ .filter(({ currency }) => {
// Only allow symbols with supported currency
- return lookupItem.currency ? true : false;
+ return currency ? true : false;
})
.sort(({ name: name1 }, { name: name2 }) => {
return name1?.toLowerCase().localeCompare(name2?.toLowerCase());
@@ -654,13 +664,13 @@ export class DataProviderService {
}
private hasCurrency({
- currency,
- dataGatheringItems
+ assetProfileIdentifiers,
+ currency
}: {
+ assetProfileIdentifiers: AssetProfileIdentifier[];
currency: string;
- dataGatheringItems: AssetProfileIdentifier[];
}) {
- return dataGatheringItems.some(({ dataSource, symbol }) => {
+ return assetProfileIdentifiers.some(({ dataSource, symbol }) => {
return (
dataSource === this.getDataSourceForExchangeRates() &&
symbol === currency
diff --git a/apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts b/apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts
index c3c948b47..7329b821b 100644
--- a/apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts
+++ b/apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts
@@ -1,4 +1,3 @@
-import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import {
DataProviderInterface,
@@ -17,7 +16,11 @@ import {
REPLACE_NAME_PARTS
} from '@ghostfolio/common/config';
import { DATE_FORMAT, isCurrency } from '@ghostfolio/common/helper';
-import { DataProviderInfo } from '@ghostfolio/common/interfaces';
+import {
+ DataProviderInfo,
+ LookupItem,
+ LookupResponse
+} from '@ghostfolio/common/interfaces';
import { MarketState } from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common';
@@ -317,9 +320,7 @@ export class EodHistoricalDataService implements DataProviderInterface {
return 'AAPL.US';
}
- public async search({
- query
- }: GetSearchParams): Promise<{ items: LookupItem[] }> {
+ public async search({ query }: GetSearchParams): Promise {
const searchResult = await this.getSearchResult(query);
return {
diff --git a/apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts b/apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts
index 7d5b38479..9334fc4cd 100644
--- a/apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts
+++ b/apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts
@@ -1,4 +1,3 @@
-import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import {
DataProviderInterface,
@@ -13,7 +12,11 @@ import {
} from '@ghostfolio/api/services/interfaces/interfaces';
import { DEFAULT_CURRENCY } from '@ghostfolio/common/config';
import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
-import { DataProviderInfo } from '@ghostfolio/common/interfaces';
+import {
+ DataProviderInfo,
+ LookupItem,
+ LookupResponse
+} from '@ghostfolio/common/interfaces';
import { Injectable, Logger } from '@nestjs/common';
import { DataSource, SymbolProfile } from '@prisma/client';
@@ -169,9 +172,7 @@ export class FinancialModelingPrepService implements DataProviderInterface {
return 'AAPL';
}
- public async search({
- query
- }: GetSearchParams): Promise<{ items: LookupItem[] }> {
+ public async search({ query }: GetSearchParams): Promise {
let items: LookupItem[] = [];
try {
diff --git a/apps/api/src/services/data-provider/google-sheets/google-sheets.service.ts b/apps/api/src/services/data-provider/google-sheets/google-sheets.service.ts
index 9f2344233..f18d670d1 100644
--- a/apps/api/src/services/data-provider/google-sheets/google-sheets.service.ts
+++ b/apps/api/src/services/data-provider/google-sheets/google-sheets.service.ts
@@ -1,4 +1,3 @@
-import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import {
DataProviderInterface,
@@ -14,7 +13,10 @@ import {
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
-import { DataProviderInfo } from '@ghostfolio/common/interfaces';
+import {
+ DataProviderInfo,
+ LookupResponse
+} from '@ghostfolio/common/interfaces';
import { Injectable, Logger } from '@nestjs/common';
import { DataSource, SymbolProfile } from '@prisma/client';
@@ -157,9 +159,7 @@ export class GoogleSheetsService implements DataProviderInterface {
return 'INDEXSP:.INX';
}
- public async search({
- query
- }: GetSearchParams): Promise<{ items: LookupItem[] }> {
+ public async search({ query }: GetSearchParams): Promise {
const items = await this.prismaService.symbolProfile.findMany({
select: {
assetClass: true,
diff --git a/apps/api/src/services/data-provider/interfaces/data-provider.interface.ts b/apps/api/src/services/data-provider/interfaces/data-provider.interface.ts
index 3b3644473..7352ce78a 100644
--- a/apps/api/src/services/data-provider/interfaces/data-provider.interface.ts
+++ b/apps/api/src/services/data-provider/interfaces/data-provider.interface.ts
@@ -1,9 +1,11 @@
-import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
import {
IDataProviderHistoricalResponse,
IDataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces';
-import { DataProviderInfo } from '@ghostfolio/common/interfaces';
+import {
+ DataProviderInfo,
+ LookupResponse
+} from '@ghostfolio/common/interfaces';
import { Granularity } from '@ghostfolio/common/types';
import { DataSource, SymbolProfile } from '@prisma/client';
@@ -44,10 +46,7 @@ export interface DataProviderInterface {
getTestSymbol(): string;
- search({
- includeIndices,
- query
- }: GetSearchParams): Promise<{ items: LookupItem[] }>;
+ search({ includeIndices, query }: GetSearchParams): Promise;
}
export interface GetDividendsParams {
diff --git a/apps/api/src/services/data-provider/manual/manual.service.ts b/apps/api/src/services/data-provider/manual/manual.service.ts
index b202f42d2..8d6e6012c 100644
--- a/apps/api/src/services/data-provider/manual/manual.service.ts
+++ b/apps/api/src/services/data-provider/manual/manual.service.ts
@@ -1,4 +1,3 @@
-import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import {
DataProviderInterface,
@@ -21,6 +20,7 @@ import {
} from '@ghostfolio/common/helper';
import {
DataProviderInfo,
+ LookupResponse,
ScraperConfiguration
} from '@ghostfolio/common/interfaces';
@@ -227,9 +227,7 @@ export class ManualService implements DataProviderInterface {
return undefined;
}
- public async search({
- query
- }: GetSearchParams): Promise<{ items: LookupItem[] }> {
+ public async search({ query }: GetSearchParams): Promise {
let items = await this.prismaService.symbolProfile.findMany({
select: {
assetClass: true,
diff --git a/apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts b/apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts
index e47e96d88..29e7f4ee9 100644
--- a/apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts
+++ b/apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts
@@ -1,4 +1,3 @@
-import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import {
DataProviderInterface,
@@ -13,7 +12,10 @@ import {
} from '@ghostfolio/api/services/interfaces/interfaces';
import { ghostfolioFearAndGreedIndexSymbol } from '@ghostfolio/common/config';
import { DATE_FORMAT, getYesterday } from '@ghostfolio/common/helper';
-import { DataProviderInfo } from '@ghostfolio/common/interfaces';
+import {
+ DataProviderInfo,
+ LookupResponse
+} from '@ghostfolio/common/interfaces';
import { Injectable, Logger } from '@nestjs/common';
import { DataSource, SymbolProfile } from '@prisma/client';
@@ -121,7 +123,7 @@ export class RapidApiService implements DataProviderInterface {
return undefined;
}
- public async search({}: GetSearchParams): Promise<{ items: LookupItem[] }> {
+ public async search({}: GetSearchParams): Promise {
return { items: [] };
}
diff --git a/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts b/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts
index 2d67c646c..27da18ab0 100644
--- a/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts
+++ b/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts
@@ -1,4 +1,3 @@
-import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service';
import { YahooFinanceDataEnhancerService } from '@ghostfolio/api/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service';
import {
@@ -14,7 +13,11 @@ import {
} from '@ghostfolio/api/services/interfaces/interfaces';
import { DEFAULT_CURRENCY } from '@ghostfolio/common/config';
import { DATE_FORMAT } from '@ghostfolio/common/helper';
-import { DataProviderInfo } from '@ghostfolio/common/interfaces';
+import {
+ DataProviderInfo,
+ LookupItem,
+ LookupResponse
+} from '@ghostfolio/common/interfaces';
import { Injectable, Logger } from '@nestjs/common';
import { DataSource, SymbolProfile } from '@prisma/client';
@@ -224,7 +227,7 @@ export class YahooFinanceService implements DataProviderInterface {
public async search({
includeIndices = false,
query
- }: GetSearchParams): Promise<{ items: LookupItem[] }> {
+ }: GetSearchParams): Promise {
const items: LookupItem[] = [];
try {
diff --git a/apps/api/src/services/prisma/prisma.module.ts b/apps/api/src/services/prisma/prisma.module.ts
index 3875c8cab..24da61047 100644
--- a/apps/api/src/services/prisma/prisma.module.ts
+++ b/apps/api/src/services/prisma/prisma.module.ts
@@ -1,9 +1,10 @@
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { Module } from '@nestjs/common';
+import { ConfigService } from '@nestjs/config';
@Module({
- providers: [PrismaService],
- exports: [PrismaService]
+ exports: [PrismaService],
+ providers: [ConfigService, PrismaService]
})
export class PrismaModule {}
diff --git a/apps/api/src/services/prisma/prisma.service.ts b/apps/api/src/services/prisma/prisma.service.ts
index e99d6ecf9..4673cbd19 100644
--- a/apps/api/src/services/prisma/prisma.service.ts
+++ b/apps/api/src/services/prisma/prisma.service.ts
@@ -1,16 +1,38 @@
import {
Injectable,
Logger,
+ LogLevel,
OnModuleDestroy,
OnModuleInit
} from '@nestjs/common';
-import { PrismaClient } from '@prisma/client';
+import { ConfigService } from '@nestjs/config';
+import { Prisma, PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService
extends PrismaClient
implements OnModuleInit, OnModuleDestroy
{
+ public constructor(configService: ConfigService) {
+ let customLogLevels: LogLevel[];
+
+ try {
+ customLogLevels = JSON.parse(
+ configService.get('LOG_LEVELS')
+ ) as LogLevel[];
+ } catch {}
+
+ const log: Prisma.LogDefinition[] =
+ customLogLevels?.includes('debug') || customLogLevels?.includes('verbose')
+ ? [{ emit: 'stdout', level: 'query' }]
+ : [];
+
+ super({
+ log,
+ errorFormat: 'colorless'
+ });
+ }
+
public async onModuleInit() {
try {
await this.$connect();
diff --git a/apps/api/src/services/queues/data-gathering/data-gathering.processor.ts b/apps/api/src/services/queues/data-gathering/data-gathering.processor.ts
index 88f3baaf9..1f88375d5 100644
--- a/apps/api/src/services/queues/data-gathering/data-gathering.processor.ts
+++ b/apps/api/src/services/queues/data-gathering/data-gathering.processor.ts
@@ -96,7 +96,7 @@ export class DataGatheringProcessor {
);
const historicalData = await this.dataProviderService.getHistoricalRaw({
- dataGatheringItems: [{ dataSource, symbol }],
+ assetProfileIdentifiers: [{ dataSource, symbol }],
from: currentDate,
to: new Date()
});
@@ -214,7 +214,7 @@ export class DataGatheringProcessor {
dates = dates.filter((d) => !marketData.some((md) => isEqual(md, d)));
const historicalData = await this.dataProviderService.getHistoricalRaw({
- dataGatheringItems: [{ dataSource, symbol }],
+ assetProfileIdentifiers: [{ dataSource, symbol }],
from: firstEntry.date,
to: new Date()
});
diff --git a/apps/api/src/services/queues/data-gathering/data-gathering.service.ts b/apps/api/src/services/queues/data-gathering/data-gathering.service.ts
index 24b174785..45b429d48 100644
--- a/apps/api/src/services/queues/data-gathering/data-gathering.service.ts
+++ b/apps/api/src/services/queues/data-gathering/data-gathering.service.ts
@@ -142,7 +142,7 @@ export class DataGatheringService {
}) {
try {
const historicalData = await this.dataProviderService.getHistoricalRaw({
- dataGatheringItems: [{ dataSource, symbol }],
+ assetProfileIdentifiers: [{ dataSource, symbol }],
from: date,
to: date
});
diff --git a/apps/client/src/app/app.component.html b/apps/client/src/app/app.component.html
index b12855488..7560e15e5 100644
--- a/apps/client/src/app/app.component.html
+++ b/apps/client/src/app/app.component.html
@@ -33,6 +33,7 @@
[deviceType]="deviceType"
[hasPermissionToChangeDateRange]="hasPermissionToChangeDateRange"
[hasPermissionToChangeFilters]="hasPermissionToChangeFilters"
+ [hasPromotion]="hasPromotion"
[hasTabs]="hasTabs"
[info]="info"
[pageTitle]="pageTitle"
diff --git a/apps/client/src/app/app.component.ts b/apps/client/src/app/app.component.ts
index 75841686c..86d4282a2 100644
--- a/apps/client/src/app/app.component.ts
+++ b/apps/client/src/app/app.component.ts
@@ -57,6 +57,7 @@ export class AppComponent implements OnDestroy, OnInit {
public hasPermissionToAccessFearAndGreedIndex: boolean;
public hasPermissionToChangeDateRange: boolean;
public hasPermissionToChangeFilters: boolean;
+ public hasPromotion = false;
public hasTabs = false;
public info: InfoItem;
public pageTitle: string;
@@ -136,6 +137,10 @@ export class AppComponent implements OnDestroy, OnInit {
permissions.enableFearAndGreedIndex
);
+ this.hasPromotion =
+ !!this.info?.subscriptionOffers?.default?.coupon ||
+ !!this.info?.subscriptionOffers?.default?.durationExtension;
+
this.impersonationStorageService
.onChangeHasImpersonation()
.pipe(takeUntil(this.unsubscribeSubject))
@@ -231,6 +236,14 @@ export class AppComponent implements OnDestroy, OnInit {
this.hasInfoMessage =
this.canCreateAccount || !!this.user?.systemMessage;
+ this.hasPromotion =
+ !!this.info?.subscriptionOffers?.[
+ this.user?.subscription?.offer ?? 'default'
+ ]?.coupon ||
+ !!this.info?.subscriptionOffers?.[
+ this.user?.subscription?.offer ?? 'default'
+ ]?.durationExtension;
+
this.initializeTheme(this.user?.settings.colorScheme);
this.changeDetectorRef.markForCheck();
diff --git a/apps/client/src/app/components/header/header.component.html b/apps/client/src/app/components/header/header.component.html
index ff36c2ebe..8a611d935 100644
--- a/apps/client/src/app/components/header/header.component.html
+++ b/apps/client/src/app/components/header/header.component.html
@@ -88,15 +88,20 @@
Pricing
+
+ Pricing
+ @if (currentRoute !== routePricing && hasPromotion) {
+ %
+ }
+
+
}
@@ -290,12 +295,17 @@
) {
Pricing
+
+ Pricing
+ @if (currentRoute !== routePricing && hasPromotion) {
+ %
+ }
+
+
}
Pricing
+
+ Pricing
+ @if (currentRoute !== routePricing && hasPromotion) {
+ %
+ }
+
+
}
@if (hasPermissionToAccessFearAndGreedIndex) {
diff --git a/apps/client/src/app/components/header/header.component.ts b/apps/client/src/app/components/header/header.component.ts
index af69f7b39..004fa5f3f 100644
--- a/apps/client/src/app/components/header/header.component.ts
+++ b/apps/client/src/app/components/header/header.component.ts
@@ -58,6 +58,7 @@ export class HeaderComponent implements OnChanges {
@Input() deviceType: string;
@Input() hasPermissionToChangeDateRange: boolean;
@Input() hasPermissionToChangeFilters: boolean;
+ @Input() hasPromotion: boolean;
@Input() hasTabs: boolean;
@Input() info: InfoItem;
@Input() pageTitle: string;
@@ -174,24 +175,18 @@ export class HeaderComponent implements OnChanges {
const userSetting: UpdateUserSettingDto = {};
for (const filter of filters) {
- const filtersType = this.getFilterType(filter.type);
-
- const userFilters = filters
- .filter((f) => f.type === filter.type && filter.id)
- .map((f) => f.id);
-
- userSetting[`filters.${filtersType}`] = userFilters.length
- ? userFilters
- : null;
+ if (filter.type === 'ACCOUNT') {
+ userSetting['filters.accounts'] = filter.id ? [filter.id] : null;
+ } else if (filter.type === 'ASSET_CLASS') {
+ userSetting['filters.assetClasses'] = filter.id ? [filter.id] : null;
+ } else if (filter.type === 'DATA_SOURCE') {
+ userSetting['filters.dataSource'] = filter.id ? filter.id : null;
+ } else if (filter.type === 'SYMBOL') {
+ userSetting['filters.symbol'] = filter.id ? filter.id : null;
+ } else if (filter.type === 'TAG') {
+ userSetting['filters.tags'] = filter.id ? [filter.id] : null;
+ }
}
- ['ACCOUNT', 'ASSET_CLASS', 'TAG']
- .filter(
- (fitlerType) =>
- !filters.some((f: Filter) => f.type.toString() === fitlerType)
- )
- .forEach((filterType) => {
- userSetting[`filters.${this.getFilterType(filterType)}`] = null;
- });
this.dataService
.putUserSetting(userSetting)
@@ -285,13 +280,4 @@ export class HeaderComponent implements OnChanges {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
- private getFilterType(filterType: string) {
- if (filterType === 'ACCOUNT') {
- return 'accounts';
- } else if (filterType === 'ASSET_CLASS') {
- return 'assetClasses';
- } else if (filterType === 'TAG') {
- return 'tags';
- }
- }
}
diff --git a/apps/client/src/app/components/user-account-membership/user-account-membership.component.ts b/apps/client/src/app/components/user-account-membership/user-account-membership.component.ts
index 93bbe641c..bde555d8e 100644
--- a/apps/client/src/app/components/user-account-membership/user-account-membership.component.ts
+++ b/apps/client/src/app/components/user-account-membership/user-account-membership.component.ts
@@ -16,6 +16,7 @@ import {
MatSnackBarRef,
TextOnlySnackBar
} from '@angular/material/snack-bar';
+import { StringValue } from 'ms';
import { StripeService } from 'ngx-stripe';
import { EMPTY, Subject } from 'rxjs';
import { catchError, switchMap, takeUntil } from 'rxjs/operators';
@@ -31,6 +32,7 @@ export class UserAccountMembershipComponent implements OnDestroy {
public coupon: number;
public couponId: string;
public defaultDateFormat: string;
+ public durationExtension: StringValue;
public hasPermissionForSubscription: boolean;
public hasPermissionToUpdateUserSettings: boolean;
public price: number;
@@ -51,7 +53,7 @@ export class UserAccountMembershipComponent implements OnDestroy {
private stripeService: StripeService,
private userService: UserService
) {
- const { baseCurrency, globalPermissions, subscriptions } =
+ const { baseCurrency, globalPermissions, subscriptionOffers } =
this.dataService.fetchInfo();
this.baseCurrency = baseCurrency;
@@ -76,11 +78,18 @@ export class UserAccountMembershipComponent implements OnDestroy {
permissions.updateUserSettings
);
- this.coupon = subscriptions?.[this.user.subscription.offer]?.coupon;
+ this.coupon =
+ subscriptionOffers?.[this.user.subscription.offer]?.coupon;
this.couponId =
- subscriptions?.[this.user.subscription.offer]?.couponId;
- this.price = subscriptions?.[this.user.subscription.offer]?.price;
- this.priceId = subscriptions?.[this.user.subscription.offer]?.priceId;
+ subscriptionOffers?.[this.user.subscription.offer]?.couponId;
+ this.durationExtension =
+ subscriptionOffers?.[
+ this.user.subscription.offer
+ ]?.durationExtension;
+ this.price =
+ subscriptionOffers?.[this.user.subscription.offer]?.price;
+ this.priceId =
+ subscriptionOffers?.[this.user.subscription.offer]?.priceId;
this.changeDetectorRef.markForCheck();
}
diff --git a/apps/client/src/app/components/user-account-membership/user-account-membership.html b/apps/client/src/app/components/user-account-membership/user-account-membership.html
index d30ce7bdd..82b329a64 100644
--- a/apps/client/src/app/components/user-account-membership/user-account-membership.html
+++ b/apps/client/src/app/components/user-account-membership/user-account-membership.html
@@ -34,6 +34,16 @@
per year
}
+ @if (durationExtension) {
+
+
+ Limited Offer! Get
+ {{ durationExtension }} extra
+
+
+ }
}
@if (!user?.subscription?.expiresAt) {
diff --git a/apps/client/src/app/pages/portfolio/fire/fire-page-routing.module.ts b/apps/client/src/app/pages/portfolio/fire/fire-page-routing.module.ts
index 885dc5509..96260adda 100644
--- a/apps/client/src/app/pages/portfolio/fire/fire-page-routing.module.ts
+++ b/apps/client/src/app/pages/portfolio/fire/fire-page-routing.module.ts
@@ -10,7 +10,7 @@ const routes: Routes = [
canActivate: [AuthGuard],
component: FirePageComponent,
path: '',
- title: $localize`FIRE`
+ title: 'FIRE'
}
];
diff --git a/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts b/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts
index d20c66912..897b9824e 100644
--- a/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts
+++ b/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts
@@ -1,12 +1,7 @@
-import { UpdateUserSettingDto } from '@ghostfolio/api/app/user/update-user-setting.dto';
import { DataService } from '@ghostfolio/client/services/data.service';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
-import {
- PortfolioReport,
- PortfolioReportRule,
- User
-} from '@ghostfolio/common/interfaces';
+import { User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
@@ -21,18 +16,11 @@ import { takeUntil } from 'rxjs/operators';
templateUrl: './fire-page.html'
})
export class FirePageComponent implements OnDestroy, OnInit {
- public accountClusterRiskRules: PortfolioReportRule[];
- public currencyClusterRiskRules: PortfolioReportRule[];
public deviceType: string;
- public economicMarketClusterRiskRules: PortfolioReportRule[];
- public emergencyFundRules: PortfolioReportRule[];
- public feeRules: PortfolioReportRule[];
public fireWealth: Big;
public hasImpersonationId: boolean;
public hasPermissionToUpdateUserSettings: boolean;
- public inactiveRules: PortfolioReportRule[];
public isLoading = false;
- public isLoadingPortfolioReport = false;
public user: User;
public withdrawalRatePerMonth: Big;
public withdrawalRatePerYear: Big;
@@ -95,8 +83,6 @@ export class FirePageComponent implements OnDestroy, OnInit {
this.changeDetectorRef.markForCheck();
}
});
-
- this.initializePortfolioReport();
}
public onAnnualInterestRateChange(annualInterestRate: number) {
@@ -133,21 +119,6 @@ export class FirePageComponent implements OnDestroy, OnInit {
});
});
}
-
- public onRulesUpdated(event: UpdateUserSettingDto) {
- this.dataService
- .putUserSetting(event)
- .pipe(takeUntil(this.unsubscribeSubject))
- .subscribe(() => {
- this.userService
- .get(true)
- .pipe(takeUntil(this.unsubscribeSubject))
- .subscribe();
-
- this.initializePortfolioReport();
- });
- }
-
public onSavingsRateChange(savingsRate: number) {
this.dataService
.putUserSetting({ savingsRate })
@@ -187,66 +158,4 @@ export class FirePageComponent implements OnDestroy, OnInit {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
-
- private initializePortfolioReport() {
- this.isLoadingPortfolioReport = true;
-
- this.dataService
- .fetchPortfolioReport()
- .pipe(takeUntil(this.unsubscribeSubject))
- .subscribe((portfolioReport) => {
- this.inactiveRules = this.mergeInactiveRules(portfolioReport);
-
- this.accountClusterRiskRules =
- portfolioReport.rules['accountClusterRisk']?.filter(
- ({ isActive }) => {
- return isActive;
- }
- ) ?? null;
-
- this.currencyClusterRiskRules =
- portfolioReport.rules['currencyClusterRisk']?.filter(
- ({ isActive }) => {
- return isActive;
- }
- ) ?? null;
-
- this.economicMarketClusterRiskRules =
- portfolioReport.rules['economicMarketClusterRisk']?.filter(
- ({ isActive }) => {
- return isActive;
- }
- ) ?? null;
-
- this.emergencyFundRules =
- portfolioReport.rules['emergencyFund']?.filter(({ isActive }) => {
- return isActive;
- }) ?? null;
-
- this.feeRules =
- portfolioReport.rules['fees']?.filter(({ isActive }) => {
- return isActive;
- }) ?? null;
-
- this.isLoadingPortfolioReport = false;
-
- this.changeDetectorRef.markForCheck();
- });
- }
-
- private mergeInactiveRules(report: PortfolioReport): PortfolioReportRule[] {
- let inactiveRules: PortfolioReportRule[] = [];
-
- for (const category in report.rules) {
- const rulesArray = report.rules[category];
-
- inactiveRules = inactiveRules.concat(
- rulesArray.filter(({ isActive }) => {
- return !isActive;
- })
- );
- }
-
- return inactiveRules;
- }
}
diff --git a/apps/client/src/app/pages/portfolio/fire/fire-page.html b/apps/client/src/app/pages/portfolio/fire/fire-page.html
index 7a336b62f..77fd1640c 100644
--- a/apps/client/src/app/pages/portfolio/fire/fire-page.html
+++ b/apps/client/src/app/pages/portfolio/fire/fire-page.html
@@ -101,133 +101,3 @@
}
-
-
-
-
-
X-ray
-
- Ghostfolio X-ray uses static analysis to identify potential issues
- and risks in your portfolio.
- It will be highly configurable in the future: activate / deactivate
- rules and customize the thresholds to match your personal investment
- style.
-
-
-
- Emergency Fund
- @if (user?.subscription?.type === 'Basic') {
-
- }
-
-
-
-
-
- Currency Cluster Risks
- @if (user?.subscription?.type === 'Basic') {
-
- }
-
-
-
-
-
- Account Cluster Risks
- @if (user?.subscription?.type === 'Basic') {
-
- }
-
-
-
-
-
- Economic Market Cluster Risks
- @if (user?.subscription?.type === 'Basic') {
-
- }
-
-
-
-
-
- Fees
- @if (user?.subscription?.type === 'Basic') {
-
- }
-
-
-
- @if (inactiveRules?.length > 0) {
-
-
Inactive
-
-
- }
-
-
-
diff --git a/apps/client/src/app/pages/portfolio/fire/fire-page.module.ts b/apps/client/src/app/pages/portfolio/fire/fire-page.module.ts
index 60e3127d9..a606ae1b4 100644
--- a/apps/client/src/app/pages/portfolio/fire/fire-page.module.ts
+++ b/apps/client/src/app/pages/portfolio/fire/fire-page.module.ts
@@ -1,4 +1,3 @@
-import { GfRulesModule } from '@ghostfolio/client/components/rules/rules.module';
import { GfFireCalculatorComponent } from '@ghostfolio/ui/fire-calculator';
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
import { GfValueComponent } from '@ghostfolio/ui/value';
@@ -17,7 +16,6 @@ import { FirePageComponent } from './fire-page.component';
FirePageRoutingModule,
GfFireCalculatorComponent,
GfPremiumIndicatorComponent,
- GfRulesModule,
GfValueComponent,
NgxSkeletonLoaderModule
],
diff --git a/apps/client/src/app/pages/portfolio/portfolio-page-routing.module.ts b/apps/client/src/app/pages/portfolio/portfolio-page-routing.module.ts
index 6146c573c..20de6f8fa 100644
--- a/apps/client/src/app/pages/portfolio/portfolio-page-routing.module.ts
+++ b/apps/client/src/app/pages/portfolio/portfolio-page-routing.module.ts
@@ -34,6 +34,11 @@ const routes: Routes = [
path: 'fire',
loadChildren: () =>
import('./fire/fire-page.module').then((m) => m.FirePageModule)
+ },
+ {
+ path: 'x-ray',
+ loadChildren: () =>
+ import('./x-ray/x-ray-page.module').then((m) => m.XRayPageModule)
}
],
component: PortfolioPageComponent,
diff --git a/apps/client/src/app/pages/portfolio/portfolio-page.component.ts b/apps/client/src/app/pages/portfolio/portfolio-page.component.ts
index 0c980e25b..7f40bf1d7 100644
--- a/apps/client/src/app/pages/portfolio/portfolio-page.component.ts
+++ b/apps/client/src/app/pages/portfolio/portfolio-page.component.ts
@@ -46,8 +46,13 @@ export class PortfolioPageComponent implements OnDestroy, OnInit {
},
{
iconName: 'calculator-outline',
- label: 'FIRE / X-ray',
+ label: 'FIRE ',
path: ['/portfolio', 'fire']
+ },
+ {
+ iconName: 'scan-outline',
+ label: 'X-ray',
+ path: ['/portfolio', 'x-ray']
}
];
this.user = state.user;
diff --git a/apps/client/src/app/pages/portfolio/x-ray/x-ray-page-routing.module.ts b/apps/client/src/app/pages/portfolio/x-ray/x-ray-page-routing.module.ts
new file mode 100644
index 000000000..091cbc49f
--- /dev/null
+++ b/apps/client/src/app/pages/portfolio/x-ray/x-ray-page-routing.module.ts
@@ -0,0 +1,21 @@
+import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
+
+import { NgModule } from '@angular/core';
+import { RouterModule, Routes } from '@angular/router';
+
+import { XRayPageComponent } from './x-ray-page.component';
+
+const routes: Routes = [
+ {
+ canActivate: [AuthGuard],
+ component: XRayPageComponent,
+ path: '',
+ title: 'X-ray'
+ }
+];
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule]
+})
+export class XRayPageRoutingModule {}
diff --git a/apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.html b/apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.html
new file mode 100644
index 000000000..cd03b49bb
--- /dev/null
+++ b/apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.html
@@ -0,0 +1,123 @@
+
+
+
+
X-ray
+
+ Ghostfolio X-ray uses static analysis to uncover potential issues and
+ risks in your portfolio. Adjust the rules below and set custom
+ thresholds to align with your personal investment strategy.
+
+
+
+ Emergency Fund
+ @if (user?.subscription?.type === 'Basic') {
+
+ }
+
+
+
+
+
+ Currency Cluster Risks
+ @if (user?.subscription?.type === 'Basic') {
+
+ }
+
+
+
+
+
+ Account Cluster Risks
+ @if (user?.subscription?.type === 'Basic') {
+
+ }
+
+
+
+
+
+ Economic Market Cluster Risks
+ @if (user?.subscription?.type === 'Basic') {
+
+ }
+
+
+
+
+
+ Fees
+ @if (user?.subscription?.type === 'Basic') {
+
+ }
+
+
+
+ @if (inactiveRules?.length > 0) {
+
+
Inactive
+
+
+ }
+
+
+
diff --git a/apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.scss b/apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.scss
new file mode 100644
index 000000000..5d4e87f30
--- /dev/null
+++ b/apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.scss
@@ -0,0 +1,3 @@
+:host {
+ display: block;
+}
diff --git a/apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.ts b/apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.ts
new file mode 100644
index 000000000..36f42fc3e
--- /dev/null
+++ b/apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.ts
@@ -0,0 +1,150 @@
+import { UpdateUserSettingDto } from '@ghostfolio/api/app/user/update-user-setting.dto';
+import { DataService } from '@ghostfolio/client/services/data.service';
+import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
+import { UserService } from '@ghostfolio/client/services/user/user.service';
+import {
+ PortfolioReportRule,
+ PortfolioReport
+} from '@ghostfolio/common/interfaces';
+import { User } from '@ghostfolio/common/interfaces/user.interface';
+import { hasPermission, permissions } from '@ghostfolio/common/permissions';
+
+import { ChangeDetectorRef, Component } from '@angular/core';
+import { Subject, takeUntil } from 'rxjs';
+
+@Component({
+ selector: 'gf-x-ray-page',
+ styleUrl: './x-ray-page.component.scss',
+ templateUrl: './x-ray-page.component.html'
+})
+export class XRayPageComponent {
+ public accountClusterRiskRules: PortfolioReportRule[];
+ public currencyClusterRiskRules: PortfolioReportRule[];
+ public economicMarketClusterRiskRules: PortfolioReportRule[];
+ public emergencyFundRules: PortfolioReportRule[];
+ public feeRules: PortfolioReportRule[];
+ public hasImpersonationId: boolean;
+ public hasPermissionToUpdateUserSettings: boolean;
+ public inactiveRules: PortfolioReportRule[];
+ public isLoadingPortfolioReport = false;
+ public user: User;
+
+ private unsubscribeSubject = new Subject();
+
+ public constructor(
+ private changeDetectorRef: ChangeDetectorRef,
+ private dataService: DataService,
+ private impersonationStorageService: ImpersonationStorageService,
+ private userService: UserService
+ ) {}
+
+ public ngOnInit() {
+ this.impersonationStorageService
+ .onChangeHasImpersonation()
+ .pipe(takeUntil(this.unsubscribeSubject))
+ .subscribe((impersonationId) => {
+ this.hasImpersonationId = !!impersonationId;
+ });
+
+ this.userService.stateChanged
+ .pipe(takeUntil(this.unsubscribeSubject))
+ .subscribe((state) => {
+ if (state?.user) {
+ this.user = state.user;
+
+ this.hasPermissionToUpdateUserSettings =
+ this.user.subscription?.type === 'Basic'
+ ? false
+ : hasPermission(
+ this.user.permissions,
+ permissions.updateUserSettings
+ );
+
+ this.changeDetectorRef.markForCheck();
+ }
+ });
+
+ this.initializePortfolioReport();
+ }
+
+ public onRulesUpdated(event: UpdateUserSettingDto) {
+ this.dataService
+ .putUserSetting(event)
+ .pipe(takeUntil(this.unsubscribeSubject))
+ .subscribe(() => {
+ this.userService
+ .get(true)
+ .pipe(takeUntil(this.unsubscribeSubject))
+ .subscribe();
+
+ this.initializePortfolioReport();
+ });
+ }
+
+ public ngOnDestroy() {
+ this.unsubscribeSubject.next();
+ this.unsubscribeSubject.complete();
+ }
+
+ private initializePortfolioReport() {
+ this.isLoadingPortfolioReport = true;
+
+ this.dataService
+ .fetchPortfolioReport()
+ .pipe(takeUntil(this.unsubscribeSubject))
+ .subscribe((portfolioReport) => {
+ this.inactiveRules = this.mergeInactiveRules(portfolioReport);
+
+ this.accountClusterRiskRules =
+ portfolioReport.rules['accountClusterRisk']?.filter(
+ ({ isActive }) => {
+ return isActive;
+ }
+ ) ?? null;
+
+ this.currencyClusterRiskRules =
+ portfolioReport.rules['currencyClusterRisk']?.filter(
+ ({ isActive }) => {
+ return isActive;
+ }
+ ) ?? null;
+
+ this.economicMarketClusterRiskRules =
+ portfolioReport.rules['economicMarketClusterRisk']?.filter(
+ ({ isActive }) => {
+ return isActive;
+ }
+ ) ?? null;
+
+ this.emergencyFundRules =
+ portfolioReport.rules['emergencyFund']?.filter(({ isActive }) => {
+ return isActive;
+ }) ?? null;
+
+ this.feeRules =
+ portfolioReport.rules['fees']?.filter(({ isActive }) => {
+ return isActive;
+ }) ?? null;
+
+ this.isLoadingPortfolioReport = false;
+
+ this.changeDetectorRef.markForCheck();
+ });
+ }
+
+ private mergeInactiveRules(report: PortfolioReport): PortfolioReportRule[] {
+ let inactiveRules: PortfolioReportRule[] = [];
+
+ for (const category in report.rules) {
+ const rulesArray = report.rules[category];
+
+ inactiveRules = inactiveRules.concat(
+ rulesArray.filter(({ isActive }) => {
+ return !isActive;
+ })
+ );
+ }
+
+ return inactiveRules;
+ }
+}
diff --git a/apps/client/src/app/pages/portfolio/x-ray/x-ray-page.module.ts b/apps/client/src/app/pages/portfolio/x-ray/x-ray-page.module.ts
new file mode 100644
index 000000000..bff4f4dc9
--- /dev/null
+++ b/apps/client/src/app/pages/portfolio/x-ray/x-ray-page.module.ts
@@ -0,0 +1,22 @@
+import { GfRulesModule } from '@ghostfolio/client/components/rules/rules.module';
+import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
+
+import { CommonModule } from '@angular/common';
+import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
+import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
+
+import { XRayPageRoutingModule } from './x-ray-page-routing.module';
+import { XRayPageComponent } from './x-ray-page.component';
+
+@NgModule({
+ declarations: [XRayPageComponent],
+ imports: [
+ CommonModule,
+ GfPremiumIndicatorComponent,
+ GfRulesModule,
+ NgxSkeletonLoaderModule,
+ XRayPageRoutingModule
+ ],
+ schemas: [CUSTOM_ELEMENTS_SCHEMA]
+})
+export class XRayPageModule {}
diff --git a/apps/client/src/app/pages/pricing/pricing-page.component.ts b/apps/client/src/app/pages/pricing/pricing-page.component.ts
index 8bd0f1bd5..f86a75904 100644
--- a/apps/client/src/app/pages/pricing/pricing-page.component.ts
+++ b/apps/client/src/app/pages/pricing/pricing-page.component.ts
@@ -6,6 +6,7 @@ import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { translate } from '@ghostfolio/ui/i18n';
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
+import { StringValue } from 'ms';
import { StripeService } from 'ngx-stripe';
import { Subject } from 'rxjs';
import { catchError, switchMap, takeUntil } from 'rxjs/operators';
@@ -20,6 +21,7 @@ export class PricingPageComponent implements OnDestroy, OnInit {
public baseCurrency: string;
public coupon: number;
public couponId: string;
+ public durationExtension: StringValue;
public hasPermissionToUpdateUserSettings: boolean;
public importAndExportTooltipBasic = translate(
'DATA_IMPORT_AND_EXPORT_TOOLTIP_BASIC'
@@ -51,11 +53,12 @@ export class PricingPageComponent implements OnDestroy, OnInit {
) {}
public ngOnInit() {
- const { baseCurrency, subscriptions } = this.dataService.fetchInfo();
+ const { baseCurrency, subscriptionOffers } = this.dataService.fetchInfo();
this.baseCurrency = baseCurrency;
- this.coupon = subscriptions?.default?.coupon;
- this.price = subscriptions?.default?.price;
+ this.coupon = subscriptionOffers?.default?.coupon;
+ this.durationExtension = subscriptionOffers?.default?.durationExtension;
+ this.price = subscriptionOffers?.default?.price;
this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject))
@@ -68,11 +71,18 @@ export class PricingPageComponent implements OnDestroy, OnInit {
permissions.updateUserSettings
);
- this.coupon = subscriptions?.[this.user?.subscription?.offer]?.coupon;
+ this.coupon =
+ subscriptionOffers?.[this.user?.subscription?.offer]?.coupon;
this.couponId =
- subscriptions?.[this.user.subscription.offer]?.couponId;
- this.price = subscriptions?.[this.user?.subscription?.offer]?.price;
- this.priceId = subscriptions?.[this.user.subscription.offer]?.priceId;
+ subscriptionOffers?.[this.user.subscription.offer]?.couponId;
+ this.durationExtension =
+ subscriptionOffers?.[
+ this.user?.subscription?.offer
+ ]?.durationExtension;
+ this.price =
+ subscriptionOffers?.[this.user?.subscription?.offer]?.price;
+ this.priceId =
+ subscriptionOffers?.[this.user.subscription.offer]?.priceId;
this.changeDetectorRef.markForCheck();
}
diff --git a/apps/client/src/app/pages/pricing/pricing-page.html b/apps/client/src/app/pages/pricing/pricing-page.html
index fe805ef62..605ad5d2e 100644
--- a/apps/client/src/app/pages/pricing/pricing-page.html
+++ b/apps/client/src/app/pages/pricing/pricing-page.html
@@ -101,6 +101,11 @@
}
+ @if (durationExtension) {
+
+ }
@@ -159,6 +164,11 @@
}
+ @if (durationExtension) {
+
+ }
@@ -289,6 +299,14 @@
}
+ @if (durationExtension) {
+
+
+ Limited Offer! Get
+ {{ durationExtension }} extra
+
+
+ }
diff --git a/apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts b/apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts
index ea14bbc6b..39dbc4813 100644
--- a/apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts
+++ b/apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts
@@ -35,9 +35,9 @@ export class GfProductPageComponent implements OnInit {
) {}
public ngOnInit() {
- const { subscriptions } = this.dataService.fetchInfo();
+ const { subscriptionOffers } = this.dataService.fetchInfo();
- this.price = subscriptions?.default?.price;
+ this.price = subscriptionOffers?.default?.price;
this.product1 = {
founded: 2021,
diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts
index 8a6d087d1..ac9772352 100644
--- a/apps/client/src/app/services/data.service.ts
+++ b/apps/client/src/app/services/data.service.ts
@@ -10,7 +10,6 @@ import {
} from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
import { PortfolioHoldingDetail } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-holding-detail.interface';
-import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
import { SymbolItem } from '@ghostfolio/api/app/symbol/interfaces/symbol-item.interface';
import { DeleteOwnUserDto } from '@ghostfolio/api/app/user/delete-own-user.dto';
import { UserItem } from '@ghostfolio/api/app/user/interfaces/user-item.interface';
@@ -30,6 +29,7 @@ import {
Filter,
ImportResponse,
InfoItem,
+ LookupResponse,
OAuthResponse,
PortfolioDetails,
PortfolioDividends,
@@ -464,10 +464,10 @@ export class DataService {
}
return this.http
- .get<{ items: LookupItem[] }>('/api/v1/symbol/lookup', { params })
+ .get('/api/v1/symbol/lookup', { params })
.pipe(
- map((respose) => {
- return respose.items;
+ map(({ items }) => {
+ return items;
})
);
}
@@ -535,7 +535,7 @@ export class DataService {
}: {
filters?: Filter[];
range?: DateRange;
- }) {
+ } = {}) {
let params = this.buildFiltersAsQueryParams({ filters });
if (range) {
diff --git a/apps/client/src/app/services/user/user.service.ts b/apps/client/src/app/services/user/user.service.ts
index 3f550bb01..184df97a3 100644
--- a/apps/client/src/app/services/user/user.service.ts
+++ b/apps/client/src/app/services/user/user.service.ts
@@ -65,6 +65,20 @@ export class UserService extends ObservableStore {
});
}
+ if (user?.settings['filters.dataSource']) {
+ filters.push({
+ id: user.settings['filters.dataSource'],
+ type: 'DATA_SOURCE'
+ });
+ }
+
+ if (user?.settings['filters.symbol']) {
+ filters.push({
+ id: user.settings['filters.symbol'],
+ type: 'SYMBOL'
+ });
+ }
+
if (user?.settings['filters.tags']) {
filters.push({
id: user.settings['filters.tags'].join(','),
diff --git a/apps/client/src/locales/messages.it.xlf b/apps/client/src/locales/messages.it.xlf
index 9364d8356..9654329eb 100644
--- a/apps/client/src/locales/messages.it.xlf
+++ b/apps/client/src/locales/messages.it.xlf
@@ -2872,7 +2872,7 @@
Hello, has shared a Portfolio with you!
- Salve, ha condiviso un Portafoglio con te!
+ Salve, ha condiviso un Portafoglio con te!
apps/client/src/app/pages/public/public-page.html
4
@@ -5881,7 +5881,7 @@
Currency Cluster Risks
- Currency Cluster Risks
+ Rischio di Concentrazione Valutario
apps/client/src/app/pages/portfolio/fire/fire-page.html
141
@@ -5889,7 +5889,7 @@
Account Cluster Risks
- Account Cluster Risks
+ Rischi di Concentrazione dei Conti
apps/client/src/app/pages/portfolio/fire/fire-page.html
160
@@ -6237,7 +6237,7 @@
Restricted view
- Restricted view
+ Vista limitata
apps/client/src/app/components/access-table/access-table.component.html
26
@@ -6357,7 +6357,7 @@
WTD
- WTD
+ Settimana corrente
libs/ui/src/lib/assistant/assistant.component.ts
212
@@ -6373,7 +6373,7 @@
MTD
- MTD
+ Mese corrente
libs/ui/src/lib/assistant/assistant.component.ts
216
@@ -6597,7 +6597,7 @@
{VAR_PLURAL, plural, =1 {activity} other {activities}}
- {VAR_PLURAL, plural, =1 {activity} other {activities}}
+ {VAR_PLURAL, plural, =1 {attività} other {attività}}
apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html
14
@@ -6781,7 +6781,7 @@
Family Office
- Family Office
+ Ufficio familiare
apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts
87
@@ -6837,7 +6837,7 @@
User Experience
- User Experience
+ Esperienza Utente
apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts
98
@@ -7061,7 +7061,7 @@
Copy link to clipboard
- Copy link to clipboard
+ Copia link negli appunti
apps/client/src/app/components/access-table/access-table.component.html
70
@@ -7069,7 +7069,7 @@
Portfolio Snapshot
- Portfolio Snapshot
+ Stato del Portfolio
apps/client/src/app/components/admin-jobs/admin-jobs.html
39
@@ -7077,7 +7077,7 @@
Change with currency effect Change
- Change with currency effect Change
+ Cambio con effetto valuta Cambia
apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html
50
@@ -7085,7 +7085,7 @@
Performance with currency effect Performance
- Performance with currency effect Performance
+ Prestazioni con effetto valuta Prestazioni
apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html
69
@@ -7093,7 +7093,7 @@
Threshold Min
- Threshold Min
+ Soglia Minima
apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html
9
@@ -7101,7 +7101,7 @@
Threshold Max
- Threshold Max
+ Soglia Massima
apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html
44
@@ -7109,7 +7109,7 @@
Close
- Close
+ Chiudi
apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html
77
@@ -7117,7 +7117,7 @@
Customize
- Customize
+ Personalizza
apps/client/src/app/components/rule/rule.component.html
67
@@ -7125,7 +7125,7 @@
No auto-renewal.
- No auto-renewal.
+ No rinnovo automatico.
apps/client/src/app/components/user-account-membership/user-account-membership.html
62
@@ -7133,7 +7133,7 @@
Today
- Today
+ Oggi
apps/client/src/app/pages/public/public-page.html
24
@@ -7141,7 +7141,7 @@
This year
- This year
+ Anno corrente
apps/client/src/app/pages/public/public-page.html
42
@@ -7149,7 +7149,7 @@
From the beginning
- From the beginning
+ Dall'inizio
apps/client/src/app/pages/public/public-page.html
60
@@ -7157,7 +7157,7 @@
Oops! Invalid currency.
- Oops! Invalid currency.
+ Oops! Valuta sbagliata.
apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html
49
@@ -7165,7 +7165,7 @@
This page has been archived.
- This page has been archived.
+ Questa pagina è stata archiviata.
apps/client/src/app/pages/resources/personal-finance-tools/product-page.html
14
@@ -7173,7 +7173,7 @@
is Open Source Software
- is Open Source Software
+ è un programma Open Source
apps/client/src/app/pages/resources/personal-finance-tools/product-page.html
139
@@ -7181,7 +7181,7 @@
is not Open Source Software
- is not Open Source Software
+ non è un programma Open Source
apps/client/src/app/pages/resources/personal-finance-tools/product-page.html
146
@@ -7189,7 +7189,7 @@
is Open Source Software
- is Open Source Software
+ è un programma Open Source
apps/client/src/app/pages/resources/personal-finance-tools/product-page.html
156
@@ -7197,7 +7197,7 @@
is not Open Source Software
- is not Open Source Software
+ non è un programma Open Source
apps/client/src/app/pages/resources/personal-finance-tools/product-page.html
163
@@ -7205,7 +7205,7 @@
can be self-hosted
- can be self-hosted
+ può essere ospitato in proprio
apps/client/src/app/pages/resources/personal-finance-tools/product-page.html
178
@@ -7213,7 +7213,7 @@
cannot be self-hosted
- cannot be self-hosted
+ non può essere ospitato in proprio
apps/client/src/app/pages/resources/personal-finance-tools/product-page.html
185
@@ -7221,7 +7221,7 @@
can be self-hosted
- can be self-hosted
+ può essere ospitato in proprio
apps/client/src/app/pages/resources/personal-finance-tools/product-page.html
195
@@ -7229,7 +7229,7 @@
cannot be self-hosted
- cannot be self-hosted
+ non può essere ospitato in proprio
apps/client/src/app/pages/resources/personal-finance-tools/product-page.html
202
@@ -7237,7 +7237,7 @@
can be used anonymously
- can be used anonymously
+ può essere usato anonimamente
apps/client/src/app/pages/resources/personal-finance-tools/product-page.html
217
@@ -7245,7 +7245,7 @@
cannot be used anonymously
- cannot be used anonymously
+ non può essere usato anonimamente
apps/client/src/app/pages/resources/personal-finance-tools/product-page.html
224
@@ -7253,7 +7253,7 @@
can be used anonymously
- can be used anonymously
+ può essere usato anonimamente
apps/client/src/app/pages/resources/personal-finance-tools/product-page.html
234
@@ -7261,7 +7261,7 @@
cannot be used anonymously
- cannot be used anonymously
+ non può essere usato anonimamente
apps/client/src/app/pages/resources/personal-finance-tools/product-page.html
241
@@ -7269,7 +7269,7 @@
offers a free plan
- offers a free plan
+ ha un piano gratuito
apps/client/src/app/pages/resources/personal-finance-tools/product-page.html
256
@@ -7277,7 +7277,7 @@
does not offer a free plan
- does not offer a free plan
+ non ha un piano gratuito
apps/client/src/app/pages/resources/personal-finance-tools/product-page.html
263
@@ -7285,7 +7285,7 @@
offers a free plan
- offers a free plan
+ ha un piano gratuito
apps/client/src/app/pages/resources/personal-finance-tools/product-page.html
273
@@ -7293,7 +7293,7 @@
does not offer a free plan
- does not offer a free plan
+ non ha un piano gratuito
apps/client/src/app/pages/resources/personal-finance-tools/product-page.html
280
@@ -7301,7 +7301,7 @@
Oops! Could not find any assets.
- Oops! Could not find any assets.
+ Oops! Non ho trovato alcun asset.
libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html
37
@@ -7309,7 +7309,7 @@
Data Providers
- Data Providers
+ Fornitori di dati
apps/client/src/app/components/admin-settings/admin-settings.component.html
4
@@ -7317,7 +7317,7 @@
NEW
- NEW
+ NUOVO
apps/client/src/app/components/admin-settings/admin-settings.component.html
14
@@ -7325,7 +7325,7 @@
Set API Key
- Set API Key
+ Imposta API Key
apps/client/src/app/components/admin-settings/admin-settings.component.html
29
@@ -7333,7 +7333,7 @@
Want to stay updated? Click below to get notified as soon as it’s available.
- Want to stay updated? Click below to get notified as soon as it’s available.
+ Vuoi seguire le novità ? Clicca sotto per essere notificato appena è disponibile.
apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.html
23
@@ -7341,7 +7341,7 @@
Notify me
- Notify me
+ Notificami
apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.html
32
@@ -7349,7 +7349,7 @@
Get access to 100’000+ tickers from over 50 exchanges
- Get access to 100’000+ tickers from over 50 exchanges
+ Ottieni accesso a oltre 100’000+ titoli da oltre 50 borse
libs/ui/src/lib/i18n.ts
24
@@ -7357,7 +7357,7 @@
Ukraine
- Ukraine
+ Ucraina
libs/ui/src/lib/i18n.ts
92
@@ -7365,7 +7365,7 @@
Skip
- Skip
+ Salta
apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.html
83
@@ -7373,7 +7373,7 @@
Join now
- Join now
+ Iscriviti adesso
apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.html
93
@@ -7381,7 +7381,7 @@
Allocation Cluster Risks
- Allocation Cluster Risks
+ Rischi di allocazione dei Conti
apps/client/src/app/pages/portfolio/fire/fire-page.html
179
@@ -7389,7 +7389,7 @@
Glossary
- Glossary
+ Glossario
apps/client/src/app/pages/resources/glossary/resources-glossary-routing.module.ts
10
@@ -7401,7 +7401,7 @@
Guides
- Guides
+ Guide
apps/client/src/app/pages/resources/guides/resources-guides-routing.module.ts
10
@@ -7413,7 +7413,7 @@
guides
- guides
+ guide
snake-case
apps/client/src/app/pages/resources/overview/resources-overview.component.ts
@@ -7426,7 +7426,7 @@
glossary
- glossary
+ glossario
snake-case
apps/client/src/app/pages/resources/overview/resources-overview.component.ts
@@ -7439,4 +7439,4 @@