Browse Source

Merge remote-tracking branch 'origin/main' into task/enable-eslint-prefer-nullish-coalescing

pull/7317/head
KenTandrian 2 days ago
parent
commit
ddf138b44c
  1. 12
      CHANGELOG.md
  2. 18
      apps/api/src/app/activities/activities.service.ts
  3. 39
      apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.controller.ts
  4. 2
      apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.module.ts
  5. 33
      apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.service.ts
  6. 43
      apps/api/src/app/endpoints/market-data/market-data.controller.ts
  7. 8
      apps/api/src/app/endpoints/market-data/market-data.module.ts
  8. 7
      apps/api/src/app/endpoints/public/public.controller.ts
  9. 12
      apps/api/src/app/export/export.service.ts
  10. 38
      apps/api/src/app/import/import.service.ts
  11. 2
      apps/api/src/app/portfolio/calculator/portfolio-calculator-test-utils.ts
  12. 4
      apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
  13. 26
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-buy.spec.ts
  14. 462
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts
  15. 26
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell.spec.ts
  16. 38
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy.spec.ts
  17. 14
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur-in-base-currency-eur.spec.ts
  18. 14
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur.spec.ts
  19. 26
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts
  20. 12
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-short.spec.ts
  21. 12
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd.spec.ts
  22. 14
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-fee.spec.ts
  23. 14
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-googl-buy.spec.ts
  24. 380
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-jnug-buy-and-sell-and-buy-and-sell.spec.ts
  25. 14
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-liability.spec.ts
  26. 38
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-and-sell.spec.ts
  27. 26
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-with-dividend.spec.ts
  28. 12
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell-partially.spec.ts
  29. 528
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell.spec.ts
  30. 14
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-valuable.spec.ts
  31. 2
      apps/api/src/app/portfolio/interfaces/portfolio-order.interface.ts
  32. 6
      apps/api/src/app/portfolio/portfolio.service.spec.ts
  33. 18
      apps/api/src/app/portfolio/portfolio.service.ts
  34. 46
      apps/api/src/app/symbol/symbol.service.ts
  35. 7
      apps/api/src/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor.ts
  36. 4
      apps/api/src/services/twitter-bot/twitter-bot.service.ts
  37. 22
      apps/client/src/app/pages/api/api-page.component.ts
  38. 52
      apps/client/src/app/pages/api/api-page.html
  39. 2
      apps/client/src/app/pages/portfolio/activities/activities-page.component.ts
  40. 30
      apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts
  41. 4
      apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/interfaces/interfaces.ts
  42. 8
      apps/client/src/app/services/import-activities.service.ts
  43. 12
      libs/common/src/lib/config.ts
  44. 11
      libs/common/src/lib/dtos/country.dto.ts
  45. 14
      libs/common/src/lib/dtos/create-asset-profile.dto.ts
  46. 11
      libs/common/src/lib/dtos/holding.dto.ts
  47. 8
      libs/common/src/lib/dtos/index.ts
  48. 35
      libs/common/src/lib/dtos/scraper-configuration.dto.ts
  49. 11
      libs/common/src/lib/dtos/sector.dto.ts
  50. 17
      libs/common/src/lib/dtos/update-asset-profile.dto.ts
  51. 8
      libs/common/src/lib/interfaces/activities.interface.ts
  52. 6
      libs/common/src/lib/interfaces/responses/public-portfolio-response.interface.ts
  53. 16
      libs/ui/src/lib/activities-table/activities-table.component.html
  54. 10
      libs/ui/src/lib/activities-table/activities-table.component.stories.ts
  55. 4
      libs/ui/src/lib/activities-table/activities-table.component.ts
  56. 536
      package-lock.json
  57. 24
      package.json

12
CHANGELOG.md

@ -7,9 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased ## Unreleased
### Added
- Added the markets endpoint for the _Fear & Greed Index_ (market mood) to the `GHOSTFOLIO` data provider
### Changed ### Changed
- Hardened the validation of the countries in the asset profile endpoints
- Hardened the validation of the holdings in the asset profile endpoints
- Hardened the validation of the scraper configuration in the asset profile endpoint
- Hardened the validation of the sectors in the asset profile endpoints
- Rounded the value of the _Fear & Greed Index_ (market mood) in the twitter bot service
- Set the change detection strategy to `OnPush` in the _X-ray_ page - Set the change detection strategy to `OnPush` in the _X-ray_ page
- Deprecated `SymbolProfile` in favor of `assetProfile` in the activity interface
- Upgraded `countries-list` from version `3.3.0` to `3.4.0`
- Upgraded `Nx` from version `23.0.1` to `23.0.2`
## 3.25.0 - 2026-07-12 ## 3.25.0 - 2026-07-12

18
apps/api/src/app/activities/activities.service.ts

@ -455,6 +455,23 @@ export class ActivitiesService {
userId, userId,
accountId: account.id, accountId: account.id,
accountUserId: account.userId, accountUserId: account.userId,
assetProfile: {
activitiesCount: 0,
assetClass: AssetClass.LIQUIDITY,
assetSubClass: AssetSubClass.CASH,
countries: [],
createdAt: new Date(balanceItem.date),
currency: account.currency,
dataSource:
this.dataProviderService.getDataSourceForExchangeRates(),
holdings: [],
id: account.currency,
isActive: true,
name: account.currency,
sectors: [],
symbol: account.currency,
updatedAt: new Date(balanceItem.date)
},
comment: account.name, comment: account.name,
createdAt: new Date(balanceItem.date), createdAt: new Date(balanceItem.date),
currency: account.currency, currency: account.currency,
@ -823,6 +840,7 @@ export class ActivitiesService {
return { return {
...order, ...order,
assetProfile,
feeInAssetProfileCurrency, feeInAssetProfileCurrency,
feeInBaseCurrency, feeInBaseCurrency,
unitPriceInAssetProfileCurrency, unitPriceInAssetProfileCurrency,

39
apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.controller.ts

@ -8,6 +8,7 @@ import {
DividendsResponse, DividendsResponse,
HistoricalResponse, HistoricalResponse,
LookupResponse, LookupResponse,
MarketDataOfMarketsResponse,
QuotesResponse QuotesResponse
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { permissions } from '@ghostfolio/common/permissions'; import { permissions } from '@ghostfolio/common/permissions';
@ -19,6 +20,7 @@ import {
HttpException, HttpException,
Inject, Inject,
Param, Param,
ParseIntPipe,
Query, Query,
UseGuards, UseGuards,
Version Version
@ -203,6 +205,43 @@ export class GhostfolioController {
} }
} }
@Get('markets')
@HasPermission(permissions.enableDataProviderGhostfolio)
@UseGuards(AuthGuard('api-key'), HasPermissionGuard)
public async getMarketDataOfMarkets(
@Query('includeHistoricalData', new ParseIntPipe({ optional: true }))
includeHistoricalData = 0
): Promise<MarketDataOfMarketsResponse> {
const maxDailyRequests = await this.ghostfolioService.getMaxDailyRequests();
if (
this.request.user.dataProviderGhostfolioDailyRequests > maxDailyRequests
) {
throw new HttpException(
getReasonPhrase(StatusCodes.TOO_MANY_REQUESTS),
StatusCodes.TOO_MANY_REQUESTS
);
}
try {
const marketDataOfMarkets =
await this.ghostfolioService.getMarketDataOfMarkets({
includeHistoricalData
});
await this.ghostfolioService.incrementDailyRequests({
userId: this.request.user.id
});
return marketDataOfMarkets;
} catch {
throw new HttpException(
getReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR),
StatusCodes.INTERNAL_SERVER_ERROR
);
}
}
@Get('quotes') @Get('quotes')
@HasPermission(permissions.enableDataProviderGhostfolio) @HasPermission(permissions.enableDataProviderGhostfolio)
@UseGuards(AuthGuard('api-key'), HasPermissionGuard) @UseGuards(AuthGuard('api-key'), HasPermissionGuard)

2
apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.module.ts

@ -1,4 +1,5 @@
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module'; import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
import { SymbolModule } from '@ghostfolio/api/app/symbol/symbol.module';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { CryptocurrencyModule } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.module'; import { CryptocurrencyModule } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.module';
import { AlphaVantageService } from '@ghostfolio/api/services/data-provider/alpha-vantage/alpha-vantage.service'; import { AlphaVantageService } from '@ghostfolio/api/services/data-provider/alpha-vantage/alpha-vantage.service';
@ -33,6 +34,7 @@ import { GhostfolioService } from './ghostfolio.service';
PrismaModule, PrismaModule,
PropertyModule, PropertyModule,
RedisCacheModule, RedisCacheModule,
SymbolModule,
SymbolProfileModule SymbolProfileModule
], ],
providers: [ providers: [

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

@ -1,3 +1,4 @@
import { SymbolService } from '@ghostfolio/api/app/symbol/symbol.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { GhostfolioService as GhostfolioDataProviderService } from '@ghostfolio/api/services/data-provider/ghostfolio/ghostfolio.service'; import { GhostfolioService as GhostfolioDataProviderService } from '@ghostfolio/api/services/data-provider/ghostfolio/ghostfolio.service';
@ -25,6 +26,7 @@ import {
HistoricalResponse, HistoricalResponse,
LookupItem, LookupItem,
LookupResponse, LookupResponse,
MarketDataOfMarketsResponse,
QuotesResponse QuotesResponse
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { UserWithSettings } from '@ghostfolio/common/types'; import { UserWithSettings } from '@ghostfolio/common/types';
@ -32,6 +34,7 @@ import { UserWithSettings } from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { DataSource, SymbolProfile } from '@prisma/client'; import { DataSource, SymbolProfile } from '@prisma/client';
import { Big } from 'big.js'; import { Big } from 'big.js';
import { isEmpty } from 'lodash';
@Injectable() @Injectable()
export class GhostfolioService { export class GhostfolioService {
@ -42,7 +45,8 @@ export class GhostfolioService {
private readonly dataProviderService: DataProviderService, private readonly dataProviderService: DataProviderService,
private readonly fetchService: FetchService, private readonly fetchService: FetchService,
private readonly prismaService: PrismaService, private readonly prismaService: PrismaService,
private readonly propertyService: PropertyService private readonly propertyService: PropertyService,
private readonly symbolService: SymbolService
) {} ) {}
public async getAssetProfile({ symbol }: GetAssetProfileParams) { public async getAssetProfile({ symbol }: GetAssetProfileParams) {
@ -198,6 +202,33 @@ export class GhostfolioService {
} }
} }
public async getMarketDataOfMarkets({
includeHistoricalData
}: {
includeHistoricalData: number;
}): Promise<MarketDataOfMarketsResponse> {
try {
const marketDataOfMarkets =
await this.symbolService.getMarketDataOfMarkets({
includeHistoricalData
});
for (const symbolItem of Object.values(
marketDataOfMarkets.fearAndGreedIndex
)) {
if (!isEmpty(symbolItem)) {
symbolItem.dataSource = DataSource.GHOSTFOLIO;
}
}
return marketDataOfMarkets;
} catch (error) {
this.logger.error(error);
throw error;
}
}
public async getMaxDailyRequests() { public async getMaxDailyRequests() {
return parseInt( return parseInt(
(await this.propertyService.getByKey<string>( (await this.propertyService.getByKey<string>(

43
apps/api/src/app/endpoints/market-data/market-data.controller.ts

@ -1,14 +1,8 @@
import { SymbolService } from '@ghostfolio/api/app/symbol/symbol.service'; import { SymbolService } from '@ghostfolio/api/app/symbol/symbol.service';
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import {
ghostfolioFearAndGreedIndexDataSourceCryptocurrencies,
ghostfolioFearAndGreedIndexSymbolCryptocurrencies,
ghostfolioFearAndGreedIndexSymbolStocks
} from '@ghostfolio/common/config';
import { UpdateBulkMarketDataDto } from '@ghostfolio/common/dtos'; import { UpdateBulkMarketDataDto } from '@ghostfolio/common/dtos';
import { getCurrencyFromSymbol, isCurrency } from '@ghostfolio/common/helper'; import { getCurrencyFromSymbol, isCurrency } from '@ghostfolio/common/helper';
import { MarketDataOfMarketsResponse } from '@ghostfolio/common/interfaces'; import { MarketDataOfMarketsResponse } from '@ghostfolio/common/interfaces';
@ -36,7 +30,6 @@ import { getReasonPhrase, StatusCodes } from 'http-status-codes';
@Controller('market-data') @Controller('market-data')
export class MarketDataController { export class MarketDataController {
public constructor( public constructor(
private readonly dataProviderService: DataProviderService,
private readonly marketDataService: MarketDataService, private readonly marketDataService: MarketDataService,
@Inject(REQUEST) private readonly request: RequestWithUser, @Inject(REQUEST) private readonly request: RequestWithUser,
private readonly symbolProfileService: SymbolProfileService, private readonly symbolProfileService: SymbolProfileService,
@ -50,39 +43,9 @@ export class MarketDataController {
@Query('includeHistoricalData', new ParseIntPipe({ optional: true })) @Query('includeHistoricalData', new ParseIntPipe({ optional: true }))
includeHistoricalData = 0 includeHistoricalData = 0
): Promise<MarketDataOfMarketsResponse> { ): Promise<MarketDataOfMarketsResponse> {
const [ return this.symbolService.getMarketDataOfMarkets({
marketDataFearAndGreedIndexCryptocurrencies, includeHistoricalData
marketDataFearAndGreedIndexStocks });
] = await Promise.all([
this.symbolService.get({
includeHistoricalData,
dataGatheringItem: {
dataSource: ghostfolioFearAndGreedIndexDataSourceCryptocurrencies,
symbol: ghostfolioFearAndGreedIndexSymbolCryptocurrencies
},
useIntradayData: true
}),
this.symbolService.get({
includeHistoricalData,
dataGatheringItem: {
dataSource:
this.dataProviderService.getDataSourceForFearAndGreedIndexStocks(),
symbol: ghostfolioFearAndGreedIndexSymbolStocks
},
useIntradayData: true
})
]);
return {
fearAndGreedIndex: {
CRYPTOCURRENCIES: {
...marketDataFearAndGreedIndexCryptocurrencies
},
STOCKS: {
...marketDataFearAndGreedIndexStocks
}
}
};
} }
@Post(':dataSource/:symbol') @Post(':dataSource/:symbol')

8
apps/api/src/app/endpoints/market-data/market-data.module.ts

@ -1,5 +1,4 @@
import { SymbolModule } from '@ghostfolio/api/app/symbol/symbol.module'; import { SymbolModule } from '@ghostfolio/api/app/symbol/symbol.module';
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
import { MarketDataModule as MarketDataServiceModule } from '@ghostfolio/api/services/market-data/market-data.module'; import { MarketDataModule as MarketDataServiceModule } from '@ghostfolio/api/services/market-data/market-data.module';
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module'; import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
@ -9,11 +8,6 @@ import { MarketDataController } from './market-data.controller';
@Module({ @Module({
controllers: [MarketDataController], controllers: [MarketDataController],
imports: [ imports: [MarketDataServiceModule, SymbolModule, SymbolProfileModule]
DataProviderModule,
MarketDataServiceModule,
SymbolModule,
SymbolProfileModule
]
}) })
export class MarketDataModule {} export class MarketDataModule {}

7
apps/api/src/app/endpoints/public/public.controller.ts

@ -110,26 +110,27 @@ export class PublicController {
? [] ? []
: activities.map( : activities.map(
({ ({
assetProfile,
currency, currency,
date, date,
fee, fee,
quantity, quantity,
SymbolProfile,
type, type,
unitPrice, unitPrice,
value, value,
valueInBaseCurrency valueInBaseCurrency
}) => { }) => {
return { return {
assetProfile,
currency, currency,
date, date,
fee, fee,
quantity, quantity,
SymbolProfile,
type, type,
unitPrice, unitPrice,
value, value,
valueInBaseCurrency valueInBaseCurrency,
SymbolProfile: assetProfile
}; };
} }
); );

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

@ -124,8 +124,8 @@ export class ExportService {
const customAssetProfiles = uniqBy( const customAssetProfiles = uniqBy(
activities activities
.map(({ SymbolProfile }) => { .map(({ assetProfile }) => {
return SymbolProfile; return assetProfile;
}) })
.filter(({ userId: assetProfileUserId }) => { .filter(({ userId: assetProfileUserId }) => {
return assetProfileUserId === userId; return assetProfileUserId === userId;
@ -224,13 +224,13 @@ export class ExportService {
activities: activities.map( activities: activities.map(
({ ({
accountId, accountId,
assetProfile,
comment, comment,
currency, currency,
date, date,
fee, fee,
id, id,
quantity, quantity,
SymbolProfile,
tags: currentTags, tags: currentTags,
type, type,
unitPrice unitPrice
@ -243,10 +243,10 @@ export class ExportService {
quantity, quantity,
type, type,
unitPrice, unitPrice,
currency: currency ?? SymbolProfile.currency, currency: currency ?? assetProfile.currency,
dataSource: SymbolProfile.dataSource, dataSource: assetProfile.dataSource,
date: date.toISOString(), date: date.toISOString(),
symbol: SymbolProfile.symbol, symbol: assetProfile.symbol,
tags: currentTags.map(({ id: tagId }) => { tags: currentTags.map(({ id: tagId }) => {
return tagId; return tagId;
}) })

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

@ -122,11 +122,11 @@ export class ImportService {
const isDuplicate = activities.some((activity) => { const isDuplicate = activities.some((activity) => {
return ( return (
activity.accountId === account?.id && activity.accountId === account?.id &&
activity.SymbolProfile.currency === assetProfile.currency && activity.assetProfile.currency === assetProfile.currency &&
activity.SymbolProfile.dataSource === assetProfile.dataSource && activity.assetProfile.dataSource === assetProfile.dataSource &&
isSameSecond(activity.date, date) && isSameSecond(activity.date, date) &&
activity.quantity === quantity && activity.quantity === quantity &&
activity.SymbolProfile.symbol === assetProfile.symbol && activity.assetProfile.symbol === assetProfile.symbol &&
activity.type === 'DIVIDEND' && activity.type === 'DIVIDEND' &&
activity.unitPrice === marketPrice activity.unitPrice === marketPrice
); );
@ -138,6 +138,7 @@ export class ImportService {
return { return {
account, account,
assetProfile,
date, date,
error, error,
quantity, quantity,
@ -463,19 +464,18 @@ export class ImportService {
const error = activity.error; const error = activity.error;
const fee = activity.fee; const fee = activity.fee;
const quantity = activity.quantity; const quantity = activity.quantity;
const SymbolProfile = activity.SymbolProfile;
const tagIds = activity.tagIds ?? []; const tagIds = activity.tagIds ?? [];
const type = activity.type; const type = activity.type;
const unitPrice = activity.unitPrice; const unitPrice = activity.unitPrice;
const assetProfile = assetProfiles[ const assetProfile = assetProfiles[
getAssetProfileIdentifier({ getAssetProfileIdentifier({
dataSource: SymbolProfile.dataSource, dataSource: activity.assetProfile.dataSource,
symbol: SymbolProfile.symbol symbol: activity.assetProfile.symbol
}) })
] ?? { ] ?? {
dataSource: SymbolProfile.dataSource, dataSource: activity.assetProfile.dataSource,
symbol: SymbolProfile.symbol symbol: activity.assetProfile.symbol
}; };
const { const {
assetClass, assetClass,
@ -619,6 +619,8 @@ export class ImportService {
activities.push({ activities.push({
...order, ...order,
// @ts-ignore
assetProfile,
error, error,
value, value,
valueInBaseCurrency, valueInBaseCurrency,
@ -633,19 +635,19 @@ export class ImportService {
if (!isDryRun) { if (!isDryRun) {
// Gather symbol data in the background, if not dry run // Gather symbol data in the background, if not dry run
const uniqueActivities = uniqBy(activities, ({ SymbolProfile }) => { const uniqueActivities = uniqBy(activities, ({ assetProfile }) => {
return getAssetProfileIdentifier({ return getAssetProfileIdentifier({
dataSource: SymbolProfile.dataSource, dataSource: assetProfile.dataSource,
symbol: SymbolProfile.symbol symbol: assetProfile.symbol
}); });
}); });
this.dataGatheringService.gatherSymbols({ this.dataGatheringService.gatherSymbols({
dataGatheringItems: uniqueActivities.map(({ date, SymbolProfile }) => { dataGatheringItems: uniqueActivities.map(({ assetProfile, date }) => {
return { return {
date, date,
dataSource: SymbolProfile.dataSource, dataSource: assetProfile.dataSource,
symbol: SymbolProfile.symbol symbol: assetProfile.symbol
}; };
}), }),
priority: DATA_GATHERING_QUEUE_PRIORITY_HIGH priority: DATA_GATHERING_QUEUE_PRIORITY_HIGH
@ -692,12 +694,12 @@ export class ImportService {
activity.accountId === accountId && activity.accountId === accountId &&
activity.comment === comment && activity.comment === comment &&
(activity.currency === currency || (activity.currency === currency ||
activity.SymbolProfile.currency === currency) && activity.assetProfile.currency === currency) &&
activity.SymbolProfile.dataSource === dataSource && activity.assetProfile.dataSource === dataSource &&
isSameSecond(activity.date, date) && isSameSecond(activity.date, date) &&
activity.fee === fee && activity.fee === fee &&
activity.quantity === quantity && activity.quantity === quantity &&
activity.SymbolProfile.symbol === symbol && activity.assetProfile.symbol === symbol &&
activity.type === type && activity.type === type &&
activity.unitPrice === unitPrice activity.unitPrice === unitPrice
); );
@ -717,7 +719,7 @@ export class ImportService {
quantity, quantity,
type, type,
unitPrice, unitPrice,
SymbolProfile: { assetProfile: {
dataSource, dataSource,
symbol, symbol,
activitiesCount: undefined, activitiesCount: undefined,

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

@ -22,7 +22,7 @@ export const activityDummyData = {
valueInBaseCurrency: undefined valueInBaseCurrency: undefined
}; };
export const symbolProfileDummyData = { export const assetProfileDummyData = {
activitiesCount: undefined, activitiesCount: undefined,
assetClass: undefined, assetClass: undefined,
assetSubClass: undefined, assetSubClass: undefined,

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

@ -121,11 +121,11 @@ export abstract class PortfolioCalculator {
this.activities = activities this.activities = activities
.map( .map(
({ ({
assetProfile,
date, date,
feeInAssetProfileCurrency, feeInAssetProfileCurrency,
feeInBaseCurrency, feeInBaseCurrency,
quantity, quantity,
SymbolProfile,
tags = [], tags = [],
type, type,
unitPriceInAssetProfileCurrency unitPriceInAssetProfileCurrency
@ -141,9 +141,9 @@ export abstract class PortfolioCalculator {
} }
return { return {
assetProfile,
tags, tags,
type, type,
assetProfile: SymbolProfile,
date: format(date, DATE_FORMAT), date: format(date, DATE_FORMAT),
fee: new Big(feeInAssetProfileCurrency), fee: new Big(feeInAssetProfileCurrency),
feeInBaseCurrency: new Big(feeInBaseCurrency), feeInBaseCurrency: new Big(feeInBaseCurrency),

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

@ -1,6 +1,6 @@
import { import {
activityDummyData, activityDummyData,
symbolProfileDummyData, assetProfileDummyData,
userDummyData userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
@ -85,33 +85,33 @@ describe('PortfolioCalculator', () => {
const activities: Activity[] = [ const activities: Activity[] = [
{ {
...activityDummyData, ...activityDummyData,
date: new Date('2021-11-22'), assetProfile: {
feeInAssetProfileCurrency: 1.55, ...assetProfileDummyData,
feeInBaseCurrency: 1.55,
quantity: 2,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'CHF', currency: 'CHF',
dataSource: 'YAHOO', dataSource: 'YAHOO',
name: 'Bâloise Holding AG', name: 'Bâloise Holding AG',
symbol: 'BALN.SW' symbol: 'BALN.SW'
}, },
date: new Date('2021-11-22'),
feeInAssetProfileCurrency: 1.55,
feeInBaseCurrency: 1.55,
quantity: 2,
type: 'BUY', type: 'BUY',
unitPriceInAssetProfileCurrency: 142.9 unitPriceInAssetProfileCurrency: 142.9
}, },
{ {
...activityDummyData, ...activityDummyData,
date: new Date('2021-11-30'), assetProfile: {
feeInAssetProfileCurrency: 1.65, ...assetProfileDummyData,
feeInBaseCurrency: 1.65,
quantity: 2,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'CHF', currency: 'CHF',
dataSource: 'YAHOO', dataSource: 'YAHOO',
name: 'Bâloise Holding AG', name: 'Bâloise Holding AG',
symbol: 'BALN.SW' symbol: 'BALN.SW'
}, },
date: new Date('2021-11-30'),
feeInAssetProfileCurrency: 1.65,
feeInBaseCurrency: 1.65,
quantity: 2,
type: 'BUY', type: 'BUY',
unitPriceInAssetProfileCurrency: 136.6 unitPriceInAssetProfileCurrency: 136.6
} }

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

@ -1,231 +1,231 @@
import { import {
activityDummyData, activityDummyData,
symbolProfileDummyData, assetProfileDummyData,
userDummyData userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock'; import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock'; import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service'; import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service';
import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock'; import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock';
import { parseDate } from '@ghostfolio/common/helper'; import { parseDate } from '@ghostfolio/common/helper';
import { Activity } from '@ghostfolio/common/interfaces'; import { Activity } from '@ghostfolio/common/interfaces';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
import { Big } from 'big.js'; import { Big } from 'big.js';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
return { return {
CurrentRateService: jest.fn().mockImplementation(() => { CurrentRateService: jest.fn().mockImplementation(() => {
return CurrentRateServiceMock; return CurrentRateServiceMock;
}) })
}; };
}); });
jest.mock( jest.mock(
'@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service', '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service',
() => { () => {
return { return {
PortfolioSnapshotService: jest.fn().mockImplementation(() => { PortfolioSnapshotService: jest.fn().mockImplementation(() => {
return PortfolioSnapshotServiceMock; return PortfolioSnapshotServiceMock;
}) })
}; };
} }
); );
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return { return {
RedisCacheService: jest.fn().mockImplementation(() => { RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock; return RedisCacheServiceMock;
}) })
}; };
}); });
describe('PortfolioCalculator', () => { describe('PortfolioCalculator', () => {
let configurationService: ConfigurationService; let configurationService: ConfigurationService;
let currentRateService: CurrentRateService; let currentRateService: CurrentRateService;
let exchangeRateDataService: ExchangeRateDataService; let exchangeRateDataService: ExchangeRateDataService;
let portfolioCalculatorFactory: PortfolioCalculatorFactory; let portfolioCalculatorFactory: PortfolioCalculatorFactory;
let portfolioSnapshotService: PortfolioSnapshotService; let portfolioSnapshotService: PortfolioSnapshotService;
let redisCacheService: RedisCacheService; let redisCacheService: RedisCacheService;
beforeEach(() => { beforeEach(() => {
configurationService = new ConfigurationService(); configurationService = new ConfigurationService();
currentRateService = new CurrentRateService(null, null, null, null); currentRateService = new CurrentRateService(null, null, null, null);
exchangeRateDataService = new ExchangeRateDataService( exchangeRateDataService = new ExchangeRateDataService(
null, null,
null, null,
null, null,
null null
); );
portfolioSnapshotService = new PortfolioSnapshotService(null, null); portfolioSnapshotService = new PortfolioSnapshotService(null, null);
redisCacheService = new RedisCacheService(null, null); redisCacheService = new RedisCacheService(null, null);
portfolioCalculatorFactory = new PortfolioCalculatorFactory( portfolioCalculatorFactory = new PortfolioCalculatorFactory(
configurationService, configurationService,
currentRateService, currentRateService,
exchangeRateDataService, exchangeRateDataService,
portfolioSnapshotService, portfolioSnapshotService,
redisCacheService redisCacheService
); );
}); });
describe('get current positions', () => { describe('get current positions', () => {
it.only('with BALN.SW buy and sell in two activities', async () => { it.only('with BALN.SW buy and sell in two activities', async () => {
jest.useFakeTimers().setSystemTime(parseDate('2021-12-18').getTime()); jest.useFakeTimers().setSystemTime(parseDate('2021-12-18').getTime());
const activities: Activity[] = [ const activities: Activity[] = [
{ {
...activityDummyData, ...activityDummyData,
date: new Date('2021-11-22'), assetProfile: {
feeInAssetProfileCurrency: 1.55, ...assetProfileDummyData,
feeInBaseCurrency: 1.55, currency: 'CHF',
quantity: 2, dataSource: 'YAHOO',
SymbolProfile: { name: 'Bâloise Holding AG',
...symbolProfileDummyData, symbol: 'BALN.SW'
currency: 'CHF', },
dataSource: 'YAHOO', date: new Date('2021-11-22'),
name: 'Bâloise Holding AG', feeInAssetProfileCurrency: 1.55,
symbol: 'BALN.SW' feeInBaseCurrency: 1.55,
}, quantity: 2,
type: 'BUY', type: 'BUY',
unitPriceInAssetProfileCurrency: 142.9 unitPriceInAssetProfileCurrency: 142.9
}, },
{ {
...activityDummyData, ...activityDummyData,
date: new Date('2021-11-30'), assetProfile: {
feeInAssetProfileCurrency: 1.65, ...assetProfileDummyData,
feeInBaseCurrency: 1.65, currency: 'CHF',
quantity: 1, dataSource: 'YAHOO',
SymbolProfile: { name: 'Bâloise Holding AG',
...symbolProfileDummyData, symbol: 'BALN.SW'
currency: 'CHF', },
dataSource: 'YAHOO', date: new Date('2021-11-30'),
name: 'Bâloise Holding AG', feeInAssetProfileCurrency: 1.65,
symbol: 'BALN.SW' feeInBaseCurrency: 1.65,
}, quantity: 1,
type: 'SELL', type: 'SELL',
unitPriceInAssetProfileCurrency: 136.6 unitPriceInAssetProfileCurrency: 136.6
}, },
{ {
...activityDummyData, ...activityDummyData,
date: new Date('2021-11-30'), assetProfile: {
feeInAssetProfileCurrency: 0, ...assetProfileDummyData,
feeInBaseCurrency: 0, currency: 'CHF',
quantity: 1, dataSource: 'YAHOO',
SymbolProfile: { name: 'Bâloise Holding AG',
...symbolProfileDummyData, symbol: 'BALN.SW'
currency: 'CHF', },
dataSource: 'YAHOO', date: new Date('2021-11-30'),
name: 'Bâloise Holding AG', feeInAssetProfileCurrency: 0,
symbol: 'BALN.SW' feeInBaseCurrency: 0,
}, quantity: 1,
type: 'SELL', type: 'SELL',
unitPriceInAssetProfileCurrency: 136.6 unitPriceInAssetProfileCurrency: 136.6
} }
]; ];
const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ const portfolioCalculator = portfolioCalculatorFactory.createCalculator({
activities, activities,
calculationType: PerformanceCalculationType.ROAI, calculationType: PerformanceCalculationType.ROAI,
currency: 'CHF', currency: 'CHF',
userId: userDummyData.id userId: userDummyData.id
}); });
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(); const portfolioSnapshot = await portfolioCalculator.computeSnapshot();
const investments = portfolioCalculator.getInvestments(); const investments = portfolioCalculator.getInvestments();
const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({ const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({
data: portfolioSnapshot.historicalData, data: portfolioSnapshot.historicalData,
groupBy: 'month' groupBy: 'month'
}); });
const investmentsByYear = portfolioCalculator.getInvestmentsByGroup({ const investmentsByYear = portfolioCalculator.getInvestmentsByGroup({
data: portfolioSnapshot.historicalData, data: portfolioSnapshot.historicalData,
groupBy: 'year' groupBy: 'year'
}); });
expect(portfolioSnapshot).toMatchObject({ expect(portfolioSnapshot).toMatchObject({
currentValueInBaseCurrency: new Big('0'), currentValueInBaseCurrency: new Big('0'),
errors: [], errors: [],
hasErrors: false, hasErrors: false,
positions: [ positions: [
{ {
activitiesCount: 3, activitiesCount: 3,
averagePrice: new Big('0'), averagePrice: new Big('0'),
currency: 'CHF', currency: 'CHF',
dataSource: 'YAHOO', dataSource: 'YAHOO',
dateOfFirstActivity: '2021-11-22', dateOfFirstActivity: '2021-11-22',
dividend: new Big('0'), dividend: new Big('0'),
dividendInBaseCurrency: new Big('0'), dividendInBaseCurrency: new Big('0'),
fee: new Big('3.2'), fee: new Big('3.2'),
feeInBaseCurrency: new Big('3.2'), feeInBaseCurrency: new Big('3.2'),
grossPerformance: new Big('-12.6'), grossPerformance: new Big('-12.6'),
grossPerformancePercentage: new Big('-0.04408677396780965649'), grossPerformancePercentage: new Big('-0.04408677396780965649'),
grossPerformancePercentageWithCurrencyEffect: new Big( grossPerformancePercentageWithCurrencyEffect: new Big(
'-0.04408677396780965649' '-0.04408677396780965649'
), ),
grossPerformanceWithCurrencyEffect: new Big('-12.6'), grossPerformanceWithCurrencyEffect: new Big('-12.6'),
investment: new Big('0'), investment: new Big('0'),
investmentWithCurrencyEffect: new Big('0'), investmentWithCurrencyEffect: new Big('0'),
netPerformancePercentageWithCurrencyEffectMap: { netPerformancePercentageWithCurrencyEffectMap: {
max: new Big('-0.0552834149755073478') max: new Big('-0.0552834149755073478')
}, },
netPerformanceWithCurrencyEffectMap: { netPerformanceWithCurrencyEffectMap: {
max: new Big('-15.8') max: new Big('-15.8')
}, },
marketPrice: 148.9, marketPrice: 148.9,
marketPriceInBaseCurrency: 148.9, marketPriceInBaseCurrency: 148.9,
quantity: new Big('0'), quantity: new Big('0'),
symbol: 'BALN.SW', symbol: 'BALN.SW',
tags: [], tags: [],
timeWeightedInvestment: new Big('285.80000000000000396627'), timeWeightedInvestment: new Big('285.80000000000000396627'),
timeWeightedInvestmentWithCurrencyEffect: new Big( timeWeightedInvestmentWithCurrencyEffect: new Big(
'285.80000000000000396627' '285.80000000000000396627'
), ),
valueInBaseCurrency: new Big('0') valueInBaseCurrency: new Big('0')
} }
], ],
totalFeesWithCurrencyEffect: new Big('3.2'), totalFeesWithCurrencyEffect: new Big('3.2'),
totalInterestWithCurrencyEffect: new Big('0'), totalInterestWithCurrencyEffect: new Big('0'),
totalInvestment: new Big('0'), totalInvestment: new Big('0'),
totalInvestmentWithCurrencyEffect: new Big('0'), totalInvestmentWithCurrencyEffect: new Big('0'),
totalLiabilitiesWithCurrencyEffect: new Big('0') totalLiabilitiesWithCurrencyEffect: new Big('0')
}); });
expect(portfolioSnapshot.historicalData.at(-1)).toMatchObject( expect(portfolioSnapshot.historicalData.at(-1)).toMatchObject(
expect.objectContaining({ expect.objectContaining({
netPerformance: -15.8, netPerformance: -15.8,
netPerformanceInPercentage: -0.05528341497550734703, netPerformanceInPercentage: -0.05528341497550734703,
netPerformanceInPercentageWithCurrencyEffect: -0.05528341497550734703, netPerformanceInPercentageWithCurrencyEffect: -0.05528341497550734703,
netPerformanceWithCurrencyEffect: -15.8, netPerformanceWithCurrencyEffect: -15.8,
totalInvestment: 0, totalInvestment: 0,
totalInvestmentValueWithCurrencyEffect: 0 totalInvestmentValueWithCurrencyEffect: 0
}) })
); );
expect(investments).toEqual([ expect(investments).toEqual([
{ date: '2021-11-22', investment: new Big('285.8') }, { date: '2021-11-22', investment: new Big('285.8') },
{ date: '2021-11-30', investment: new Big('0') } { date: '2021-11-30', investment: new Big('0') }
]); ]);
expect(investmentsByMonth).toEqual([ expect(investmentsByMonth).toEqual([
{ date: '2021-11-01', investment: 0 }, { date: '2021-11-01', investment: 0 },
{ date: '2021-12-01', investment: 0 } { date: '2021-12-01', investment: 0 }
]); ]);
expect(investmentsByYear).toEqual([ expect(investmentsByYear).toEqual([
{ date: '2021-01-01', investment: 0 } { date: '2021-01-01', investment: 0 }
]); ]);
}); });
}); });
}); });

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

@ -1,6 +1,6 @@
import { import {
activityDummyData, activityDummyData,
symbolProfileDummyData, assetProfileDummyData,
userDummyData userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
@ -85,33 +85,33 @@ describe('PortfolioCalculator', () => {
const activities: Activity[] = [ const activities: Activity[] = [
{ {
...activityDummyData, ...activityDummyData,
date: new Date('2021-11-22'), assetProfile: {
feeInAssetProfileCurrency: 1.55, ...assetProfileDummyData,
feeInBaseCurrency: 1.55,
quantity: 2,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'CHF', currency: 'CHF',
dataSource: 'YAHOO', dataSource: 'YAHOO',
name: 'Bâloise Holding AG', name: 'Bâloise Holding AG',
symbol: 'BALN.SW' symbol: 'BALN.SW'
}, },
date: new Date('2021-11-22'),
feeInAssetProfileCurrency: 1.55,
feeInBaseCurrency: 1.55,
quantity: 2,
type: 'BUY', type: 'BUY',
unitPriceInAssetProfileCurrency: 142.9 unitPriceInAssetProfileCurrency: 142.9
}, },
{ {
...activityDummyData, ...activityDummyData,
date: new Date('2021-11-30'), assetProfile: {
feeInAssetProfileCurrency: 1.65, ...assetProfileDummyData,
feeInBaseCurrency: 1.65,
quantity: 2,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'CHF', currency: 'CHF',
dataSource: 'YAHOO', dataSource: 'YAHOO',
name: 'Bâloise Holding AG', name: 'Bâloise Holding AG',
symbol: 'BALN.SW' symbol: 'BALN.SW'
}, },
date: new Date('2021-11-30'),
feeInAssetProfileCurrency: 1.65,
feeInBaseCurrency: 1.65,
quantity: 2,
type: 'SELL', type: 'SELL',
unitPriceInAssetProfileCurrency: 136.6 unitPriceInAssetProfileCurrency: 136.6
} }

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

@ -1,6 +1,6 @@
import { import {
activityDummyData, activityDummyData,
symbolProfileDummyData, assetProfileDummyData,
userDummyData userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
@ -85,17 +85,17 @@ describe('PortfolioCalculator', () => {
const activities: Activity[] = [ const activities: Activity[] = [
{ {
...activityDummyData, ...activityDummyData,
date: new Date('2021-11-30'), assetProfile: {
feeInAssetProfileCurrency: 1.55, ...assetProfileDummyData,
feeInBaseCurrency: 1.55,
quantity: 2,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'CHF', currency: 'CHF',
dataSource: 'YAHOO', dataSource: 'YAHOO',
name: 'Bâloise Holding AG', name: 'Bâloise Holding AG',
symbol: 'BALN.SW' symbol: 'BALN.SW'
}, },
date: new Date('2021-11-30'),
feeInAssetProfileCurrency: 1.55,
feeInBaseCurrency: 1.55,
quantity: 2,
type: 'BUY', type: 'BUY',
unitPriceInAssetProfileCurrency: 136.6 unitPriceInAssetProfileCurrency: 136.6
} }
@ -217,17 +217,17 @@ describe('PortfolioCalculator', () => {
const activities: Activity[] = [ const activities: Activity[] = [
{ {
...activityDummyData, ...activityDummyData,
date: new Date('2021-11-30'), assetProfile: {
feeInAssetProfileCurrency: 1.55, ...assetProfileDummyData,
feeInBaseCurrency: 1.55,
quantity: 2,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'CHF', currency: 'CHF',
dataSource: 'YAHOO', dataSource: 'YAHOO',
name: 'Bâloise Holding AG', name: 'Bâloise Holding AG',
symbol: 'BALN.SW' symbol: 'BALN.SW'
}, },
date: new Date('2021-11-30'),
feeInAssetProfileCurrency: 1.55,
feeInBaseCurrency: 1.55,
quantity: 2,
type: 'BUY', type: 'BUY',
unitPriceInAssetProfileCurrency: 135.0 unitPriceInAssetProfileCurrency: 135.0
} }
@ -257,17 +257,17 @@ describe('PortfolioCalculator', () => {
const activities: Activity[] = [ const activities: Activity[] = [
{ {
...activityDummyData, ...activityDummyData,
date: new Date('2021-11-30'), assetProfile: {
feeInAssetProfileCurrency: 1.55, ...assetProfileDummyData,
feeInBaseCurrency: 1.55,
quantity: 2,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'CHF', currency: 'CHF',
dataSource: 'YAHOO', dataSource: 'YAHOO',
name: 'Bâloise Holding AG', name: 'Bâloise Holding AG',
symbol: 'BALN.SW' symbol: 'BALN.SW'
}, },
date: new Date('2021-11-30'),
feeInAssetProfileCurrency: 1.55,
feeInBaseCurrency: 1.55,
quantity: 2,
type: 'BUY', type: 'BUY',
unitPriceInAssetProfileCurrency: 135.0 unitPriceInAssetProfileCurrency: 135.0
} }

14
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur-in-base-currency-eur.spec.ts

@ -1,7 +1,7 @@
import { import {
activityDummyData, activityDummyData,
assetProfileDummyData,
loadExportFile, loadExportFile,
symbolProfileDummyData,
userDummyData userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
@ -107,17 +107,17 @@ describe('PortfolioCalculator', () => {
const activities: Activity[] = exportResponse.activities.map( const activities: Activity[] = exportResponse.activities.map(
(activity) => ({ (activity) => ({
...activityDummyData, ...activityDummyData,
...activity, assetProfile: {
date: parseDate(activity.date), ...assetProfileDummyData,
feeInAssetProfileCurrency: 4.46,
feeInBaseCurrency: 3.94,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'USD', currency: 'USD',
dataSource: activity.dataSource, dataSource: activity.dataSource,
name: 'Bitcoin', name: 'Bitcoin',
symbol: activity.symbol symbol: activity.symbol
}, },
...activity,
date: parseDate(activity.date),
feeInAssetProfileCurrency: 4.46,
feeInBaseCurrency: 3.94,
unitPriceInAssetProfileCurrency: 44558.42 unitPriceInAssetProfileCurrency: 44558.42
}) })
); );

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

@ -1,7 +1,7 @@
import { import {
activityDummyData, activityDummyData,
assetProfileDummyData,
loadExportFile, loadExportFile,
symbolProfileDummyData,
userDummyData userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
@ -95,17 +95,17 @@ describe('PortfolioCalculator', () => {
const activities: Activity[] = exportResponse.activities.map( const activities: Activity[] = exportResponse.activities.map(
(activity) => ({ (activity) => ({
...activityDummyData, ...activityDummyData,
...activity, assetProfile: {
date: parseDate(activity.date), ...assetProfileDummyData,
feeInAssetProfileCurrency: 4.46,
feeInBaseCurrency: 4.46,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'USD', currency: 'USD',
dataSource: activity.dataSource, dataSource: activity.dataSource,
name: 'Bitcoin', name: 'Bitcoin',
symbol: activity.symbol symbol: activity.symbol
}, },
...activity,
date: parseDate(activity.date),
feeInAssetProfileCurrency: 4.46,
feeInBaseCurrency: 4.46,
unitPriceInAssetProfileCurrency: 44558.42 unitPriceInAssetProfileCurrency: 44558.42
}) })
); );

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

@ -1,6 +1,6 @@
import { import {
activityDummyData, activityDummyData,
symbolProfileDummyData, assetProfileDummyData,
userDummyData userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
@ -98,33 +98,33 @@ describe('PortfolioCalculator', () => {
const activities: Activity[] = [ const activities: Activity[] = [
{ {
...activityDummyData, ...activityDummyData,
date: new Date('2015-01-01'), assetProfile: {
feeInAssetProfileCurrency: 0, ...assetProfileDummyData,
feeInBaseCurrency: 0,
quantity: 2,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'USD', currency: 'USD',
dataSource: 'YAHOO', dataSource: 'YAHOO',
name: 'Bitcoin USD', name: 'Bitcoin USD',
symbol: 'BTCUSD' symbol: 'BTCUSD'
}, },
date: new Date('2015-01-01'),
feeInAssetProfileCurrency: 0,
feeInBaseCurrency: 0,
quantity: 2,
type: 'BUY', type: 'BUY',
unitPriceInAssetProfileCurrency: 320.43 unitPriceInAssetProfileCurrency: 320.43
}, },
{ {
...activityDummyData, ...activityDummyData,
date: new Date('2017-12-31'), assetProfile: {
feeInAssetProfileCurrency: 0, ...assetProfileDummyData,
feeInBaseCurrency: 0,
quantity: 1,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'USD', currency: 'USD',
dataSource: 'YAHOO', dataSource: 'YAHOO',
name: 'Bitcoin USD', name: 'Bitcoin USD',
symbol: 'BTCUSD' symbol: 'BTCUSD'
}, },
date: new Date('2017-12-31'),
feeInAssetProfileCurrency: 0,
feeInBaseCurrency: 0,
quantity: 1,
type: 'SELL', type: 'SELL',
unitPriceInAssetProfileCurrency: 14156.4 unitPriceInAssetProfileCurrency: 14156.4
} }

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

@ -1,7 +1,7 @@
import { import {
activityDummyData, activityDummyData,
assetProfileDummyData,
loadExportFile, loadExportFile,
symbolProfileDummyData,
userDummyData userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
@ -96,16 +96,16 @@ describe('PortfolioCalculator', () => {
(activity) => ({ (activity) => ({
...activityDummyData, ...activityDummyData,
...activity, ...activity,
date: parseDate(activity.date), assetProfile: {
feeInAssetProfileCurrency: activity.fee, ...assetProfileDummyData,
feeInBaseCurrency: activity.fee,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'USD', currency: 'USD',
dataSource: activity.dataSource, dataSource: activity.dataSource,
name: 'Bitcoin', name: 'Bitcoin',
symbol: activity.symbol symbol: activity.symbol
}, },
date: parseDate(activity.date),
feeInAssetProfileCurrency: activity.fee,
feeInBaseCurrency: activity.fee,
unitPriceInAssetProfileCurrency: activity.unitPrice unitPriceInAssetProfileCurrency: activity.unitPrice
}) })
); );

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

@ -1,7 +1,7 @@
import { import {
activityDummyData, activityDummyData,
assetProfileDummyData,
loadExportFile, loadExportFile,
symbolProfileDummyData,
userDummyData userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
@ -96,16 +96,16 @@ describe('PortfolioCalculator', () => {
(activity) => ({ (activity) => ({
...activityDummyData, ...activityDummyData,
...activity, ...activity,
date: parseDate(activity.date), assetProfile: {
feeInAssetProfileCurrency: 4.46, ...assetProfileDummyData,
feeInBaseCurrency: 4.46,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'USD', currency: 'USD',
dataSource: activity.dataSource, dataSource: activity.dataSource,
name: 'Bitcoin', name: 'Bitcoin',
symbol: activity.symbol symbol: activity.symbol
}, },
date: parseDate(activity.date),
feeInAssetProfileCurrency: 4.46,
feeInBaseCurrency: 4.46,
unitPriceInAssetProfileCurrency: 44558.42 unitPriceInAssetProfileCurrency: 44558.42
}) })
); );

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

@ -1,6 +1,6 @@
import { import {
activityDummyData, activityDummyData,
symbolProfileDummyData, assetProfileDummyData,
userDummyData userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
@ -85,17 +85,17 @@ describe('PortfolioCalculator', () => {
const activities: Activity[] = [ const activities: Activity[] = [
{ {
...activityDummyData, ...activityDummyData,
date: new Date('2021-09-01'), assetProfile: {
feeInAssetProfileCurrency: 49, ...assetProfileDummyData,
feeInBaseCurrency: 49,
quantity: 0,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'USD', currency: 'USD',
dataSource: 'MANUAL', dataSource: 'MANUAL',
name: 'Account Opening Fee', name: 'Account Opening Fee',
symbol: '2c463fb3-af07-486e-adb0-8301b3d72141' symbol: '2c463fb3-af07-486e-adb0-8301b3d72141'
}, },
date: new Date('2021-09-01'),
feeInAssetProfileCurrency: 49,
feeInBaseCurrency: 49,
quantity: 0,
type: 'FEE', type: 'FEE',
unitPriceInAssetProfileCurrency: 0 unitPriceInAssetProfileCurrency: 0
} }

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

@ -1,6 +1,6 @@
import { import {
activityDummyData, activityDummyData,
symbolProfileDummyData, assetProfileDummyData,
userDummyData userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
@ -97,17 +97,17 @@ describe('PortfolioCalculator', () => {
const activities: Activity[] = [ const activities: Activity[] = [
{ {
...activityDummyData, ...activityDummyData,
date: new Date('2023-01-03'), assetProfile: {
feeInAssetProfileCurrency: 1, ...assetProfileDummyData,
feeInBaseCurrency: 0.9238,
quantity: 1,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'USD', currency: 'USD',
dataSource: 'YAHOO', dataSource: 'YAHOO',
name: 'Alphabet Inc.', name: 'Alphabet Inc.',
symbol: 'GOOGL' symbol: 'GOOGL'
}, },
date: new Date('2023-01-03'),
feeInAssetProfileCurrency: 1,
feeInBaseCurrency: 0.9238,
quantity: 1,
type: 'BUY', type: 'BUY',
unitPriceInAssetProfileCurrency: 89.12 unitPriceInAssetProfileCurrency: 89.12
} }

380
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-jnug-buy-and-sell-and-buy-and-sell.spec.ts

@ -1,190 +1,190 @@
import { import {
activityDummyData, activityDummyData,
loadExportFile, assetProfileDummyData,
symbolProfileDummyData, loadExportFile,
userDummyData userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock'; import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock'; import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service'; import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service';
import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock'; import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock';
import { parseDate } from '@ghostfolio/common/helper'; import { parseDate } from '@ghostfolio/common/helper';
import { Activity, ExportResponse } from '@ghostfolio/common/interfaces'; import { Activity, ExportResponse } from '@ghostfolio/common/interfaces';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
import { Big } from 'big.js'; import { Big } from 'big.js';
import { join } from 'node:path'; import { join } from 'node:path';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
return { return {
CurrentRateService: jest.fn().mockImplementation(() => { CurrentRateService: jest.fn().mockImplementation(() => {
return CurrentRateServiceMock; return CurrentRateServiceMock;
}) })
}; };
}); });
jest.mock( jest.mock(
'@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service', '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service',
() => { () => {
return { return {
PortfolioSnapshotService: jest.fn().mockImplementation(() => { PortfolioSnapshotService: jest.fn().mockImplementation(() => {
return PortfolioSnapshotServiceMock; return PortfolioSnapshotServiceMock;
}) })
}; };
} }
); );
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return { return {
RedisCacheService: jest.fn().mockImplementation(() => { RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock; return RedisCacheServiceMock;
}) })
}; };
}); });
describe('PortfolioCalculator', () => { describe('PortfolioCalculator', () => {
let exportResponse: ExportResponse; let exportResponse: ExportResponse;
let configurationService: ConfigurationService; let configurationService: ConfigurationService;
let currentRateService: CurrentRateService; let currentRateService: CurrentRateService;
let exchangeRateDataService: ExchangeRateDataService; let exchangeRateDataService: ExchangeRateDataService;
let portfolioCalculatorFactory: PortfolioCalculatorFactory; let portfolioCalculatorFactory: PortfolioCalculatorFactory;
let portfolioSnapshotService: PortfolioSnapshotService; let portfolioSnapshotService: PortfolioSnapshotService;
let redisCacheService: RedisCacheService; let redisCacheService: RedisCacheService;
beforeAll(() => { beforeAll(() => {
exportResponse = loadExportFile( exportResponse = loadExportFile(
join( join(
__dirname, __dirname,
'../../../../../../../test/import/ok/jnug-buy-and-sell-and-buy-and-sell.json' '../../../../../../../test/import/ok/jnug-buy-and-sell-and-buy-and-sell.json'
) )
); );
}); });
beforeEach(() => { beforeEach(() => {
configurationService = new ConfigurationService(); configurationService = new ConfigurationService();
currentRateService = new CurrentRateService(null, null, null, null); currentRateService = new CurrentRateService(null, null, null, null);
exchangeRateDataService = new ExchangeRateDataService( exchangeRateDataService = new ExchangeRateDataService(
null, null,
null, null,
null, null,
null null
); );
portfolioSnapshotService = new PortfolioSnapshotService(null, null); portfolioSnapshotService = new PortfolioSnapshotService(null, null);
redisCacheService = new RedisCacheService(null, null); redisCacheService = new RedisCacheService(null, null);
portfolioCalculatorFactory = new PortfolioCalculatorFactory( portfolioCalculatorFactory = new PortfolioCalculatorFactory(
configurationService, configurationService,
currentRateService, currentRateService,
exchangeRateDataService, exchangeRateDataService,
portfolioSnapshotService, portfolioSnapshotService,
redisCacheService redisCacheService
); );
}); });
describe('get current positions', () => { describe('get current positions', () => {
it.only('with JNUG buy and sell', async () => { it.only('with JNUG buy and sell', async () => {
jest.useFakeTimers().setSystemTime(parseDate('2025-12-28').getTime()); jest.useFakeTimers().setSystemTime(parseDate('2025-12-28').getTime());
const activities: Activity[] = exportResponse.activities.map( const activities: Activity[] = exportResponse.activities.map(
(activity) => ({ (activity) => ({
...activityDummyData, ...activityDummyData,
...activity, ...activity,
date: parseDate(activity.date), assetProfile: {
feeInAssetProfileCurrency: activity.fee, ...assetProfileDummyData,
feeInBaseCurrency: activity.fee, currency: activity.currency,
SymbolProfile: { dataSource: activity.dataSource,
...symbolProfileDummyData, name: 'Direxion Daily Junior Gold Miners Index Bull 2X Shares',
currency: activity.currency, symbol: activity.symbol
dataSource: activity.dataSource, },
name: 'Direxion Daily Junior Gold Miners Index Bull 2X Shares', date: parseDate(activity.date),
symbol: activity.symbol feeInAssetProfileCurrency: activity.fee,
}, feeInBaseCurrency: activity.fee,
unitPriceInAssetProfileCurrency: activity.unitPrice unitPriceInAssetProfileCurrency: activity.unitPrice
}) })
); );
const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ const portfolioCalculator = portfolioCalculatorFactory.createCalculator({
activities, activities,
calculationType: PerformanceCalculationType.ROAI, calculationType: PerformanceCalculationType.ROAI,
currency: exportResponse.user.settings.currency, currency: exportResponse.user.settings.currency,
userId: userDummyData.id userId: userDummyData.id
}); });
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(); const portfolioSnapshot = await portfolioCalculator.computeSnapshot();
const investments = portfolioCalculator.getInvestments(); const investments = portfolioCalculator.getInvestments();
const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({ const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({
data: portfolioSnapshot.historicalData, data: portfolioSnapshot.historicalData,
groupBy: 'month' groupBy: 'month'
}); });
const investmentsByYear = portfolioCalculator.getInvestmentsByGroup({ const investmentsByYear = portfolioCalculator.getInvestmentsByGroup({
data: portfolioSnapshot.historicalData, data: portfolioSnapshot.historicalData,
groupBy: 'year' groupBy: 'year'
}); });
expect(portfolioSnapshot).toMatchObject({ expect(portfolioSnapshot).toMatchObject({
currentValueInBaseCurrency: new Big('0'), currentValueInBaseCurrency: new Big('0'),
errors: [], errors: [],
hasErrors: false, hasErrors: false,
positions: [ positions: [
{ {
activitiesCount: 4, activitiesCount: 4,
averagePrice: new Big('0'), averagePrice: new Big('0'),
currency: 'USD', currency: 'USD',
dataSource: 'YAHOO', dataSource: 'YAHOO',
dateOfFirstActivity: '2025-12-11', dateOfFirstActivity: '2025-12-11',
dividend: new Big('0'), dividend: new Big('0'),
dividendInBaseCurrency: new Big('0'), dividendInBaseCurrency: new Big('0'),
fee: new Big('4'), fee: new Big('4'),
feeInBaseCurrency: new Big('4'), feeInBaseCurrency: new Big('4'),
grossPerformance: new Big('43.95'), // (1890.00 - 1885.05) + (2080.10 - 2041.10) grossPerformance: new Big('43.95'), // (1890.00 - 1885.05) + (2080.10 - 2041.10)
grossPerformanceWithCurrencyEffect: new Big('43.95'), // (1890.00 - 1885.05) + (2080.10 - 2041.10) grossPerformanceWithCurrencyEffect: new Big('43.95'), // (1890.00 - 1885.05) + (2080.10 - 2041.10)
investment: new Big('0'), investment: new Big('0'),
investmentWithCurrencyEffect: new Big('0'), investmentWithCurrencyEffect: new Big('0'),
netPerformance: new Big('39.95'), // (1890.00 - 1885.05) + (2080.10 - 2041.10) - 4 netPerformance: new Big('39.95'), // (1890.00 - 1885.05) + (2080.10 - 2041.10) - 4
netPerformanceWithCurrencyEffectMap: { netPerformanceWithCurrencyEffectMap: {
max: new Big('39.95') // (1890.00 - 1885.05) + (2080.10 - 2041.10) - 4 max: new Big('39.95') // (1890.00 - 1885.05) + (2080.10 - 2041.10) - 4
}, },
marketPrice: 237.8000030517578, marketPrice: 237.8000030517578,
marketPriceInBaseCurrency: 237.8000030517578, marketPriceInBaseCurrency: 237.8000030517578,
quantity: new Big('0'), quantity: new Big('0'),
symbol: 'JNUG', symbol: 'JNUG',
tags: [], tags: [],
valueInBaseCurrency: new Big('0') valueInBaseCurrency: new Big('0')
} }
], ],
totalFeesWithCurrencyEffect: new Big('4'), totalFeesWithCurrencyEffect: new Big('4'),
totalInterestWithCurrencyEffect: new Big('0'), totalInterestWithCurrencyEffect: new Big('0'),
totalInvestment: new Big('0'), totalInvestment: new Big('0'),
totalInvestmentWithCurrencyEffect: new Big('0'), totalInvestmentWithCurrencyEffect: new Big('0'),
totalLiabilitiesWithCurrencyEffect: new Big('0') totalLiabilitiesWithCurrencyEffect: new Big('0')
}); });
expect(investments).toEqual([ expect(investments).toEqual([
{ date: '2025-12-11', investment: new Big('1885.05') }, { date: '2025-12-11', investment: new Big('1885.05') },
{ date: '2025-12-18', investment: new Big('2041.1') }, { date: '2025-12-18', investment: new Big('2041.1') },
{ date: '2025-12-28', investment: new Big('0') } { date: '2025-12-28', investment: new Big('0') }
]); ]);
expect(investmentsByMonth).toEqual([ expect(investmentsByMonth).toEqual([
{ date: '2025-12-01', investment: 0 } { date: '2025-12-01', investment: 0 }
]); ]);
expect(investmentsByYear).toEqual([ expect(investmentsByYear).toEqual([
{ date: '2025-01-01', investment: 0 } { date: '2025-01-01', investment: 0 }
]); ]);
}); });
}); });
}); });

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

@ -1,6 +1,6 @@
import { import {
activityDummyData, activityDummyData,
symbolProfileDummyData, assetProfileDummyData,
userDummyData userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
@ -85,17 +85,17 @@ describe('PortfolioCalculator', () => {
const activities: Activity[] = [ const activities: Activity[] = [
{ {
...activityDummyData, ...activityDummyData,
date: new Date('2023-01-01'), // Date in future assetProfile: {
feeInAssetProfileCurrency: 0, ...assetProfileDummyData,
feeInBaseCurrency: 0,
quantity: 1,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'USD', currency: 'USD',
dataSource: 'MANUAL', dataSource: 'MANUAL',
name: 'Loan', name: 'Loan',
symbol: '55196015-1365-4560-aa60-8751ae6d18f8' symbol: '55196015-1365-4560-aa60-8751ae6d18f8'
}, },
date: new Date('2023-01-01'), // Date in future
feeInAssetProfileCurrency: 0,
feeInBaseCurrency: 0,
quantity: 1,
type: 'LIABILITY', type: 'LIABILITY',
unitPriceInAssetProfileCurrency: 3000 unitPriceInAssetProfileCurrency: 3000
} }

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

@ -1,6 +1,6 @@
import { import {
activityDummyData, activityDummyData,
symbolProfileDummyData, assetProfileDummyData,
userDummyData userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
@ -78,49 +78,49 @@ describe('PortfolioCalculator', () => {
const activities: Activity[] = [ const activities: Activity[] = [
{ {
...activityDummyData, ...activityDummyData,
date: new Date('2024-03-08'), assetProfile: {
feeInAssetProfileCurrency: 0, ...assetProfileDummyData,
feeInBaseCurrency: 0,
quantity: 0.3333333333333333,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'USD', currency: 'USD',
dataSource: 'YAHOO', dataSource: 'YAHOO',
name: 'Microsoft Inc.', name: 'Microsoft Inc.',
symbol: 'MSFT' symbol: 'MSFT'
}, },
date: new Date('2024-03-08'),
feeInAssetProfileCurrency: 0,
feeInBaseCurrency: 0,
quantity: 0.3333333333333333,
type: 'BUY', type: 'BUY',
unitPriceInAssetProfileCurrency: 408 unitPriceInAssetProfileCurrency: 408
}, },
{ {
...activityDummyData, ...activityDummyData,
date: new Date('2024-03-13'), assetProfile: {
feeInAssetProfileCurrency: 0, ...assetProfileDummyData,
feeInBaseCurrency: 0,
quantity: 0.6666666666666666,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'USD', currency: 'USD',
dataSource: 'YAHOO', dataSource: 'YAHOO',
name: 'Microsoft Inc.', name: 'Microsoft Inc.',
symbol: 'MSFT' symbol: 'MSFT'
}, },
date: new Date('2024-03-13'),
feeInAssetProfileCurrency: 0,
feeInBaseCurrency: 0,
quantity: 0.6666666666666666,
type: 'BUY', type: 'BUY',
unitPriceInAssetProfileCurrency: 400 unitPriceInAssetProfileCurrency: 400
}, },
{ {
...activityDummyData, ...activityDummyData,
date: new Date('2024-03-14'), assetProfile: {
feeInAssetProfileCurrency: 0, ...assetProfileDummyData,
feeInBaseCurrency: 0,
quantity: 1,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'USD', currency: 'USD',
dataSource: 'YAHOO', dataSource: 'YAHOO',
name: 'Microsoft Inc.', name: 'Microsoft Inc.',
symbol: 'MSFT' symbol: 'MSFT'
}, },
date: new Date('2024-03-14'),
feeInAssetProfileCurrency: 0,
feeInBaseCurrency: 0,
quantity: 1,
type: 'SELL', type: 'SELL',
unitPriceInAssetProfileCurrency: 411 unitPriceInAssetProfileCurrency: 411
} }

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

@ -1,6 +1,6 @@
import { import {
activityDummyData, activityDummyData,
symbolProfileDummyData, assetProfileDummyData,
userDummyData userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
@ -85,33 +85,33 @@ describe('PortfolioCalculator', () => {
const activities: Activity[] = [ const activities: Activity[] = [
{ {
...activityDummyData, ...activityDummyData,
date: new Date('2021-09-16'), assetProfile: {
feeInAssetProfileCurrency: 19, ...assetProfileDummyData,
feeInBaseCurrency: 19,
quantity: 1,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'USD', currency: 'USD',
dataSource: 'YAHOO', dataSource: 'YAHOO',
name: 'Microsoft Inc.', name: 'Microsoft Inc.',
symbol: 'MSFT' symbol: 'MSFT'
}, },
date: new Date('2021-09-16'),
feeInAssetProfileCurrency: 19,
feeInBaseCurrency: 19,
quantity: 1,
type: 'BUY', type: 'BUY',
unitPriceInAssetProfileCurrency: 298.58 unitPriceInAssetProfileCurrency: 298.58
}, },
{ {
...activityDummyData, ...activityDummyData,
date: new Date('2021-11-16'), assetProfile: {
feeInAssetProfileCurrency: 0, ...assetProfileDummyData,
feeInBaseCurrency: 0,
quantity: 1,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'USD', currency: 'USD',
dataSource: 'YAHOO', dataSource: 'YAHOO',
name: 'Microsoft Inc.', name: 'Microsoft Inc.',
symbol: 'MSFT' symbol: 'MSFT'
}, },
date: new Date('2021-11-16'),
feeInAssetProfileCurrency: 0,
feeInBaseCurrency: 0,
quantity: 1,
type: 'DIVIDEND', type: 'DIVIDEND',
unitPriceInAssetProfileCurrency: 0.62 unitPriceInAssetProfileCurrency: 0.62
} }

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

@ -1,7 +1,7 @@
import { import {
activityDummyData, activityDummyData,
assetProfileDummyData,
loadExportFile, loadExportFile,
symbolProfileDummyData,
userDummyData userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
@ -99,16 +99,16 @@ describe('PortfolioCalculator', () => {
(activity) => ({ (activity) => ({
...activityDummyData, ...activityDummyData,
...activity, ...activity,
date: parseDate(activity.date), assetProfile: {
feeInAssetProfileCurrency: activity.fee, ...assetProfileDummyData,
feeInBaseCurrency: activity.fee,
SymbolProfile: {
...symbolProfileDummyData,
currency: activity.currency, currency: activity.currency,
dataSource: activity.dataSource, dataSource: activity.dataSource,
name: 'Novartis AG', name: 'Novartis AG',
symbol: activity.symbol symbol: activity.symbol
}, },
date: parseDate(activity.date),
feeInAssetProfileCurrency: activity.fee,
feeInBaseCurrency: activity.fee,
unitPriceInAssetProfileCurrency: activity.unitPrice unitPriceInAssetProfileCurrency: activity.unitPrice
}) })
); );

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

@ -1,264 +1,264 @@
import { import {
activityDummyData, activityDummyData,
loadExportFile, assetProfileDummyData,
symbolProfileDummyData, loadExportFile,
userDummyData userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock'; import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock'; import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service'; import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service';
import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock'; import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock';
import { parseDate } from '@ghostfolio/common/helper'; import { parseDate } from '@ghostfolio/common/helper';
import { Activity, ExportResponse } from '@ghostfolio/common/interfaces'; import { Activity, ExportResponse } from '@ghostfolio/common/interfaces';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
import { Big } from 'big.js'; import { Big } from 'big.js';
import { join } from 'node:path'; import { join } from 'node:path';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
return { return {
CurrentRateService: jest.fn().mockImplementation(() => { CurrentRateService: jest.fn().mockImplementation(() => {
return CurrentRateServiceMock; return CurrentRateServiceMock;
}) })
}; };
}); });
jest.mock( jest.mock(
'@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service', '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service',
() => { () => {
return { return {
PortfolioSnapshotService: jest.fn().mockImplementation(() => { PortfolioSnapshotService: jest.fn().mockImplementation(() => {
return PortfolioSnapshotServiceMock; return PortfolioSnapshotServiceMock;
}) })
}; };
} }
); );
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return { return {
RedisCacheService: jest.fn().mockImplementation(() => { RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock; return RedisCacheServiceMock;
}) })
}; };
}); });
describe('PortfolioCalculator', () => { describe('PortfolioCalculator', () => {
let exportResponse: ExportResponse; let exportResponse: ExportResponse;
let configurationService: ConfigurationService; let configurationService: ConfigurationService;
let currentRateService: CurrentRateService; let currentRateService: CurrentRateService;
let exchangeRateDataService: ExchangeRateDataService; let exchangeRateDataService: ExchangeRateDataService;
let portfolioCalculatorFactory: PortfolioCalculatorFactory; let portfolioCalculatorFactory: PortfolioCalculatorFactory;
let portfolioSnapshotService: PortfolioSnapshotService; let portfolioSnapshotService: PortfolioSnapshotService;
let redisCacheService: RedisCacheService; let redisCacheService: RedisCacheService;
beforeAll(() => { beforeAll(() => {
exportResponse = loadExportFile( exportResponse = loadExportFile(
join( join(
__dirname, __dirname,
'../../../../../../../test/import/ok/novn-buy-and-sell.json' '../../../../../../../test/import/ok/novn-buy-and-sell.json'
) )
); );
}); });
beforeEach(() => { beforeEach(() => {
configurationService = new ConfigurationService(); configurationService = new ConfigurationService();
currentRateService = new CurrentRateService(null, null, null, null); currentRateService = new CurrentRateService(null, null, null, null);
exchangeRateDataService = new ExchangeRateDataService( exchangeRateDataService = new ExchangeRateDataService(
null, null,
null, null,
null, null,
null null
); );
portfolioSnapshotService = new PortfolioSnapshotService(null, null); portfolioSnapshotService = new PortfolioSnapshotService(null, null);
redisCacheService = new RedisCacheService(null, null); redisCacheService = new RedisCacheService(null, null);
portfolioCalculatorFactory = new PortfolioCalculatorFactory( portfolioCalculatorFactory = new PortfolioCalculatorFactory(
configurationService, configurationService,
currentRateService, currentRateService,
exchangeRateDataService, exchangeRateDataService,
portfolioSnapshotService, portfolioSnapshotService,
redisCacheService redisCacheService
); );
}); });
describe('get current positions', () => { describe('get current positions', () => {
it.only('with NOVN.SW buy and sell', async () => { it.only('with NOVN.SW buy and sell', async () => {
jest.useFakeTimers().setSystemTime(parseDate('2022-04-11').getTime()); jest.useFakeTimers().setSystemTime(parseDate('2022-04-11').getTime());
const activities: Activity[] = exportResponse.activities.map( const activities: Activity[] = exportResponse.activities.map(
(activity) => ({ (activity) => ({
...activityDummyData, ...activityDummyData,
...activity, ...activity,
date: parseDate(activity.date), assetProfile: {
feeInAssetProfileCurrency: activity.fee, ...assetProfileDummyData,
feeInBaseCurrency: activity.fee, currency: activity.currency,
SymbolProfile: { dataSource: activity.dataSource,
...symbolProfileDummyData, name: 'Novartis AG',
currency: activity.currency, symbol: activity.symbol
dataSource: activity.dataSource, },
name: 'Novartis AG', date: parseDate(activity.date),
symbol: activity.symbol feeInAssetProfileCurrency: activity.fee,
}, feeInBaseCurrency: activity.fee,
unitPriceInAssetProfileCurrency: activity.unitPrice unitPriceInAssetProfileCurrency: activity.unitPrice
}) })
); );
const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ const portfolioCalculator = portfolioCalculatorFactory.createCalculator({
activities, activities,
calculationType: PerformanceCalculationType.ROAI, calculationType: PerformanceCalculationType.ROAI,
currency: exportResponse.user.settings.currency, currency: exportResponse.user.settings.currency,
userId: userDummyData.id userId: userDummyData.id
}); });
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(); const portfolioSnapshot = await portfolioCalculator.computeSnapshot();
const investments = portfolioCalculator.getInvestments(); const investments = portfolioCalculator.getInvestments();
const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({ const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({
data: portfolioSnapshot.historicalData, data: portfolioSnapshot.historicalData,
groupBy: 'month' groupBy: 'month'
}); });
const investmentsByYear = portfolioCalculator.getInvestmentsByGroup({ const investmentsByYear = portfolioCalculator.getInvestmentsByGroup({
data: portfolioSnapshot.historicalData, data: portfolioSnapshot.historicalData,
groupBy: 'year' groupBy: 'year'
}); });
expect(portfolioSnapshot.historicalData[0]).toEqual({ expect(portfolioSnapshot.historicalData[0]).toEqual({
date: '2022-03-06', date: '2022-03-06',
investmentValueWithCurrencyEffect: 0, investmentValueWithCurrencyEffect: 0,
netPerformance: 0, netPerformance: 0,
netPerformanceInPercentage: 0, netPerformanceInPercentage: 0,
netPerformanceInPercentageWithCurrencyEffect: 0, netPerformanceInPercentageWithCurrencyEffect: 0,
netPerformanceWithCurrencyEffect: 0, netPerformanceWithCurrencyEffect: 0,
netWorth: 0, netWorth: 0,
totalAccountBalance: 0, totalAccountBalance: 0,
totalInvestment: 0, totalInvestment: 0,
totalInvestmentValueWithCurrencyEffect: 0, totalInvestmentValueWithCurrencyEffect: 0,
value: 0, value: 0,
valueWithCurrencyEffect: 0 valueWithCurrencyEffect: 0
}); });
/** /**
* Closing price on 2022-03-07 is unknown, * Closing price on 2022-03-07 is unknown,
* hence it uses the last unit price (2022-04-11): 87.8 * hence it uses the last unit price (2022-04-11): 87.8
*/ */
expect(portfolioSnapshot.historicalData[1]).toEqual({ expect(portfolioSnapshot.historicalData[1]).toEqual({
date: '2022-03-07', date: '2022-03-07',
investmentValueWithCurrencyEffect: 151.6, investmentValueWithCurrencyEffect: 151.6,
netPerformance: 24, // 2 * (87.8 - 75.8) = 24 netPerformance: 24, // 2 * (87.8 - 75.8) = 24
netPerformanceInPercentage: 0.158311345646438, // 24 ÷ 151.6 = 0.158311345646438 netPerformanceInPercentage: 0.158311345646438, // 24 ÷ 151.6 = 0.158311345646438
netPerformanceInPercentageWithCurrencyEffect: 0.158311345646438, // 24 ÷ 151.6 = 0.158311345646438 netPerformanceInPercentageWithCurrencyEffect: 0.158311345646438, // 24 ÷ 151.6 = 0.158311345646438
netPerformanceWithCurrencyEffect: 24, netPerformanceWithCurrencyEffect: 24,
netWorth: 175.6, // 2 * 87.8 = 175.6 netWorth: 175.6, // 2 * 87.8 = 175.6
totalAccountBalance: 0, totalAccountBalance: 0,
totalInvestment: 151.6, totalInvestment: 151.6,
totalInvestmentValueWithCurrencyEffect: 151.6, totalInvestmentValueWithCurrencyEffect: 151.6,
value: 175.6, // 2 * 87.8 = 175.6 value: 175.6, // 2 * 87.8 = 175.6
valueWithCurrencyEffect: 175.6 valueWithCurrencyEffect: 175.6
}); });
expect( expect(
portfolioSnapshot.historicalData[ portfolioSnapshot.historicalData[
portfolioSnapshot.historicalData.length - 1 portfolioSnapshot.historicalData.length - 1
] ]
).toEqual({ ).toEqual({
date: '2022-04-11', date: '2022-04-11',
investmentValueWithCurrencyEffect: 0, investmentValueWithCurrencyEffect: 0,
netPerformance: 19.86, netPerformance: 19.86,
netPerformanceInPercentage: 0.13100263852242744, netPerformanceInPercentage: 0.13100263852242744,
netPerformanceInPercentageWithCurrencyEffect: 0.13100263852242744, netPerformanceInPercentageWithCurrencyEffect: 0.13100263852242744,
netPerformanceWithCurrencyEffect: 19.86, netPerformanceWithCurrencyEffect: 19.86,
netWorth: 0, netWorth: 0,
totalAccountBalance: 0, totalAccountBalance: 0,
totalInvestment: 0, totalInvestment: 0,
totalInvestmentValueWithCurrencyEffect: 0, totalInvestmentValueWithCurrencyEffect: 0,
value: 0, value: 0,
valueWithCurrencyEffect: 0 valueWithCurrencyEffect: 0
}); });
expect(portfolioSnapshot).toMatchObject({ expect(portfolioSnapshot).toMatchObject({
currentValueInBaseCurrency: new Big('0'), currentValueInBaseCurrency: new Big('0'),
errors: [], errors: [],
hasErrors: false, hasErrors: false,
positions: [ positions: [
{ {
activitiesCount: 2, activitiesCount: 2,
averagePrice: new Big('0'), averagePrice: new Big('0'),
currency: 'CHF', currency: 'CHF',
dataSource: 'YAHOO', dataSource: 'YAHOO',
dateOfFirstActivity: '2022-03-07', dateOfFirstActivity: '2022-03-07',
dividend: new Big('0'), dividend: new Big('0'),
dividendInBaseCurrency: new Big('0'), dividendInBaseCurrency: new Big('0'),
fee: new Big('0'), fee: new Big('0'),
feeInBaseCurrency: new Big('0'), feeInBaseCurrency: new Big('0'),
grossPerformance: new Big('19.86'), grossPerformance: new Big('19.86'),
grossPerformancePercentage: new Big('0.13100263852242744063'), grossPerformancePercentage: new Big('0.13100263852242744063'),
grossPerformancePercentageWithCurrencyEffect: new Big( grossPerformancePercentageWithCurrencyEffect: new Big(
'0.13100263852242744063' '0.13100263852242744063'
), ),
grossPerformanceWithCurrencyEffect: new Big('19.86'), grossPerformanceWithCurrencyEffect: new Big('19.86'),
investment: new Big('0'), investment: new Big('0'),
investmentWithCurrencyEffect: new Big('0'), investmentWithCurrencyEffect: new Big('0'),
netPerformance: new Big('19.86'), netPerformance: new Big('19.86'),
netPerformancePercentage: new Big('0.13100263852242744063'), netPerformancePercentage: new Big('0.13100263852242744063'),
netPerformancePercentageWithCurrencyEffectMap: { netPerformancePercentageWithCurrencyEffectMap: {
max: new Big('0.13100263852242744063') max: new Big('0.13100263852242744063')
}, },
netPerformanceWithCurrencyEffectMap: { netPerformanceWithCurrencyEffectMap: {
max: new Big('19.86') max: new Big('19.86')
}, },
marketPrice: 87.8, marketPrice: 87.8,
marketPriceInBaseCurrency: 87.8, marketPriceInBaseCurrency: 87.8,
quantity: new Big('0'), quantity: new Big('0'),
symbol: 'NOVN.SW', symbol: 'NOVN.SW',
tags: [], tags: [],
timeWeightedInvestment: new Big('151.6'), timeWeightedInvestment: new Big('151.6'),
timeWeightedInvestmentWithCurrencyEffect: new Big('151.6'), timeWeightedInvestmentWithCurrencyEffect: new Big('151.6'),
valueInBaseCurrency: new Big('0') valueInBaseCurrency: new Big('0')
} }
], ],
totalFeesWithCurrencyEffect: new Big('0'), totalFeesWithCurrencyEffect: new Big('0'),
totalInterestWithCurrencyEffect: new Big('0'), totalInterestWithCurrencyEffect: new Big('0'),
totalInvestment: new Big('0'), totalInvestment: new Big('0'),
totalInvestmentWithCurrencyEffect: new Big('0'), totalInvestmentWithCurrencyEffect: new Big('0'),
totalLiabilitiesWithCurrencyEffect: new Big('0') totalLiabilitiesWithCurrencyEffect: new Big('0')
}); });
expect(portfolioSnapshot.historicalData.at(-1)).toMatchObject( expect(portfolioSnapshot.historicalData.at(-1)).toMatchObject(
expect.objectContaining({ expect.objectContaining({
netPerformance: 19.86, netPerformance: 19.86,
netPerformanceInPercentage: 0.13100263852242744063, netPerformanceInPercentage: 0.13100263852242744063,
netPerformanceInPercentageWithCurrencyEffect: 0.13100263852242744063, netPerformanceInPercentageWithCurrencyEffect: 0.13100263852242744063,
netPerformanceWithCurrencyEffect: 19.86, netPerformanceWithCurrencyEffect: 19.86,
totalInvestment: 0, totalInvestment: 0,
totalInvestmentValueWithCurrencyEffect: 0 totalInvestmentValueWithCurrencyEffect: 0
}) })
); );
expect(investments).toEqual([ expect(investments).toEqual([
{ date: '2022-03-07', investment: new Big('151.6') }, { date: '2022-03-07', investment: new Big('151.6') },
{ date: '2022-04-08', investment: new Big('0') } { date: '2022-04-08', investment: new Big('0') }
]); ]);
expect(investmentsByMonth).toEqual([ expect(investmentsByMonth).toEqual([
{ date: '2022-03-01', investment: 151.6 }, { date: '2022-03-01', investment: 151.6 },
{ date: '2022-04-01', investment: -151.6 } { date: '2022-04-01', investment: -151.6 }
]); ]);
expect(investmentsByYear).toEqual([ expect(investmentsByYear).toEqual([
{ date: '2022-01-01', investment: 0 } { date: '2022-01-01', investment: 0 }
]); ]);
}); });
}); });
}); });

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

@ -1,6 +1,6 @@
import { import {
activityDummyData, activityDummyData,
symbolProfileDummyData, assetProfileDummyData,
userDummyData userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
@ -85,17 +85,17 @@ describe('PortfolioCalculator', () => {
const activities: Activity[] = [ const activities: Activity[] = [
{ {
...activityDummyData, ...activityDummyData,
date: new Date('2022-01-01'), assetProfile: {
feeInAssetProfileCurrency: 0, ...assetProfileDummyData,
feeInBaseCurrency: 0,
quantity: 1,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'USD', currency: 'USD',
dataSource: 'MANUAL', dataSource: 'MANUAL',
name: 'Penthouse Apartment', name: 'Penthouse Apartment',
symbol: 'dac95060-d4f2-4653-a253-2c45e6fb5cde' symbol: 'dac95060-d4f2-4653-a253-2c45e6fb5cde'
}, },
date: new Date('2022-01-01'),
feeInAssetProfileCurrency: 0,
feeInBaseCurrency: 0,
quantity: 1,
type: 'BUY', type: 'BUY',
unitPriceInAssetProfileCurrency: 500000 unitPriceInAssetProfileCurrency: 500000
} }

2
apps/api/src/app/portfolio/interfaces/portfolio-order.interface.ts

@ -2,7 +2,7 @@ import { Activity } from '@ghostfolio/common/interfaces';
export interface PortfolioOrder extends Pick<Activity, 'tags' | 'type'> { export interface PortfolioOrder extends Pick<Activity, 'tags' | 'type'> {
assetProfile: Pick< assetProfile: Pick<
Activity['SymbolProfile'], Activity['assetProfile'],
'assetSubClass' | 'currency' | 'dataSource' | 'name' | 'symbol' | 'userId' 'assetSubClass' | 'currency' | 'dataSource' | 'name' | 'symbol' | 'userId'
>; >;
date: string; date: string;

6
apps/api/src/app/portfolio/portfolio.service.spec.ts

@ -373,15 +373,15 @@ describe('PortfolioService', () => {
{ {
account, account,
accountId: account.id, accountId: account.id,
assetProfile: { symbol: 'AAPL' },
quantity: 1, quantity: 1,
SymbolProfile: { symbol: 'AAPL' },
type: 'BUY' type: 'BUY'
}, },
{ {
account: null, account: null,
accountId: null, accountId: null,
assetProfile: { symbol: 'BABA' },
quantity: 2, quantity: 2,
SymbolProfile: { symbol: 'BABA' },
type: 'BUY' type: 'BUY'
} }
], ],
@ -409,8 +409,8 @@ describe('PortfolioService', () => {
{ {
account, account,
accountId: account.id, accountId: account.id,
assetProfile: { symbol: 'AAPL' },
quantity: 1, quantity: 1,
SymbolProfile: { symbol: 'AAPL' },
type: 'BUY' type: 'BUY'
} }
], ],

18
apps/api/src/app/portfolio/portfolio.service.ts

@ -829,10 +829,9 @@ export class PortfolioService {
timeWeightedInvestmentWithCurrencyEffect timeWeightedInvestmentWithCurrencyEffect
} = holding; } = holding;
const activitiesOfHolding = activities.filter(({ SymbolProfile }) => { const activitiesOfHolding = activities.filter(({ assetProfile }) => {
return ( return (
SymbolProfile.dataSource === dataSource && assetProfile.dataSource === dataSource && assetProfile.symbol === symbol
SymbolProfile.symbol === symbol
); );
}); });
@ -2056,11 +2055,11 @@ export class PortfolioService {
.filter(({ isDraft, type }) => { .filter(({ isDraft, type }) => {
return isDraft === false && type === activityType; return isDraft === false && type === activityType;
}) })
.map(({ currency, quantity, SymbolProfile, unitPrice }) => { .map(({ assetProfile, currency, quantity, unitPrice }) => {
return new Big( return new Big(
this.exchangeRateDataService.toCurrency( this.exchangeRateDataService.toCurrency(
new Big(quantity).mul(unitPrice).toNumber(), new Big(quantity).mul(unitPrice).toNumber(),
currency ?? SymbolProfile.currency, currency ?? assetProfile.currency,
userCurrency userCurrency
) )
); );
@ -2198,16 +2197,11 @@ export class PortfolioService {
} }
} }
for (const { for (const { account, assetProfile, quantity, type } of ordersByAccount) {
account,
quantity,
SymbolProfile,
type
} of ordersByAccount) {
const currentValueOfSymbolInBaseCurrency = const currentValueOfSymbolInBaseCurrency =
getFactor(type) * getFactor(type) *
quantity * quantity *
(portfolioItemsNow[SymbolProfile.symbol]?.marketPriceInBaseCurrency ?? (portfolioItemsNow[assetProfile.symbol]?.marketPriceInBaseCurrency ??
0); 0);
if (accounts[account?.id || UNKNOWN_KEY]?.valueInBaseCurrency) { if (accounts[account?.id || UNKNOWN_KEY]?.valueInBaseCurrency) {

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

@ -1,6 +1,11 @@
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { DataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces'; import { DataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import {
ghostfolioFearAndGreedIndexDataSourceCryptocurrencies,
ghostfolioFearAndGreedIndexSymbolCryptocurrencies,
ghostfolioFearAndGreedIndexSymbolStocks
} from '@ghostfolio/common/config';
import { import {
DATE_FORMAT, DATE_FORMAT,
getAssetProfileIdentifier getAssetProfileIdentifier
@ -9,6 +14,7 @@ import {
DataProviderHistoricalResponse, DataProviderHistoricalResponse,
HistoricalDataItem, HistoricalDataItem,
LookupResponse, LookupResponse,
MarketDataOfMarketsResponse,
SymbolItem SymbolItem
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { UserWithSettings } from '@ghostfolio/common/types'; import { UserWithSettings } from '@ghostfolio/common/types';
@ -122,6 +128,46 @@ export class SymbolService {
}; };
} }
public async getMarketDataOfMarkets({
includeHistoricalData
}: {
includeHistoricalData: number;
}): Promise<MarketDataOfMarketsResponse> {
const [
marketDataFearAndGreedIndexCryptocurrencies,
marketDataFearAndGreedIndexStocks
] = await Promise.all([
this.get({
includeHistoricalData,
dataGatheringItem: {
dataSource: ghostfolioFearAndGreedIndexDataSourceCryptocurrencies,
symbol: ghostfolioFearAndGreedIndexSymbolCryptocurrencies
},
useIntradayData: true
}),
this.get({
includeHistoricalData,
dataGatheringItem: {
dataSource:
this.dataProviderService.getDataSourceForFearAndGreedIndexStocks(),
symbol: ghostfolioFearAndGreedIndexSymbolStocks
},
useIntradayData: true
})
]);
return {
fearAndGreedIndex: {
CRYPTOCURRENCIES: {
...marketDataFearAndGreedIndexCryptocurrencies
},
STOCKS: {
...marketDataFearAndGreedIndexStocks
}
}
};
}
public async lookup({ public async lookup({
includeIndices = false, includeIndices = false,
query, query,

7
apps/api/src/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor.ts

@ -78,8 +78,12 @@ export class TransformDataSourceInResponseInterceptor<
valueMap, valueMap,
object: data, object: data,
paths: [ paths: [
'activities[*].assetProfile.dataSource',
'activities[*].dataSource', 'activities[*].dataSource',
/* @deprecated */
'activities[*].SymbolProfile.dataSource', 'activities[*].SymbolProfile.dataSource',
'assetProfile.dataSource', 'assetProfile.dataSource',
'benchmarks[*].dataSource', 'benchmarks[*].dataSource',
'errors[*].dataSource', 'errors[*].dataSource',
@ -88,7 +92,10 @@ export class TransformDataSourceInResponseInterceptor<
'holdings[*].assetProfile.dataSource', 'holdings[*].assetProfile.dataSource',
'holdings[*].dataSource', 'holdings[*].dataSource',
'items[*].dataSource', 'items[*].dataSource',
/* @deprecated */
'SymbolProfile.dataSource', 'SymbolProfile.dataSource',
'watchlist[*].dataSource' 'watchlist[*].dataSource'
] ]
}); });

4
apps/api/src/services/twitter-bot/twitter-bot.service.ts

@ -59,9 +59,9 @@ export class TwitterBotService implements OnModuleInit {
symbolItem.marketPrice symbolItem.marketPrice
); );
let status = `Current market mood is ${emoji} ${text.toLowerCase()} (${ let status = `Current market mood is ${emoji} ${text.toLowerCase()} (${round(
symbolItem.marketPrice symbolItem.marketPrice
}/100)`; )}/100)`;
const benchmarkListing = await this.getBenchmarkListing(); const benchmarkListing = await this.getBenchmarkListing();

22
apps/client/src/app/pages/api/api-page.component.ts

@ -1,3 +1,4 @@
import { GfFearAndGreedIndexComponent } from '@ghostfolio/client/components/fear-and-greed-index/fear-and-greed-index.component';
import { import {
HEADER_KEY_SKIP_INTERCEPTOR, HEADER_KEY_SKIP_INTERCEPTOR,
HEADER_KEY_TOKEN HEADER_KEY_TOKEN
@ -10,6 +11,7 @@ import {
DividendsResponse, DividendsResponse,
HistoricalResponse, HistoricalResponse,
LookupResponse, LookupResponse,
MarketDataOfMarketsResponse,
QuotesResponse QuotesResponse
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
@ -32,7 +34,12 @@ import { FetchFailure, FetchResult } from './interfaces/interfaces';
@Component({ @Component({
host: { class: 'page' }, host: { class: 'page' },
imports: [CommonModule, MatCardModule, NgxSkeletonLoaderModule], imports: [
CommonModule,
GfFearAndGreedIndexComponent,
MatCardModule,
NgxSkeletonLoaderModule
],
selector: 'gf-api-page', selector: 'gf-api-page',
styleUrls: ['./api-page.scss'], styleUrls: ['./api-page.scss'],
templateUrl: './api-page.html' templateUrl: './api-page.html'
@ -48,6 +55,9 @@ export class GfApiPageComponent implements OnInit {
>; >;
protected isinLookupItems$: Observable<FetchResult<LookupResponse['items']>>; protected isinLookupItems$: Observable<FetchResult<LookupResponse['items']>>;
protected lookupItems$: Observable<FetchResult<LookupResponse['items']>>; protected lookupItems$: Observable<FetchResult<LookupResponse['items']>>;
protected marketDataOfMarkets$: Observable<
FetchResult<MarketDataOfMarketsResponse>
>;
protected quotes$: Observable<FetchResult<QuotesResponse['quotes']>>; protected quotes$: Observable<FetchResult<QuotesResponse['quotes']>>;
protected status$: Observable< protected status$: Observable<
FetchResult<DataProviderGhostfolioStatusResponse> FetchResult<DataProviderGhostfolioStatusResponse>
@ -68,6 +78,7 @@ export class GfApiPageComponent implements OnInit {
this.historicalData$ = this.fetchHistoricalData({ symbol: 'AAPL' }); this.historicalData$ = this.fetchHistoricalData({ symbol: 'AAPL' });
this.isinLookupItems$ = this.fetchLookupItems({ query: 'US0378331005' }); this.isinLookupItems$ = this.fetchLookupItems({ query: 'US0378331005' });
this.lookupItems$ = this.fetchLookupItems({ query: 'apple' }); this.lookupItems$ = this.fetchLookupItems({ query: 'apple' });
this.marketDataOfMarkets$ = this.fetchMarketDataOfMarkets();
this.quotes$ = this.fetchQuotes({ symbols: ['AAPL', 'VOO'] }); this.quotes$ = this.fetchQuotes({ symbols: ['AAPL', 'VOO'] });
this.status$ = this.fetchStatus(); this.status$ = this.fetchStatus();
} }
@ -172,6 +183,15 @@ export class GfApiPageComponent implements OnInit {
); );
} }
private fetchMarketDataOfMarkets() {
return this.http
.get<MarketDataOfMarketsResponse>(
'/api/v1/data-providers/ghostfolio/markets',
{ headers: this.getHeaders() }
)
.pipe(this.catchFetchFailure(), takeUntilDestroyed(this.destroyRef));
}
private fetchQuotes({ symbols }: { symbols: string[] }) { private fetchQuotes({ symbols }: { symbols: string[] }) {
const params = new HttpParams().set('symbols', symbols.join(',')); const params = new HttpParams().set('symbols', symbols.join(','));

52
apps/client/src/app/pages/api/api-page.html

@ -168,6 +168,58 @@
} }
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
<div class="row">
@let marketDataOfMarkets = marketDataOfMarkets$ | async;
<div class="col-xs-12 col-md-6">
<mat-card appearance="outlined" class="mb-3">
<mat-card-header>
<mat-card-title class="mb-0">Markets</mat-card-title>
<mat-card-subtitle>Stocks</mat-card-subtitle>
</mat-card-header>
<mat-card-content class="pt-3">
@if (isFetchFailure(marketDataOfMarkets)) {
🔴 {{ marketDataOfMarkets.fetchError }}
} @else if (marketDataOfMarkets) {
<gf-fear-and-greed-index
[fearAndGreedIndex]="
marketDataOfMarkets.fearAndGreedIndex.STOCKS.marketPrice
"
/>
} @else {
<ngx-skeleton-loader
animation="pulse"
[theme]="{ height: '2.5rem', width: '100%' }"
/>
}
</mat-card-content>
</mat-card>
</div>
<div class="col-xs-12 col-md-6">
<mat-card appearance="outlined" class="mb-3">
<mat-card-header>
<mat-card-title class="mb-0">Markets</mat-card-title>
<mat-card-subtitle>Cryptocurrencies</mat-card-subtitle>
</mat-card-header>
<mat-card-content class="pt-3">
@if (isFetchFailure(marketDataOfMarkets)) {
🔴 {{ marketDataOfMarkets.fetchError }}
} @else if (marketDataOfMarkets) {
<gf-fear-and-greed-index
[fearAndGreedIndex]="
marketDataOfMarkets.fearAndGreedIndex.CRYPTOCURRENCIES
.marketPrice
"
/>
} @else {
<ngx-skeleton-loader
animation="pulse"
[theme]="{ height: '2.5rem', width: '100%' }"
/>
}
</mat-card-content>
</mat-card>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

2
apps/client/src/app/pages/portfolio/activities/activities-page.component.ts

@ -409,10 +409,10 @@ export class GfActivitiesPageComponent implements OnInit {
activity: { activity: {
...aActivity, ...aActivity,
accountId: aActivity?.accountId, accountId: aActivity?.accountId,
assetProfile: null,
date: new Date(), date: new Date(),
id: null, id: null,
fee: 0, fee: 0,
SymbolProfile: null,
type: aActivity?.type ?? 'BUY', type: aActivity?.type ?? 'BUY',
unitPrice: null unitPrice: null
}, },

30
apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts

@ -119,7 +119,7 @@ export class GfCreateOrUpdateActivityDialogComponent {
} }
public ngOnInit() { public ngOnInit() {
this.currencyOfAssetProfile = this.data.activity?.SymbolProfile?.currency; this.currencyOfAssetProfile = this.data.activity?.assetProfile?.currency;
this.hasPermissionToCreateOwnTag = this.hasPermissionToCreateOwnTag =
this.data.user?.settings?.isExperimentalFeatures && this.data.user?.settings?.isExperimentalFeatures &&
hasPermission(this.data.user?.permissions, permissions.createOwnTag); hasPermission(this.data.user?.permissions, permissions.createOwnTag);
@ -185,31 +185,31 @@ export class GfCreateOrUpdateActivityDialogComponent {
? this.data.accounts[0].id ? this.data.accounts[0].id
: this.data.activity?.accountId : this.data.activity?.accountId
], ],
assetClass: [this.data.activity?.SymbolProfile?.assetClass], assetClass: [this.data.activity?.assetProfile?.assetClass],
assetSubClass: [this.data.activity?.SymbolProfile?.assetSubClass], assetSubClass: [this.data.activity?.assetProfile?.assetSubClass],
comment: [this.data.activity?.comment], comment: [this.data.activity?.comment],
currency: [ currency: [
this.data.activity?.SymbolProfile?.currency, this.data.activity?.assetProfile?.currency,
Validators.required Validators.required
], ],
currencyOfUnitPrice: [ currencyOfUnitPrice: [
this.data.activity?.currency ?? this.data.activity?.currency ??
this.data.activity?.SymbolProfile?.currency, this.data.activity?.assetProfile?.currency,
Validators.required Validators.required
], ],
dataSource: [ dataSource: [
this.data.activity?.SymbolProfile?.dataSource, this.data.activity?.assetProfile?.dataSource,
Validators.required Validators.required
], ],
date: [this.data.activity?.date, Validators.required], date: [this.data.activity?.date, Validators.required],
fee: [this.data.activity?.fee, Validators.required], fee: [this.data.activity?.fee, Validators.required],
name: [this.data.activity?.SymbolProfile?.name, Validators.required], name: [this.data.activity?.assetProfile?.name, Validators.required],
quantity: [this.data.activity?.quantity, Validators.required], quantity: [this.data.activity?.quantity, Validators.required],
searchSymbol: [ searchSymbol: [
this.data.activity?.SymbolProfile this.data.activity?.assetProfile
? { ? {
dataSource: this.data.activity?.SymbolProfile?.dataSource, dataSource: this.data.activity?.assetProfile?.dataSource,
symbol: this.data.activity?.SymbolProfile?.symbol symbol: this.data.activity?.assetProfile?.symbol
} }
: null, : null,
Validators.required Validators.required
@ -310,7 +310,7 @@ export class GfCreateOrUpdateActivityDialogComponent {
this.activityForm.get('searchSymbol')?.valueChanges.subscribe(() => { this.activityForm.get('searchSymbol')?.valueChanges.subscribe(() => {
if (this.activityForm.get('searchSymbol')?.invalid) { if (this.activityForm.get('searchSymbol')?.invalid) {
this.data.activity.SymbolProfile = null; this.data.activity.assetProfile = null;
} else if ( } else if (
['BUY', 'DIVIDEND', 'SELL'].includes( ['BUY', 'DIVIDEND', 'SELL'].includes(
this.activityForm.get('type')?.value this.activityForm.get('type')?.value
@ -454,11 +454,11 @@ export class GfCreateOrUpdateActivityDialogComponent {
this.activityForm.get('type')?.disable(); this.activityForm.get('type')?.disable();
} }
if (this.data.activity?.SymbolProfile?.symbol) { if (this.data.activity?.assetProfile?.symbol) {
this.dataService this.dataService
.fetchSymbolItem({ .fetchSymbolItem({
dataSource: this.data.activity?.SymbolProfile?.dataSource, dataSource: this.data.activity?.assetProfile?.dataSource,
symbol: this.data.activity?.SymbolProfile?.symbol symbol: this.data.activity?.assetProfile?.symbol
}) })
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ marketPrice }) => { .subscribe(({ marketPrice }) => {
@ -569,7 +569,7 @@ export class GfCreateOrUpdateActivityDialogComponent {
}) })
.pipe( .pipe(
catchError(() => { catchError(() => {
this.data.activity.SymbolProfile = null; this.data.activity.assetProfile = null;
this.isLoading = false; this.isLoading = false;

4
apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/interfaces/interfaces.ts

@ -4,9 +4,9 @@ import { Account } from '@prisma/client';
export interface CreateOrUpdateActivityDialogParams { export interface CreateOrUpdateActivityDialogParams {
accounts: Account[]; accounts: Account[];
activity: Partial<Omit<Activity, 'id' | 'SymbolProfile' | 'unitPrice'>> & { activity: Partial<Omit<Activity, 'assetProfile' | 'id' | 'unitPrice'>> & {
assetProfile: Activity['assetProfile'] | null;
id: string | null; id: string | null;
SymbolProfile: Activity['SymbolProfile'] | null;
unitPrice: number | null; unitPrice: number | null;
}; };
user: User; user: User;

8
apps/client/src/app/services/import-activities.service.ts

@ -165,12 +165,12 @@ export class ImportActivitiesService {
private convertToCreateOrderDto({ private convertToCreateOrderDto({
accountId, accountId,
assetProfile,
comment, comment,
currency, currency,
date, date,
fee, fee,
quantity, quantity,
SymbolProfile,
tags, tags,
type, type,
unitPrice, unitPrice,
@ -184,10 +184,10 @@ export class ImportActivitiesService {
updateAccountBalance, updateAccountBalance,
accountId: accountId ?? undefined, accountId: accountId ?? undefined,
comment: comment ?? undefined, comment: comment ?? undefined,
currency: currency ?? SymbolProfile.currency ?? '', currency: currency ?? assetProfile.currency ?? '',
dataSource: SymbolProfile.dataSource, dataSource: assetProfile.dataSource,
date: date.toString(), date: date.toString(),
symbol: SymbolProfile.symbol, symbol: assetProfile.symbol,
tags: tags?.map(({ id }) => { tags: tags?.map(({ id }) => {
return id; return id;
}) })

12
libs/common/src/lib/config.ts

@ -106,13 +106,20 @@ export const DEFAULT_REDACTED_PATHS = [
'accounts[*].valueInBaseCurrency', 'accounts[*].valueInBaseCurrency',
'activities[*].account.balance', 'activities[*].account.balance',
'activities[*].account.comment', 'activities[*].account.comment',
'activities[*].assetProfile.symbolMapping',
'activities[*].assetProfile.watchedByCount',
'activities[*].comment', 'activities[*].comment',
'activities[*].fee', 'activities[*].fee',
'activities[*].feeInAssetProfileCurrency', 'activities[*].feeInAssetProfileCurrency',
'activities[*].feeInBaseCurrency', 'activities[*].feeInBaseCurrency',
'activities[*].quantity', 'activities[*].quantity',
/* @deprecated */
'activities[*].SymbolProfile.symbolMapping', 'activities[*].SymbolProfile.symbolMapping',
/* @deprecated */
'activities[*].SymbolProfile.watchedByCount', 'activities[*].SymbolProfile.watchedByCount',
'activities[*].value', 'activities[*].value',
'activities[*].valueInBaseCurrency', 'activities[*].valueInBaseCurrency',
'balance', 'balance',
@ -143,8 +150,13 @@ export const DEFAULT_REDACTED_PATHS = [
'platforms[*].balance', 'platforms[*].balance',
'platforms[*].valueInBaseCurrency', 'platforms[*].valueInBaseCurrency',
'quantity', 'quantity',
/* @deprecated */
'SymbolProfile.symbolMapping', 'SymbolProfile.symbolMapping',
/* @deprecated */
'SymbolProfile.watchedByCount', 'SymbolProfile.watchedByCount',
'totalBalanceInBaseCurrency', 'totalBalanceInBaseCurrency',
'totalDividendInBaseCurrency', 'totalDividendInBaseCurrency',
'totalInterestInBaseCurrency', 'totalInterestInBaseCurrency',

11
libs/common/src/lib/dtos/country.dto.ts

@ -0,0 +1,11 @@
import { IsISO31661Alpha2, IsNumber, Max, Min } from 'class-validator';
export class CountryDto {
@IsISO31661Alpha2()
code: string;
@IsNumber()
@Max(1)
@Min(0)
weight: number;
}

14
libs/common/src/lib/dtos/create-asset-profile.dto.ts

@ -1,15 +1,21 @@
import { IsCurrencyCode } from '@ghostfolio/common/validators/is-currency-code'; import { IsCurrencyCode } from '@ghostfolio/common/validators/is-currency-code';
import { AssetClass, AssetSubClass, DataSource, Prisma } from '@prisma/client'; import { AssetClass, AssetSubClass, DataSource, Prisma } from '@prisma/client';
import { Type } from 'class-transformer';
import { import {
IsArray, IsArray,
IsBoolean, IsBoolean,
IsEnum, IsEnum,
IsOptional, IsOptional,
IsString, IsString,
IsUrl IsUrl,
ValidateNested
} from 'class-validator'; } from 'class-validator';
import { CountryDto } from './country.dto';
import { HoldingDto } from './holding.dto';
import { SectorDto } from './sector.dto';
export class CreateAssetProfileDto { export class CreateAssetProfileDto {
@IsEnum(AssetClass) @IsEnum(AssetClass)
@IsOptional() @IsOptional()
@ -25,6 +31,8 @@ export class CreateAssetProfileDto {
@IsArray() @IsArray()
@IsOptional() @IsOptional()
@Type(() => CountryDto)
@ValidateNested({ each: true })
countries?: Prisma.InputJsonArray; countries?: Prisma.InputJsonArray;
@IsCurrencyCode() @IsCurrencyCode()
@ -51,6 +59,8 @@ export class CreateAssetProfileDto {
@IsArray() @IsArray()
@IsOptional() @IsOptional()
@Type(() => HoldingDto)
@ValidateNested({ each: true })
holdings?: Prisma.InputJsonArray; holdings?: Prisma.InputJsonArray;
@IsBoolean() @IsBoolean()
@ -67,6 +77,8 @@ export class CreateAssetProfileDto {
@IsArray() @IsArray()
@IsOptional() @IsOptional()
@Type(() => SectorDto)
@ValidateNested({ each: true })
sectors?: Prisma.InputJsonArray; sectors?: Prisma.InputJsonArray;
@IsString() @IsString()

11
libs/common/src/lib/dtos/holding.dto.ts

@ -0,0 +1,11 @@
import { IsNumber, IsString, Max, Min } from 'class-validator';
export class HoldingDto {
@IsNumber()
@Max(1)
@Min(0)
allocationInPercentage: number;
@IsString()
name: string;
}

8
libs/common/src/lib/dtos/index.ts

@ -1,4 +1,5 @@
import { AuthDeviceDto } from './auth-device.dto'; import { AuthDeviceDto } from './auth-device.dto';
import { CountryDto } from './country.dto';
import { CreateAccessDto } from './create-access.dto'; import { CreateAccessDto } from './create-access.dto';
import { CreateAccountBalanceDto } from './create-account-balance.dto'; import { CreateAccountBalanceDto } from './create-account-balance.dto';
import { CreateAccountWithBalancesDto } from './create-account-with-balances.dto'; import { CreateAccountWithBalancesDto } from './create-account-with-balances.dto';
@ -10,6 +11,9 @@ import { CreatePlatformDto } from './create-platform.dto';
import { CreateTagDto } from './create-tag.dto'; import { CreateTagDto } from './create-tag.dto';
import { CreateWatchlistItemDto } from './create-watchlist-item.dto'; import { CreateWatchlistItemDto } from './create-watchlist-item.dto';
import { DeleteOwnUserDto } from './delete-own-user.dto'; import { DeleteOwnUserDto } from './delete-own-user.dto';
import { HoldingDto } from './holding.dto';
import { ScraperConfigurationDto } from './scraper-configuration.dto';
import { SectorDto } from './sector.dto';
import { TransferBalanceDto } from './transfer-balance.dto'; import { TransferBalanceDto } from './transfer-balance.dto';
import { UpdateAccessDto } from './update-access.dto'; import { UpdateAccessDto } from './update-access.dto';
import { UpdateAccountDto } from './update-account.dto'; import { UpdateAccountDto } from './update-account.dto';
@ -26,6 +30,7 @@ import { UpdateUserSettingDto } from './update-user-setting.dto';
export { export {
AuthDeviceDto, AuthDeviceDto,
CountryDto,
CreateAccessDto, CreateAccessDto,
CreateAccountBalanceDto, CreateAccountBalanceDto,
CreateAccountDto, CreateAccountDto,
@ -37,6 +42,9 @@ export {
CreateTagDto, CreateTagDto,
CreateWatchlistItemDto, CreateWatchlistItemDto,
DeleteOwnUserDto, DeleteOwnUserDto,
HoldingDto,
ScraperConfigurationDto,
SectorDto,
TransferBalanceDto, TransferBalanceDto,
UpdateAccessDto, UpdateAccessDto,
UpdateAccountDto, UpdateAccountDto,

35
libs/common/src/lib/dtos/scraper-configuration.dto.ts

@ -0,0 +1,35 @@
import {
IsIn,
IsNumber,
IsObject,
IsOptional,
IsString,
IsUrl
} from 'class-validator';
export class ScraperConfigurationDto {
@IsNumber()
@IsOptional()
defaultMarketPrice?: number;
@IsObject()
@IsOptional()
headers?: { [key: string]: string };
@IsOptional()
@IsString()
locale?: string;
@IsIn(['instant', 'lazy'])
@IsOptional()
mode?: 'instant' | 'lazy';
@IsString()
selector: string;
@IsUrl({
protocols: ['http', 'https'],
require_protocol: true
})
url: string;
}

11
libs/common/src/lib/dtos/sector.dto.ts

@ -0,0 +1,11 @@
import { IsNumber, IsString, Max, Min } from 'class-validator';
export class SectorDto {
@IsString()
name: string;
@IsNumber()
@Max(1)
@Min(0)
weight: number;
}

17
libs/common/src/lib/dtos/update-asset-profile.dto.ts

@ -7,6 +7,7 @@ import {
DataSource, DataSource,
Prisma Prisma
} from '@prisma/client'; } from '@prisma/client';
import { Type } from 'class-transformer';
import { import {
IsArray, IsArray,
IsBoolean, IsBoolean,
@ -14,9 +15,15 @@ import {
IsObject, IsObject,
IsOptional, IsOptional,
IsString, IsString,
IsUrl IsUrl,
ValidateNested
} from 'class-validator'; } from 'class-validator';
import { CountryDto } from './country.dto';
import { HoldingDto } from './holding.dto';
import { ScraperConfigurationDto } from './scraper-configuration.dto';
import { SectorDto } from './sector.dto';
export class UpdateAssetProfileDto { export class UpdateAssetProfileDto {
@IsEnum(AssetClass) @IsEnum(AssetClass)
@IsOptional() @IsOptional()
@ -32,6 +39,8 @@ export class UpdateAssetProfileDto {
@IsArray() @IsArray()
@IsOptional() @IsOptional()
@Type(() => CountryDto)
@ValidateNested({ each: true })
countries?: Prisma.InputJsonArray; countries?: Prisma.InputJsonArray;
@IsCurrencyCode() @IsCurrencyCode()
@ -48,6 +57,8 @@ export class UpdateAssetProfileDto {
@IsArray() @IsArray()
@IsOptional() @IsOptional()
@Type(() => HoldingDto)
@ValidateNested({ each: true })
holdings?: Prisma.InputJsonArray; holdings?: Prisma.InputJsonArray;
@IsBoolean() @IsBoolean()
@ -60,10 +71,14 @@ export class UpdateAssetProfileDto {
@IsObject() @IsObject()
@IsOptional() @IsOptional()
@Type(() => ScraperConfigurationDto)
@ValidateNested()
scraperConfiguration?: Prisma.InputJsonObject; scraperConfiguration?: Prisma.InputJsonObject;
@IsArray() @IsArray()
@IsOptional() @IsOptional()
@Type(() => SectorDto)
@ValidateNested({ each: true })
sectors?: Prisma.InputJsonArray; sectors?: Prisma.InputJsonArray;
@IsOptional() @IsOptional()

8
libs/common/src/lib/interfaces/activities.interface.ts

@ -5,10 +5,16 @@ import { Order, Tag } from '@prisma/client';
export interface Activity extends Order { export interface Activity extends Order {
account?: AccountWithPlatform; account?: AccountWithPlatform;
assetProfile: EnhancedSymbolProfile;
error?: ActivityError; error?: ActivityError;
feeInAssetProfileCurrency: number; feeInAssetProfileCurrency: number;
feeInBaseCurrency: number; feeInBaseCurrency: number;
SymbolProfile: EnhancedSymbolProfile;
/**
* @deprecated Use `assetProfile` instead
*/
SymbolProfile?: EnhancedSymbolProfile;
tagIds?: string[]; tagIds?: string[];
tags?: Tag[]; tags?: Tag[];
unitPriceInAssetProfileCurrency: number; unitPriceInAssetProfileCurrency: number;

6
libs/common/src/lib/interfaces/responses/public-portfolio-response.interface.ts

@ -26,7 +26,13 @@ export interface PublicPortfolioResponse extends PublicPortfolioResponseV1 {
Order, Order,
'currency' | 'date' | 'fee' | 'quantity' | 'type' | 'unitPrice' 'currency' | 'date' | 'fee' | 'quantity' | 'type' | 'unitPrice'
> & { > & {
assetProfile?: EnhancedSymbolProfile;
/**
* @deprecated Use `assetProfile` instead
*/
SymbolProfile?: EnhancedSymbolProfile; SymbolProfile?: EnhancedSymbolProfile;
value: number; value: number;
valueInBaseCurrency: number; valueInBaseCurrency: number;
})[]; })[];

16
libs/ui/src/lib/activities-table/activities-table.component.html

@ -149,9 +149,9 @@
<th *matHeaderCellDef class="px-1" mat-header-cell></th> <th *matHeaderCellDef class="px-1" mat-header-cell></th>
<td *matCellDef="let element" class="px-1 text-center" mat-cell> <td *matCellDef="let element" class="px-1 text-center" mat-cell>
<gf-entity-logo <gf-entity-logo
[dataSource]="element.SymbolProfile?.dataSource" [dataSource]="element.assetProfile?.dataSource"
[symbol]="element.SymbolProfile?.symbol" [symbol]="element.assetProfile?.symbol"
[tooltip]="element.SymbolProfile?.name" [tooltip]="element.assetProfile?.name"
/> />
</td> </td>
</ng-container> </ng-container>
@ -162,18 +162,18 @@
</th> </th>
<td *matCellDef="let element" class="px-1" mat-cell> <td *matCellDef="let element" class="px-1" mat-cell>
<div class="align-items-center d-flex line-height-1 text-nowrap"> <div class="align-items-center d-flex line-height-1 text-nowrap">
<span class="text-truncate">{{ element.SymbolProfile?.name }}</span> <span class="text-truncate">{{ element.assetProfile?.name }}</span>
@if (element.isDraft) { @if (element.isDraft) {
<span class="badge badge-secondary ml-1" i18n>Draft</span> <span class="badge badge-secondary ml-1" i18n>Draft</span>
} }
</div> </div>
@if ( @if (
element.SymbolProfile?.dataSource !== 'MANUAL' && element.assetProfile?.dataSource !== 'MANUAL' &&
!isUUID(element.SymbolProfile?.symbol) !isUUID(element.assetProfile?.symbol)
) { ) {
<div> <div>
<small class="text-muted">{{ <small class="text-muted">{{
element.SymbolProfile?.symbol | gfSymbol element.assetProfile?.symbol | gfSymbol
}}</small> }}</small>
</div> </div>
} }
@ -309,7 +309,7 @@
class="d-none d-lg-table-cell px-1" class="d-none d-lg-table-cell px-1"
mat-cell mat-cell
> >
{{ element.currency ?? element.SymbolProfile?.currency }} {{ element.currency ?? element.assetProfile?.currency }}
</td> </td>
</ng-container> </ng-container>

10
libs/ui/src/lib/activities-table/activities-table.component.stories.ts

@ -56,7 +56,7 @@ const activities: Activity[] = [
url: 'https://interactivebrokers.com' url: 'https://interactivebrokers.com'
} }
}, },
SymbolProfile: { assetProfile: {
assetClass: 'EQUITY', assetClass: 'EQUITY',
assetSubClass: 'ETF', assetSubClass: 'ETF',
comment: undefined, comment: undefined,
@ -123,7 +123,7 @@ const activities: Activity[] = [
url: 'https://interactivebrokers.com' url: 'https://interactivebrokers.com'
} }
}, },
SymbolProfile: { assetProfile: {
assetClass: 'EQUITY', assetClass: 'EQUITY',
assetSubClass: 'ETF', assetSubClass: 'ETF',
comment: undefined, comment: undefined,
@ -190,7 +190,7 @@ const activities: Activity[] = [
url: 'https://interactivebrokers.com' url: 'https://interactivebrokers.com'
} }
}, },
SymbolProfile: { assetProfile: {
assetClass: 'LIQUIDITY', assetClass: 'LIQUIDITY',
assetSubClass: 'CRYPTOCURRENCY', assetSubClass: 'CRYPTOCURRENCY',
comment: undefined, comment: undefined,
@ -257,7 +257,7 @@ const activities: Activity[] = [
url: 'https://interactivebrokers.com' url: 'https://interactivebrokers.com'
} }
}, },
SymbolProfile: { assetProfile: {
assetClass: 'FIXED_INCOME', assetClass: 'FIXED_INCOME',
assetSubClass: 'BOND', assetSubClass: 'BOND',
comment: 'No data', comment: 'No data',
@ -324,7 +324,7 @@ const activities: Activity[] = [
url: 'https://interactivebrokers.com' url: 'https://interactivebrokers.com'
} }
}, },
SymbolProfile: { assetProfile: {
assetClass: 'EQUITY', assetClass: 'EQUITY',
assetSubClass: 'ETF', assetSubClass: 'ETF',
comment: undefined, comment: undefined,

4
libs/ui/src/lib/activities-table/activities-table.component.ts

@ -285,8 +285,8 @@ export class GfActivitiesTableComponent implements AfterViewInit, OnInit {
} }
} else if (this.canClickActivity(activity)) { } else if (this.canClickActivity(activity)) {
this.activityClicked.emit({ this.activityClicked.emit({
dataSource: activity.SymbolProfile.dataSource, dataSource: activity.assetProfile.dataSource,
symbol: activity.SymbolProfile.symbol symbol: activity.assetProfile.symbol
}); });
} }
} }

536
package-lock.json

@ -63,7 +63,7 @@
"color": "5.0.3", "color": "5.0.3",
"cookie-parser": "1.4.7", "cookie-parser": "1.4.7",
"countries-and-timezones": "3.9.0", "countries-and-timezones": "3.9.0",
"countries-list": "3.3.0", "countries-list": "3.4.0",
"countup.js": "2.10.0", "countup.js": "2.10.0",
"date-fns": "4.4.0", "date-fns": "4.4.0",
"dotenv": "17.2.3", "dotenv": "17.2.3",
@ -117,16 +117,16 @@
"@eslint/js": "9.35.0", "@eslint/js": "9.35.0",
"@nestjs/schematics": "11.1.0", "@nestjs/schematics": "11.1.0",
"@nestjs/testing": "11.1.27", "@nestjs/testing": "11.1.27",
"@nx/angular": "23.0.1", "@nx/angular": "23.0.2",
"@nx/eslint-plugin": "23.0.1", "@nx/eslint-plugin": "23.0.2",
"@nx/jest": "23.0.1", "@nx/jest": "23.0.2",
"@nx/js": "23.0.1", "@nx/js": "23.0.2",
"@nx/module-federation": "23.0.1", "@nx/module-federation": "23.0.2",
"@nx/nest": "23.0.1", "@nx/nest": "23.0.2",
"@nx/node": "23.0.1", "@nx/node": "23.0.2",
"@nx/storybook": "23.0.1", "@nx/storybook": "23.0.2",
"@nx/web": "23.0.1", "@nx/web": "23.0.2",
"@nx/workspace": "23.0.1", "@nx/workspace": "23.0.2",
"@prisma/config": "7.8.0", "@prisma/config": "7.8.0",
"@schematics/angular": "21.2.6", "@schematics/angular": "21.2.6",
"@storybook/addon-docs": "10.1.10", "@storybook/addon-docs": "10.1.10",
@ -154,7 +154,7 @@
"jest": "30.3.0", "jest": "30.3.0",
"jest-environment-jsdom": "30.2.0", "jest-environment-jsdom": "30.2.0",
"jest-preset-angular": "16.0.0", "jest-preset-angular": "16.0.0",
"nx": "23.0.1", "nx": "23.0.2",
"prettier": "3.8.4", "prettier": "3.8.4",
"prettier-plugin-organize-attributes": "1.0.0", "prettier-plugin-organize-attributes": "1.0.0",
"prisma": "7.8.0", "prisma": "7.8.0",
@ -7835,19 +7835,19 @@
} }
}, },
"node_modules/@nx/angular": { "node_modules/@nx/angular": {
"version": "23.0.1", "version": "23.0.2",
"resolved": "https://registry.npmjs.org/@nx/angular/-/angular-23.0.1.tgz", "resolved": "https://registry.npmjs.org/@nx/angular/-/angular-23.0.2.tgz",
"integrity": "sha512-/SJEhwGrAOO35fQjRMwLi3RBqzSaPNViCGqiQldeTokzJqtr9h4QVQxVH+mD+zLdwRI4teoC9taekVrsjXcMoA==", "integrity": "sha512-2xeTEyMgeUMLN9YFSnlQj38JxGjkOUmkSKvRCcrTJNZFuZpxMxwW/oXkPJ3EBBjryAMWCA/qyAh3Te0dokw0/g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@nx/devkit": "23.0.1", "@nx/devkit": "23.0.2",
"@nx/eslint": "23.0.1", "@nx/eslint": "23.0.2",
"@nx/js": "23.0.1", "@nx/js": "23.0.2",
"@nx/module-federation": "23.0.1", "@nx/module-federation": "23.0.2",
"@nx/rspack": "23.0.1", "@nx/rspack": "23.0.2",
"@nx/web": "23.0.1", "@nx/web": "23.0.2",
"@nx/webpack": "23.0.1", "@nx/webpack": "23.0.2",
"@phenomnomnominal/tsquery": "~6.2.0", "@phenomnomnominal/tsquery": "~6.2.0",
"@typescript-eslint/type-utils": "^8.0.0", "@typescript-eslint/type-utils": "^8.0.0",
"enquirer": "~2.3.6", "enquirer": "~2.3.6",
@ -7895,15 +7895,15 @@
} }
}, },
"node_modules/@nx/cypress": { "node_modules/@nx/cypress": {
"version": "23.0.1", "version": "23.0.2",
"resolved": "https://registry.npmjs.org/@nx/cypress/-/cypress-23.0.1.tgz", "resolved": "https://registry.npmjs.org/@nx/cypress/-/cypress-23.0.2.tgz",
"integrity": "sha512-is3oXDoTd2K4C7b2qq3tzKVctBDOdKsfEHIztCwV7ZO3jd9cWl83fLQUL6WI3WVoafo8ALxARRSe1ri4GkzyFg==", "integrity": "sha512-+6RbjMA4KHcBNFIbqOF6oKDWwGsqsJaf2uhH69vDM9rfjJnL7j5CKFzLGSO746Nk4BcLqI0ARR6R8T+w0Ees5g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@nx/devkit": "23.0.1", "@nx/devkit": "23.0.2",
"@nx/eslint": "23.0.1", "@nx/eslint": "23.0.2",
"@nx/js": "23.0.1", "@nx/js": "23.0.2",
"@phenomnomnominal/tsquery": "~6.2.0", "@phenomnomnominal/tsquery": "~6.2.0",
"detect-port": "^2.1.0", "detect-port": "^2.1.0",
"semver": "^7.6.3", "semver": "^7.6.3",
@ -7920,9 +7920,9 @@
} }
}, },
"node_modules/@nx/devkit": { "node_modules/@nx/devkit": {
"version": "23.0.1", "version": "23.0.2",
"resolved": "https://registry.npmjs.org/@nx/devkit/-/devkit-23.0.1.tgz", "resolved": "https://registry.npmjs.org/@nx/devkit/-/devkit-23.0.2.tgz",
"integrity": "sha512-A/chuNS1RZwdbRe/Nf+w0qtPEFHLcZNPzo8Abw5mBxyXmy9yvHZpuZuqDbt/lASFU+TEb74xExL1AnKWwqpOIg==", "integrity": "sha512-svkaufC+nucp3stMucqVN0sV4nz9fEgAd33WB9u2fnFWYx/4F0VJcV4ZLyiG3hHrgs2/Gz5K0OnygPqjCsSqMw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -7932,6 +7932,7 @@
"minimatch": "10.2.5", "minimatch": "10.2.5",
"semver": "^7.6.3", "semver": "^7.6.3",
"tslib": "^2.3.0", "tslib": "^2.3.0",
"yaml": "^2.8.3",
"yargs-parser": "21.1.1" "yargs-parser": "21.1.1"
}, },
"peerDependencies": { "peerDependencies": {
@ -7949,9 +7950,9 @@
} }
}, },
"node_modules/@nx/devkit/node_modules/brace-expansion": { "node_modules/@nx/devkit/node_modules/brace-expansion": {
"version": "5.0.6", "version": "5.0.7",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz",
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -7991,32 +7992,32 @@
} }
}, },
"node_modules/@nx/docker": { "node_modules/@nx/docker": {
"version": "23.0.1", "version": "23.0.2",
"resolved": "https://registry.npmjs.org/@nx/docker/-/docker-23.0.1.tgz", "resolved": "https://registry.npmjs.org/@nx/docker/-/docker-23.0.2.tgz",
"integrity": "sha512-H2/wGZa10X2KhM+BzUkZLCI9wDG1kGqDGco1tDWZczKwk9ViOcwV23ljjt5RtIQIson6B7L56pO2F/8WKJ+1CQ==", "integrity": "sha512-3eccPjL3kwAndVq7t0V1XQDEkEGz7AhnSzpPwr9IMHgVKqgCIJBbNF1Xi5c3AbPlnjy0GHfnGOeb1/jXf2RuNA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@nx/devkit": "23.0.1", "@nx/devkit": "23.0.2",
"enquirer": "~2.3.6", "enquirer": "~2.3.6",
"tslib": "^2.3.0" "tslib": "^2.3.0"
} }
}, },
"node_modules/@nx/eslint": { "node_modules/@nx/eslint": {
"version": "23.0.1", "version": "23.0.2",
"resolved": "https://registry.npmjs.org/@nx/eslint/-/eslint-23.0.1.tgz", "resolved": "https://registry.npmjs.org/@nx/eslint/-/eslint-23.0.2.tgz",
"integrity": "sha512-/P+iXDUsXHeqU7NMDviE/tjJkaVWssc7TLcCoJ6TRy/p+U7HaigLx57VAogBIznldHiyL7KbM7Y0fzqo/hPofw==", "integrity": "sha512-nghKUrUWPj6mZajGkPIC1LxSO+GYWifZjtFnEQU7n5SDffS+QA3mUX5Tr16CZL5tcSLTwpk2/fx3GM/Ji/3DOA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@nx/devkit": "23.0.1", "@nx/devkit": "23.0.2",
"@nx/js": "23.0.1", "@nx/js": "23.0.2",
"semver": "^7.6.3", "semver": "^7.6.3",
"tslib": "^2.3.0", "tslib": "^2.3.0",
"typescript": "~5.9.2" "typescript": "~5.9.2"
}, },
"peerDependencies": { "peerDependencies": {
"@nx/jest": "23.0.1", "@nx/jest": "23.0.2",
"@zkochan/js-yaml": "0.0.7", "@zkochan/js-yaml": "0.0.7",
"eslint": "^8.0.0 || ^9.0.0 || ^10.0.0" "eslint": "^8.0.0 || ^9.0.0 || ^10.0.0"
}, },
@ -8030,14 +8031,14 @@
} }
}, },
"node_modules/@nx/eslint-plugin": { "node_modules/@nx/eslint-plugin": {
"version": "23.0.1", "version": "23.0.2",
"resolved": "https://registry.npmjs.org/@nx/eslint-plugin/-/eslint-plugin-23.0.1.tgz", "resolved": "https://registry.npmjs.org/@nx/eslint-plugin/-/eslint-plugin-23.0.2.tgz",
"integrity": "sha512-fZ4fU4fYxvmHYUtiPuh9vg6KItjqaz4c8SHeqOCRvDsbm7NwQidWsGiruk853No5KnmCHfj/En87v/qGOguoGw==", "integrity": "sha512-ckkifbXqmYrA7OgpfxqeqG5feYndN28hfvbMA0yD0nWQFbx24lC6CkzsJJjuIN3RsQa8yrhKIoObYtiMRxdNLQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@nx/devkit": "23.0.1", "@nx/devkit": "23.0.2",
"@nx/js": "23.0.1", "@nx/js": "23.0.2",
"@phenomnomnominal/tsquery": "~6.2.0", "@phenomnomnominal/tsquery": "~6.2.0",
"@typescript-eslint/type-utils": "^8.0.0", "@typescript-eslint/type-utils": "^8.0.0",
"@typescript-eslint/utils": "^8.0.0", "@typescript-eslint/utils": "^8.0.0",
@ -8075,16 +8076,16 @@
} }
}, },
"node_modules/@nx/jest": { "node_modules/@nx/jest": {
"version": "23.0.1", "version": "23.0.2",
"resolved": "https://registry.npmjs.org/@nx/jest/-/jest-23.0.1.tgz", "resolved": "https://registry.npmjs.org/@nx/jest/-/jest-23.0.2.tgz",
"integrity": "sha512-F5lhjttIExH8hEJ09cqv3Ac1o8GyGTHpD1OLLWnNAYaN/CPpbI16Ix8VLY/fuwJYf6wcm1/vBL3knbv6LABODA==", "integrity": "sha512-NGMUJMzEccj0y/yk+ct+OIFNB1RrdC9C6rFBZ3VFJgxbJlSbxCqQXcO+/668CtGlLVPAZe0baASrCVcvKNUGCQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@jest/reporters": "^30.0.2", "@jest/reporters": "^30.0.2",
"@jest/test-result": "^30.0.2", "@jest/test-result": "^30.0.2",
"@nx/devkit": "23.0.1", "@nx/devkit": "23.0.2",
"@nx/js": "23.0.1", "@nx/js": "23.0.2",
"@phenomnomnominal/tsquery": "~6.2.0", "@phenomnomnominal/tsquery": "~6.2.0",
"identity-obj-proxy": "3.0.0", "identity-obj-proxy": "3.0.0",
"jest-config": "^30.0.2", "jest-config": "^30.0.2",
@ -8150,9 +8151,9 @@
} }
}, },
"node_modules/@nx/js": { "node_modules/@nx/js": {
"version": "23.0.1", "version": "23.0.2",
"resolved": "https://registry.npmjs.org/@nx/js/-/js-23.0.1.tgz", "resolved": "https://registry.npmjs.org/@nx/js/-/js-23.0.2.tgz",
"integrity": "sha512-H8jw1gk7hA8PCXBFC9ocTBpzuXOTvVQ1gA+OlEBMyKqmUaOLNm7yuoOYozwvLsLlCVY27onohSIS8xIdAR/Zow==", "integrity": "sha512-ixtH09vcr6qFaYloCC8JOMLazxTocws2FnHWNbLOpYaO0Nyh4szh8Ttclky7eiI21XYF9z07+0O7/GFlh8hCBA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -8163,8 +8164,8 @@
"@babel/preset-env": "^7.23.2", "@babel/preset-env": "^7.23.2",
"@babel/preset-typescript": "^7.22.5", "@babel/preset-typescript": "^7.22.5",
"@babel/runtime": "^7.22.6", "@babel/runtime": "^7.22.6",
"@nx/devkit": "23.0.1", "@nx/devkit": "23.0.2",
"@nx/workspace": "23.0.1", "@nx/workspace": "23.0.2",
"@zkochan/js-yaml": "0.0.7", "@zkochan/js-yaml": "0.0.7",
"babel-plugin-const-enum": "^1.0.1", "babel-plugin-const-enum": "^1.0.1",
"babel-plugin-macros": "^3.1.0", "babel-plugin-macros": "^3.1.0",
@ -8228,15 +8229,15 @@
} }
}, },
"node_modules/@nx/module-federation": { "node_modules/@nx/module-federation": {
"version": "23.0.1", "version": "23.0.2",
"resolved": "https://registry.npmjs.org/@nx/module-federation/-/module-federation-23.0.1.tgz", "resolved": "https://registry.npmjs.org/@nx/module-federation/-/module-federation-23.0.2.tgz",
"integrity": "sha512-scYruTqvCegeTwAHiBL9MNRKb9RaU+XJ3+wxcmZekyNnvcQjmEPPHndE013wHUuwohyEpVFYrVCulbVb+h2Tvg==", "integrity": "sha512-CkSmu0FCb85YU2mNmi96a5csZzXoas9NJZSj5WEvwOWJpfasImpqMrD06ozG27vv6b/TbIPC1MXUfQw5eyxP5g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@nx/devkit": "23.0.1", "@nx/devkit": "23.0.2",
"@nx/js": "23.0.1", "@nx/js": "23.0.2",
"@nx/web": "23.0.1", "@nx/web": "23.0.2",
"@rspack/core": "1.6.8", "@rspack/core": "1.6.8",
"express": "^4.21.2", "express": "^4.21.2",
"http-proxy-middleware": "^3.0.5", "http-proxy-middleware": "^3.0.5",
@ -8314,9 +8315,9 @@
} }
}, },
"node_modules/@nx/module-federation/node_modules/body-parser": { "node_modules/@nx/module-federation/node_modules/body-parser": {
"version": "1.20.5", "version": "1.20.6",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz",
"integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -8596,17 +8597,17 @@
} }
}, },
"node_modules/@nx/nest": { "node_modules/@nx/nest": {
"version": "23.0.1", "version": "23.0.2",
"resolved": "https://registry.npmjs.org/@nx/nest/-/nest-23.0.1.tgz", "resolved": "https://registry.npmjs.org/@nx/nest/-/nest-23.0.2.tgz",
"integrity": "sha512-/iSE/XzbOl1C1zDuQgeoKBmQAIJYwAj1PJNrnJKf2fS8Mf1s2wGdWnegNsp7iNmAWvZw3zXxhozFiNzJv/HUzA==", "integrity": "sha512-HwawKHpIImnwNfLyULeE+AywB/BIVm9m+ZuoulYP8Gwjvg7Lrq28stnf98sTKEuyGjDDWvTWH4RNHYs0nRXEiA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@nestjs/schematics": "^11.0.0", "@nestjs/schematics": "^11.0.0",
"@nx/devkit": "23.0.1", "@nx/devkit": "23.0.2",
"@nx/eslint": "23.0.1", "@nx/eslint": "23.0.2",
"@nx/js": "23.0.1", "@nx/js": "23.0.2",
"@nx/node": "23.0.1", "@nx/node": "23.0.2",
"semver": "^7.6.3", "semver": "^7.6.3",
"tslib": "^2.3.0" "tslib": "^2.3.0"
}, },
@ -8632,17 +8633,17 @@
} }
}, },
"node_modules/@nx/node": { "node_modules/@nx/node": {
"version": "23.0.1", "version": "23.0.2",
"resolved": "https://registry.npmjs.org/@nx/node/-/node-23.0.1.tgz", "resolved": "https://registry.npmjs.org/@nx/node/-/node-23.0.2.tgz",
"integrity": "sha512-s13ja7MITncR4M/uTGM786Jqco8lJs/9Kvo7jrKFg0Ct8d5XHbu+JFpulmCGPatsBhFA0E/JHSSfR+D9E7MWxA==", "integrity": "sha512-WugI9xFXFs0GEKaelYDl+kpbPgwtvleSI1gnCxsA8MukjqZlm5cSTB4/+fOQSFaWhTesR0BZW5SasRQBJTB65A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@nx/devkit": "23.0.1", "@nx/devkit": "23.0.2",
"@nx/docker": "23.0.1", "@nx/docker": "23.0.2",
"@nx/eslint": "23.0.1", "@nx/eslint": "23.0.2",
"@nx/jest": "23.0.1", "@nx/jest": "23.0.2",
"@nx/js": "23.0.1", "@nx/js": "23.0.2",
"kill-port": "^1.6.1", "kill-port": "^1.6.1",
"semver": "^7.6.3", "semver": "^7.6.3",
"tcp-port-used": "^1.0.2", "tcp-port-used": "^1.0.2",
@ -8666,9 +8667,9 @@
} }
}, },
"node_modules/@nx/nx-darwin-arm64": { "node_modules/@nx/nx-darwin-arm64": {
"version": "23.0.1", "version": "23.0.2",
"resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-23.0.1.tgz", "resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-23.0.2.tgz",
"integrity": "sha512-gQJvgPnbI91DBe23Th2CqD9R/S54cPS3C1f0DhyQ8YEf9rR7EEc+sVGjhgVxlhfOk2W7I1Gy6EkXwpN4aDoW4w==", "integrity": "sha512-9sqhZMVFpF+qM7hq6y2xA4gVK+6RdxRioAwHxorhOZRSXdW7Y7NESs5fm8vOmdddlG07QB7sMefOKLrqCV3zGg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -8680,9 +8681,9 @@
] ]
}, },
"node_modules/@nx/nx-darwin-x64": { "node_modules/@nx/nx-darwin-x64": {
"version": "23.0.1", "version": "23.0.2",
"resolved": "https://registry.npmjs.org/@nx/nx-darwin-x64/-/nx-darwin-x64-23.0.1.tgz", "resolved": "https://registry.npmjs.org/@nx/nx-darwin-x64/-/nx-darwin-x64-23.0.2.tgz",
"integrity": "sha512-e/lvzHKN6gpuD7MqEtfH1fOfnR75E55ytYNt8jaRxKI6EvpCq+Q3MunDuh9GQYAkqDrUqE7AhHrHc+eKATVEHw==", "integrity": "sha512-p6L3AvRhRRaR8Bl3jr76/9H04RdWUQbSgB7agK7GB7vqaLI8RifP2lqeaXcAngzjDAjw2EAf0TjOBP+T67hhcg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -8694,9 +8695,9 @@
] ]
}, },
"node_modules/@nx/nx-freebsd-x64": { "node_modules/@nx/nx-freebsd-x64": {
"version": "23.0.1", "version": "23.0.2",
"resolved": "https://registry.npmjs.org/@nx/nx-freebsd-x64/-/nx-freebsd-x64-23.0.1.tgz", "resolved": "https://registry.npmjs.org/@nx/nx-freebsd-x64/-/nx-freebsd-x64-23.0.2.tgz",
"integrity": "sha512-f582OhSYN9qHpA9Ox9qnr3kZSZ7gQHs7crmBUutmbXmZQB2TDS/TlhvYSNnxudpwHR/tuWGi2IOQqa7zGOZj1Q==", "integrity": "sha512-/py4I8Rp2UURses9H/+SQmgPVnHVSJgPimJLhXIfsRavKGu4RS7Ddu1OyNqSkCT3Otic6ImMTtkufURW22KiEQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -8708,9 +8709,9 @@
] ]
}, },
"node_modules/@nx/nx-linux-arm-gnueabihf": { "node_modules/@nx/nx-linux-arm-gnueabihf": {
"version": "23.0.1", "version": "23.0.2",
"resolved": "https://registry.npmjs.org/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-23.0.1.tgz", "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-23.0.2.tgz",
"integrity": "sha512-VjhqPc6E7aiI0e+lowrkVbdyulsmP9fgMdcX1mCzXCEu/XZDcUbZ5qveR964cMhvm5qKn0ILJtJOUqZgmOT3Xg==", "integrity": "sha512-xv2IzeiWJFWi4WjK0ocMkP+ze1lDeoPVCg0xOTqVs40gM66V1wVw3EK077gTqU4m0Bq1wUxe6/I8WaIGlkLgug==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -8722,9 +8723,9 @@
] ]
}, },
"node_modules/@nx/nx-linux-arm64-gnu": { "node_modules/@nx/nx-linux-arm64-gnu": {
"version": "23.0.1", "version": "23.0.2",
"resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-23.0.1.tgz", "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-23.0.2.tgz",
"integrity": "sha512-zX2JdHQejZWB3DRgNsh77qOVYaSSjSLuBP2qIqc7EWVlCUnR7Aj3e65PTIps4LxMMmUp4twZA2ezS0rtyK2A4w==", "integrity": "sha512-ckA6hTXST+agxt/HzPGqMss9qFCZhO9b07o8usygb7QFBYQRXFgcYzhTblq4yiTL5ibJmXAGGh98011fLA2MVw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -8736,9 +8737,9 @@
] ]
}, },
"node_modules/@nx/nx-linux-arm64-musl": { "node_modules/@nx/nx-linux-arm64-musl": {
"version": "23.0.1", "version": "23.0.2",
"resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-23.0.1.tgz", "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-23.0.2.tgz",
"integrity": "sha512-9lyhxRNBgNYwHt6paq0OLzoKNoEGF5LnNW2YYrgFY8Cjtsg/Q4pcfZ1vB5o9FX9OmUgUQs3t2d4tU8YDukRUWg==", "integrity": "sha512-PAxBxy7m//cKxUeIb6Sk2X5MJ/wjcJcqCx7/L0p8omTt/y/+q1TGpVy6qmJMPUWzNgAULXtsVRSOK4rmiBcrQQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -8750,9 +8751,9 @@
] ]
}, },
"node_modules/@nx/nx-linux-x64-gnu": { "node_modules/@nx/nx-linux-x64-gnu": {
"version": "23.0.1", "version": "23.0.2",
"resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-23.0.1.tgz", "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-23.0.2.tgz",
"integrity": "sha512-kVszY2xRyyrCXgdCdM1qG1WUhDjNPZxtdWq86a0TyIRJjfJTP9NHqpyhmvj9c2RdZxKVWHotx6fBJzY6Vn2ZrA==", "integrity": "sha512-gR146Mo+BjhrIU2fNOJeVjX8HEAHrtmc7IyrD0qD9yqyq0l9Mdx92JnMW3yVQaYIMpPJIqsOvHTDIUbTLFrmTA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -8764,9 +8765,9 @@
] ]
}, },
"node_modules/@nx/nx-linux-x64-musl": { "node_modules/@nx/nx-linux-x64-musl": {
"version": "23.0.1", "version": "23.0.2",
"resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-23.0.1.tgz", "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-23.0.2.tgz",
"integrity": "sha512-co71K2n4zcS1SYR8EBRlCvIko7M1YycO2tZL0nrCrga87AF5dzCwx+wEclpyCR/4tNOY3FrACk4gIkVskh3CdA==", "integrity": "sha512-L7JkaoAI0p+DpAi2CNCqPq8nHWkApQw2td0Cyt+stOx/OJlmEteyc/MXyTHBoMPoopNj1wWANJJQm/G1anOLhQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -8778,9 +8779,9 @@
] ]
}, },
"node_modules/@nx/nx-win32-arm64-msvc": { "node_modules/@nx/nx-win32-arm64-msvc": {
"version": "23.0.1", "version": "23.0.2",
"resolved": "https://registry.npmjs.org/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-23.0.1.tgz", "resolved": "https://registry.npmjs.org/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-23.0.2.tgz",
"integrity": "sha512-7oma7iy5fbnn+x5AP7SFGMuleAA2R5RZm26dn+faikyQ4PXjoRAikWJJNiOWAeCA0BaMAeVedI6fJeAsVeDUKg==", "integrity": "sha512-B3ePaYeu31ivP3i72Vou5RBYFVrDKCMZgz7Jsax+RvEJa8dNfI+Ynh/PSoXzHudN0YsrekkKbjlxNvp/D6fWFA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -8792,9 +8793,9 @@
] ]
}, },
"node_modules/@nx/nx-win32-x64-msvc": { "node_modules/@nx/nx-win32-x64-msvc": {
"version": "23.0.1", "version": "23.0.2",
"resolved": "https://registry.npmjs.org/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-23.0.1.tgz", "resolved": "https://registry.npmjs.org/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-23.0.2.tgz",
"integrity": "sha512-TE/wvBa2cpkVXmk/AXUQAneong4JReS2hyNpAUONKG1yXU7TDKe0wvn1xQXxAbyspudT9NuCnVtpVuEkRz8S+Q==", "integrity": "sha512-/NiB9w8nYrw7LUkcmwAJ9wis5O+kh3ahSZXMDHVYgFnD8yN7kLql39MNJD+/DGstjNSDvWh45ftqr7mZMi6wpQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -8806,16 +8807,16 @@
] ]
}, },
"node_modules/@nx/rspack": { "node_modules/@nx/rspack": {
"version": "23.0.1", "version": "23.0.2",
"resolved": "https://registry.npmjs.org/@nx/rspack/-/rspack-23.0.1.tgz", "resolved": "https://registry.npmjs.org/@nx/rspack/-/rspack-23.0.2.tgz",
"integrity": "sha512-Hbn4vUrotIy4EVaPHKQX/ijuuH9+fx3ndaEdzQT0O08iJmVmABozsJ1GmQDYO4aU4qvu3eDsJr1IOQ3jqgp7vQ==", "integrity": "sha512-Aa/7028o5pPqFbR6qG+kurkrGx+/CfSJiOgwsDz1ND9pd1SYP/Z2J9YXL/tgxsDWMy24FaAJvs4MvHMZvAyvLw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@nx/devkit": "23.0.1", "@nx/devkit": "23.0.2",
"@nx/js": "23.0.1", "@nx/js": "23.0.2",
"@nx/module-federation": "23.0.1", "@nx/module-federation": "23.0.2",
"@nx/web": "23.0.1", "@nx/web": "23.0.2",
"@phenomnomnominal/tsquery": "~6.2.0", "@phenomnomnominal/tsquery": "~6.2.0",
"autoprefixer": "^10.4.9", "autoprefixer": "^10.4.9",
"browserslist": "^4.26.0", "browserslist": "^4.26.0",
@ -8888,9 +8889,9 @@
} }
}, },
"node_modules/@nx/rspack/node_modules/body-parser": { "node_modules/@nx/rspack/node_modules/body-parser": {
"version": "1.20.5", "version": "1.20.6",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz",
"integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -9228,22 +9229,22 @@
} }
}, },
"node_modules/@nx/storybook": { "node_modules/@nx/storybook": {
"version": "23.0.1", "version": "23.0.2",
"resolved": "https://registry.npmjs.org/@nx/storybook/-/storybook-23.0.1.tgz", "resolved": "https://registry.npmjs.org/@nx/storybook/-/storybook-23.0.2.tgz",
"integrity": "sha512-QNJHgxG4qwHpvpcaJ4sllpmlxHCEJjse1dTBFRGL9U+F71ye/W6vYrmHirKuV9yO1X6DR6D1rQMj6WVTGSnvzw==", "integrity": "sha512-UG6Vh66L6rfSicjUDyP6dkdkX/TimoXxvmHmUXfO2YZlR/uQXIbkav2KpYloea7NPyjX2jehezCEOrTbINEf6A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@nx/cypress": "23.0.1", "@nx/cypress": "23.0.2",
"@nx/devkit": "23.0.1", "@nx/devkit": "23.0.2",
"@nx/eslint": "23.0.1", "@nx/eslint": "23.0.2",
"@nx/js": "23.0.1", "@nx/js": "23.0.2",
"@phenomnomnominal/tsquery": "~6.2.0", "@phenomnomnominal/tsquery": "~6.2.0",
"semver": "^7.6.3", "semver": "^7.6.3",
"tslib": "^2.3.0" "tslib": "^2.3.0"
}, },
"peerDependencies": { "peerDependencies": {
"@nx/web": "23.0.1", "@nx/web": "23.0.2",
"storybook": ">=8.0.0 <11.0.0" "storybook": ">=8.0.0 <11.0.0"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
@ -9253,26 +9254,26 @@
} }
}, },
"node_modules/@nx/web": { "node_modules/@nx/web": {
"version": "23.0.1", "version": "23.0.2",
"resolved": "https://registry.npmjs.org/@nx/web/-/web-23.0.1.tgz", "resolved": "https://registry.npmjs.org/@nx/web/-/web-23.0.2.tgz",
"integrity": "sha512-0iwqCPJ5A27/XzqF6637dcRmJO3/TY7nQrEobS0yJ4W2xhvCIRwsWmPkuUAZgjd4622yQauHAwPQAnkIEILPiQ==", "integrity": "sha512-PIUMDB79yvt6nj9ypLTCYZfA7N9v8mvWEHRbt43rusrzl3P4wqZIiveuyIstmt4HuvIHW3rRjbZTSs/eYrkqRg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@nx/devkit": "23.0.1", "@nx/devkit": "23.0.2",
"@nx/js": "23.0.1", "@nx/js": "23.0.2",
"detect-port": "^2.1.0", "detect-port": "^2.1.0",
"http-server": "^14.1.0", "http-server": "^14.1.0",
"picocolors": "^1.1.0", "picocolors": "^1.1.0",
"tslib": "^2.3.0" "tslib": "^2.3.0"
}, },
"peerDependencies": { "peerDependencies": {
"@nx/cypress": "23.0.1", "@nx/cypress": "23.0.2",
"@nx/eslint": "23.0.1", "@nx/eslint": "23.0.2",
"@nx/jest": "23.0.1", "@nx/jest": "23.0.2",
"@nx/playwright": "23.0.1", "@nx/playwright": "23.0.2",
"@nx/vite": "23.0.1", "@nx/vite": "23.0.2",
"@nx/webpack": "23.0.1" "@nx/webpack": "23.0.2"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"@nx/cypress": { "@nx/cypress": {
@ -9296,15 +9297,15 @@
} }
}, },
"node_modules/@nx/webpack": { "node_modules/@nx/webpack": {
"version": "23.0.1", "version": "23.0.2",
"resolved": "https://registry.npmjs.org/@nx/webpack/-/webpack-23.0.1.tgz", "resolved": "https://registry.npmjs.org/@nx/webpack/-/webpack-23.0.2.tgz",
"integrity": "sha512-0ELnKItRtIlcHAN8AaSY4uwabA62aAFDCvYFqadQCTeN5eBYl1VGfzenWMvGyVZoU7K9dYtPVSkZSUhltvHQgw==", "integrity": "sha512-TbMBG8mLaD7SImTNKH9CZyhgciS6vJZ6/dPK01aHJ4lamKtnFyRGsktgPkLvhKH9jJqLSWSWb4IWAsoHMe46GA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/core": "^7.23.2", "@babel/core": "^7.23.2",
"@nx/devkit": "23.0.1", "@nx/devkit": "23.0.2",
"@nx/js": "23.0.1", "@nx/js": "23.0.2",
"@phenomnomnominal/tsquery": "~6.2.0", "@phenomnomnominal/tsquery": "~6.2.0",
"ajv": "^8.0.0", "ajv": "^8.0.0",
"autoprefixer": "^10.4.9", "autoprefixer": "^10.4.9",
@ -9331,7 +9332,7 @@
"source-map-loader": "^5.0.0", "source-map-loader": "^5.0.0",
"style-loader": "^3.3.0", "style-loader": "^3.3.0",
"terser-webpack-plugin": "^5.3.3", "terser-webpack-plugin": "^5.3.3",
"ts-loader": "^9.3.1", "ts-loader": "^9.5.7",
"tsconfig-paths-webpack-plugin": "4.2.0", "tsconfig-paths-webpack-plugin": "4.2.0",
"tslib": "^2.3.0", "tslib": "^2.3.0",
"webpack-node-externals": "^3.0.0", "webpack-node-externals": "^3.0.0",
@ -9461,17 +9462,17 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@nx/workspace": { "node_modules/@nx/workspace": {
"version": "23.0.1", "version": "23.0.2",
"resolved": "https://registry.npmjs.org/@nx/workspace/-/workspace-23.0.1.tgz", "resolved": "https://registry.npmjs.org/@nx/workspace/-/workspace-23.0.2.tgz",
"integrity": "sha512-VdbvMTSEzp3ONZwiy83XEu8ktykC8aEI7M4mqKs5RNKHBFg3jtao2NFo3wDqHqnn1q9Fdaj8EbyUn08BUR5L3w==", "integrity": "sha512-7As52HT7l7ypwukR0cKZIDZ771O0hYTDwkfJmzWPHl3YghiROnK3/Xlh/84ITo+4gtCMORUyZXZgQfOi8136ug==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@nx/devkit": "23.0.1", "@nx/devkit": "23.0.2",
"@zkochan/js-yaml": "0.0.7", "@zkochan/js-yaml": "0.0.7",
"chalk": "^4.1.0", "chalk": "^4.1.0",
"enquirer": "~2.3.6", "enquirer": "~2.3.6",
"nx": "23.0.1", "nx": "23.0.2",
"picomatch": "4.0.4", "picomatch": "4.0.4",
"semver": "^7.6.3", "semver": "^7.6.3",
"tslib": "^2.3.0", "tslib": "^2.3.0",
@ -15072,17 +15073,45 @@
} }
}, },
"node_modules/axios": { "node_modules/axios": {
"version": "1.16.0", "version": "1.16.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.1.tgz",
"integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==", "integrity": "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"follow-redirects": "^1.16.0", "follow-redirects": "^1.16.0",
"form-data": "^4.0.5", "form-data": "^4.0.5",
"https-proxy-agent": "^5.0.1",
"proxy-from-env": "^2.1.0" "proxy-from-env": "^2.1.0"
} }
}, },
"node_modules/axios/node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"debug": "4"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/axios/node_modules/https-proxy-agent": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
"dev": true,
"license": "MIT",
"dependencies": {
"agent-base": "6",
"debug": "4"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/axobject-query": { "node_modules/axobject-query": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
@ -17033,9 +17062,9 @@
} }
}, },
"node_modules/countries-list": { "node_modules/countries-list": {
"version": "3.3.0", "version": "3.4.0",
"resolved": "https://registry.npmjs.org/countries-list/-/countries-list-3.3.0.tgz", "resolved": "https://registry.npmjs.org/countries-list/-/countries-list-3.4.0.tgz",
"integrity": "sha512-XRUjS+dcZuNh/fg3+mka3bXgcg4TbQZ1gaK5IJqO6qulerBANl1bmrd20P2dgmPkBpP+5FnejiSF1gd7bgAg+g==", "integrity": "sha512-SonKkjlEGUNz/Ugj6akOMUl8/F6EBVbLq/nQbDKhZo60EFyNgsVIgDP521J9pGhywikV514Ct4BueIqiOnvBBA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/countup.js": { "node_modules/countup.js": {
@ -17277,9 +17306,9 @@
} }
}, },
"node_modules/css-minimizer-webpack-plugin/node_modules/postcss": { "node_modules/css-minimizer-webpack-plugin/node_modules/postcss": {
"version": "8.5.15", "version": "8.5.19",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz",
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", "integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -26840,9 +26869,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/nx": { "node_modules/nx": {
"version": "23.0.1", "version": "23.0.2",
"resolved": "https://registry.npmjs.org/nx/-/nx-23.0.1.tgz", "resolved": "https://registry.npmjs.org/nx/-/nx-23.0.2.tgz",
"integrity": "sha512-HnK0Ke8FcPeVQffYm1oyzkLNn7khrI8SeDeC3iyLhw/UEMCB24hjI5JSs6Amlyeb0/GaeiuQuts8NkQKd/NpGA==", "integrity": "sha512-e5H6ceqj0Z8ovAmtsiHXswwpiNPrr1DRhvJMTpc2AW8G1za9PKxk3bP5josShsIrmGEsOlBNZZsxszXA2+Q2dw==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
@ -26855,12 +26884,13 @@
"@tybys/wasm-util": "0.9.0", "@tybys/wasm-util": "0.9.0",
"@yarnpkg/lockfile": "1.1.0", "@yarnpkg/lockfile": "1.1.0",
"@zkochan/js-yaml": "0.0.7", "@zkochan/js-yaml": "0.0.7",
"agent-base": "6.0.2",
"ansi-colors": "4.1.3", "ansi-colors": "4.1.3",
"ansi-regex": "5.0.1", "ansi-regex": "5.0.1",
"ansi-styles": "4.3.0", "ansi-styles": "4.3.0",
"argparse": "2.0.1", "argparse": "2.0.1",
"asynckit": "0.4.0", "asynckit": "0.4.0",
"axios": "1.16.0", "axios": "1.16.1",
"balanced-match": "4.0.3", "balanced-match": "4.0.3",
"base64-js": "1.5.1", "base64-js": "1.5.1",
"bl": "4.1.0", "bl": "4.1.0",
@ -26875,6 +26905,7 @@
"color-convert": "2.0.1", "color-convert": "2.0.1",
"color-name": "1.1.4", "color-name": "1.1.4",
"combined-stream": "1.0.8", "combined-stream": "1.0.8",
"debug": "4.4.3",
"defaults": "1.0.4", "defaults": "1.0.4",
"define-lazy-prop": "2.0.0", "define-lazy-prop": "2.0.0",
"delayed-stream": "1.0.0", "delayed-stream": "1.0.0",
@ -26905,6 +26936,7 @@
"has-symbols": "1.1.0", "has-symbols": "1.1.0",
"has-tostringtag": "1.0.2", "has-tostringtag": "1.0.2",
"hasown": "2.0.4", "hasown": "2.0.4",
"https-proxy-agent": "5.0.1",
"ieee754": "1.2.1", "ieee754": "1.2.1",
"ignore": "7.0.5", "ignore": "7.0.5",
"inherits": "2.0.4", "inherits": "2.0.4",
@ -26924,6 +26956,7 @@
"mimic-fn": "2.1.0", "mimic-fn": "2.1.0",
"minimatch": "10.2.5", "minimatch": "10.2.5",
"minimist": "1.2.8", "minimist": "1.2.8",
"ms": "2.1.3",
"npm-run-path": "4.0.1", "npm-run-path": "4.0.1",
"once": "1.4.0", "once": "1.4.0",
"onetime": "5.1.2", "onetime": "5.1.2",
@ -26964,16 +26997,16 @@
"nx-cloud": "dist/bin/nx-cloud.js" "nx-cloud": "dist/bin/nx-cloud.js"
}, },
"optionalDependencies": { "optionalDependencies": {
"@nx/nx-darwin-arm64": "23.0.1", "@nx/nx-darwin-arm64": "23.0.2",
"@nx/nx-darwin-x64": "23.0.1", "@nx/nx-darwin-x64": "23.0.2",
"@nx/nx-freebsd-x64": "23.0.1", "@nx/nx-freebsd-x64": "23.0.2",
"@nx/nx-linux-arm-gnueabihf": "23.0.1", "@nx/nx-linux-arm-gnueabihf": "23.0.2",
"@nx/nx-linux-arm64-gnu": "23.0.1", "@nx/nx-linux-arm64-gnu": "23.0.2",
"@nx/nx-linux-arm64-musl": "23.0.1", "@nx/nx-linux-arm64-musl": "23.0.2",
"@nx/nx-linux-x64-gnu": "23.0.1", "@nx/nx-linux-x64-gnu": "23.0.2",
"@nx/nx-linux-x64-musl": "23.0.1", "@nx/nx-linux-x64-musl": "23.0.2",
"@nx/nx-win32-arm64-msvc": "23.0.1", "@nx/nx-win32-arm64-msvc": "23.0.2",
"@nx/nx-win32-x64-msvc": "23.0.1" "@nx/nx-win32-x64-msvc": "23.0.2"
}, },
"peerDependencies": { "peerDependencies": {
"@swc-node/register": "^1.11.1", "@swc-node/register": "^1.11.1",
@ -27039,6 +27072,19 @@
"tslib": "^2.4.0" "tslib": "^2.4.0"
} }
}, },
"node_modules/nx/node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"debug": "4"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/nx/node_modules/balanced-match": { "node_modules/nx/node_modules/balanced-match": {
"version": "4.0.3", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.3.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.3.tgz",
@ -27128,6 +27174,20 @@
"node": ">=0.8.0" "node": ">=0.8.0"
} }
}, },
"node_modules/nx/node_modules/https-proxy-agent": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
"dev": true,
"license": "MIT",
"dependencies": {
"agent-base": "6",
"debug": "4"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/nx/node_modules/ignore": { "node_modules/nx/node_modules/ignore": {
"version": "7.0.5", "version": "7.0.5",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
@ -27256,6 +27316,13 @@
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/nx/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
"license": "MIT"
},
"node_modules/nx/node_modules/open": { "node_modules/nx/node_modules/open": {
"version": "8.4.2", "version": "8.4.2",
"resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz",
@ -32460,9 +32527,9 @@
} }
}, },
"node_modules/svgo": { "node_modules/svgo": {
"version": "4.0.1", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.1.tgz", "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.2.tgz",
"integrity": "sha512-XDpWUOPC6FEibaLzjfe0ucaV0YrOjYotGJO1WpF0Zd+n6ZGEQUsSugaoLq9QkEZtAfQIxT42UChcssDVPP3+/w==", "integrity": "sha512-ekx94z1rRc5LDi6oSUaeRnYhd0UOJxdtQCL2rF8xpWxD3TPAsISWOrxezqGovqS38GRZOdpDfvQe3ts6F7nsng==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -33192,15 +33259,15 @@
} }
}, },
"node_modules/ts-checker-rspack-plugin": { "node_modules/ts-checker-rspack-plugin": {
"version": "1.5.1", "version": "1.5.2",
"resolved": "https://registry.npmjs.org/ts-checker-rspack-plugin/-/ts-checker-rspack-plugin-1.5.1.tgz", "resolved": "https://registry.npmjs.org/ts-checker-rspack-plugin/-/ts-checker-rspack-plugin-1.5.2.tgz",
"integrity": "sha512-h0jF3PAIrG+UA2nZ2OPUEt0NywkpiJkydkoVg/Kd1OFPFlrayNBJoSP0zeNPUpdsFEt7ICX1BjENCfOH9nmMyA==", "integrity": "sha512-YtdNbLnJ9fU6itfQky0K9Lebb/N0TUSbSVUByyoGt27em3hiTCVnfW2MzxFUHpdMv5+7xPmoS8YBwdDyHfEaGQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@rspack/lite-tapable": "^1.1.2", "@rspack/lite-tapable": "^1.1.2",
"chokidar": "^3.6.0", "chokidar": "^3.6.0",
"memfs": "^4.57.7", "memfs": "^4.57.8",
"picocolors": "^1.1.1" "picocolors": "^1.1.1"
}, },
"peerDependencies": { "peerDependencies": {
@ -33252,14 +33319,14 @@
} }
}, },
"node_modules/ts-checker-rspack-plugin/node_modules/@jsonjoy.com/fs-core": { "node_modules/ts-checker-rspack-plugin/node_modules/@jsonjoy.com/fs-core": {
"version": "4.57.8", "version": "4.64.0",
"resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-core/-/fs-core-4.57.8.tgz", "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-core/-/fs-core-4.64.0.tgz",
"integrity": "sha512-YzVbwggV9452VCeHgo0bjsTaUt1O7JE0XpEsPar93nn/+RAwXk0mb1Y+f5EDJ3TRtRCFe+Ck5RuojdfB4jeHVw==", "integrity": "sha512-zs2TAq7Six5jgMuoMNjpspAvOP3mhtgq/k1UyQodEzCtQi/N83y2/y+zcvnZSGp/Rxq96DBN+bValOBQAyn/ew==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@jsonjoy.com/fs-node-builtins": "4.57.8", "@jsonjoy.com/fs-node-builtins": "4.64.0",
"@jsonjoy.com/fs-node-utils": "4.57.8", "@jsonjoy.com/fs-node-utils": "4.64.0",
"thingies": "^2.5.0" "thingies": "^2.5.0"
}, },
"engines": { "engines": {
@ -33274,15 +33341,15 @@
} }
}, },
"node_modules/ts-checker-rspack-plugin/node_modules/@jsonjoy.com/fs-fsa": { "node_modules/ts-checker-rspack-plugin/node_modules/@jsonjoy.com/fs-fsa": {
"version": "4.57.8", "version": "4.64.0",
"resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-fsa/-/fs-fsa-4.57.8.tgz", "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-fsa/-/fs-fsa-4.64.0.tgz",
"integrity": "sha512-vmClyvCQMxgqz7uamDiGtRfp4MjzOznk3pcQjCxlIwJcw7TWeyr+bF30hI0x8NxdtNOGMg1pHM74VDIXOeyjuw==", "integrity": "sha512-nMWOVbkLFyEgmXZih3wyvxA9XpgyyqyfrINMHvEFqhi7uqfRl7c9ERJt6yX7vgMPrB9Uo+OJO+Spa0cFzPD01w==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@jsonjoy.com/fs-core": "4.57.8", "@jsonjoy.com/fs-core": "4.64.0",
"@jsonjoy.com/fs-node-builtins": "4.57.8", "@jsonjoy.com/fs-node-builtins": "4.64.0",
"@jsonjoy.com/fs-node-utils": "4.57.8", "@jsonjoy.com/fs-node-utils": "4.64.0",
"thingies": "^2.5.0" "thingies": "^2.5.0"
}, },
"engines": { "engines": {
@ -33297,17 +33364,17 @@
} }
}, },
"node_modules/ts-checker-rspack-plugin/node_modules/@jsonjoy.com/fs-node": { "node_modules/ts-checker-rspack-plugin/node_modules/@jsonjoy.com/fs-node": {
"version": "4.57.8", "version": "4.64.0",
"resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node/-/fs-node-4.57.8.tgz", "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node/-/fs-node-4.64.0.tgz",
"integrity": "sha512-IPEOlDYSnTDYpjQlQg2F8h+eqxKQN3sdbroI0WrteRiQZ462HzVpBo9ZZX485njz4nAacoe3fd4iDiIhk+k5Hg==", "integrity": "sha512-dO+NNkODbUli4uV42bcNrrLvq5rE7SNpdZ5TNd0dtbLsAaNK3MDiIC9lUi+brboGoIjW6vd2fB1qao60nrk5xA==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@jsonjoy.com/fs-core": "4.57.8", "@jsonjoy.com/fs-core": "4.64.0",
"@jsonjoy.com/fs-node-builtins": "4.57.8", "@jsonjoy.com/fs-node-builtins": "4.64.0",
"@jsonjoy.com/fs-node-utils": "4.57.8", "@jsonjoy.com/fs-node-utils": "4.64.0",
"@jsonjoy.com/fs-print": "4.57.8", "@jsonjoy.com/fs-print": "4.64.0",
"@jsonjoy.com/fs-snapshot": "4.57.8", "@jsonjoy.com/fs-snapshot": "4.64.0",
"glob-to-regex.js": "^1.0.0", "glob-to-regex.js": "^1.0.0",
"thingies": "^2.5.0" "thingies": "^2.5.0"
}, },
@ -33323,9 +33390,9 @@
} }
}, },
"node_modules/ts-checker-rspack-plugin/node_modules/@jsonjoy.com/fs-node-builtins": { "node_modules/ts-checker-rspack-plugin/node_modules/@jsonjoy.com/fs-node-builtins": {
"version": "4.57.8", "version": "4.64.0",
"resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-builtins/-/fs-node-builtins-4.57.8.tgz", "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-builtins/-/fs-node-builtins-4.64.0.tgz",
"integrity": "sha512-mxXSXw8zZwRVakcjLqR2I/psy4gURFSASZS10kKJ2kJw05GC2nXGroGrWVHxwgkxXgQLsFQnB74QaLzsxzdL/w==", "integrity": "sha512-/o7WRFhUWaM/fOrslwLZGnzn4RmRILykn+lAL+mNObqqRNw+CQSiij6hpCeZ+C7buhdoVo7go/OYqzaSUfDYmA==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"engines": { "engines": {
@ -33340,15 +33407,15 @@
} }
}, },
"node_modules/ts-checker-rspack-plugin/node_modules/@jsonjoy.com/fs-node-to-fsa": { "node_modules/ts-checker-rspack-plugin/node_modules/@jsonjoy.com/fs-node-to-fsa": {
"version": "4.57.8", "version": "4.64.0",
"resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-to-fsa/-/fs-node-to-fsa-4.57.8.tgz", "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-to-fsa/-/fs-node-to-fsa-4.64.0.tgz",
"integrity": "sha512-AWZcT/4+H+iDl4XCukbXrarvwEgOrf/prFI5/7eg4ix9FxqVsZysIDJd1Kjd+AjlCeHKHJOaRqjLd5HiGSCJEw==", "integrity": "sha512-WDD9WVs0hb7UAEKTgZW2f66WDrbj7gIIWwpP3spbLyXa0rghtUaFTB8L4gdR3ZCWwiKIsj38/CNijpVmpnuPUw==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@jsonjoy.com/fs-fsa": "4.57.8", "@jsonjoy.com/fs-fsa": "4.64.0",
"@jsonjoy.com/fs-node-builtins": "4.57.8", "@jsonjoy.com/fs-node-builtins": "4.64.0",
"@jsonjoy.com/fs-node-utils": "4.57.8" "@jsonjoy.com/fs-node-utils": "4.64.0"
}, },
"engines": { "engines": {
"node": ">=10.0" "node": ">=10.0"
@ -33362,13 +33429,14 @@
} }
}, },
"node_modules/ts-checker-rspack-plugin/node_modules/@jsonjoy.com/fs-node-utils": { "node_modules/ts-checker-rspack-plugin/node_modules/@jsonjoy.com/fs-node-utils": {
"version": "4.57.8", "version": "4.64.0",
"resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-utils/-/fs-node-utils-4.57.8.tgz", "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-utils/-/fs-node-utils-4.64.0.tgz",
"integrity": "sha512-E/bJ7sQAb4pu9nbeJhbULU3WnqWrswte4N9Js/oHt7aHB746S8/XBqKlcbrqIgnD3095XluovNEZuu5ONT230g==", "integrity": "sha512-k5Indsx9hWW9xSF7Y6oSKKwtCUNhzZxadub3owhIlitc+iMRVlPPdX2duTKQWBL3qNWpXya8jykgaaWpheeS4w==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@jsonjoy.com/fs-node-builtins": "4.57.8" "@jsonjoy.com/fs-node-builtins": "4.64.0",
"glob-to-regex.js": "^1.0.1"
}, },
"engines": { "engines": {
"node": ">=10.0" "node": ">=10.0"
@ -33382,13 +33450,13 @@
} }
}, },
"node_modules/ts-checker-rspack-plugin/node_modules/@jsonjoy.com/fs-print": { "node_modules/ts-checker-rspack-plugin/node_modules/@jsonjoy.com/fs-print": {
"version": "4.57.8", "version": "4.64.0",
"resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-print/-/fs-print-4.57.8.tgz", "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-print/-/fs-print-4.64.0.tgz",
"integrity": "sha512-DfzhOBpmvNu5P/KSe4NNQaOnvNliTdcf0qrh/4EReErF/XUQXYkd0vZl/OiJCm/qjEEo8DWRstliw2/JNS84dA==", "integrity": "sha512-PHZFccchvkhWrwPWHjmVAhbC3vSHCtyZvlZfJJ3ho2bnzl450hXri6/8e6pbkWdH+SkmLXNml0sV8e5HDAfxKw==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@jsonjoy.com/fs-node-utils": "4.57.8", "@jsonjoy.com/fs-node-utils": "4.64.0",
"tree-dump": "^1.1.0" "tree-dump": "^1.1.0"
}, },
"engines": { "engines": {
@ -33403,14 +33471,14 @@
} }
}, },
"node_modules/ts-checker-rspack-plugin/node_modules/@jsonjoy.com/fs-snapshot": { "node_modules/ts-checker-rspack-plugin/node_modules/@jsonjoy.com/fs-snapshot": {
"version": "4.57.8", "version": "4.64.0",
"resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-snapshot/-/fs-snapshot-4.57.8.tgz", "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-snapshot/-/fs-snapshot-4.64.0.tgz",
"integrity": "sha512-L+eqKaWOHLDaiMv1dh/EWQ4hA+o6xAhWSumTo3Teg7OM18jU/KE13/e8Mfal+eAZ/pSl4wIhKHcDiwapJzC8Wg==", "integrity": "sha512-oM7UDeL83q6NBzzsfKAsYKXKVXlykKFqqOLh4xZZKAzzROTlInkPbc6LTDGThEOnPiFiUzA7tYziHG9xavd76Q==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@jsonjoy.com/buffers": "^17.65.0", "@jsonjoy.com/buffers": "^17.65.0",
"@jsonjoy.com/fs-node-utils": "4.57.8", "@jsonjoy.com/fs-node-utils": "4.64.0",
"@jsonjoy.com/json-pack": "^17.65.0", "@jsonjoy.com/json-pack": "^17.65.0",
"@jsonjoy.com/util": "^17.65.0" "@jsonjoy.com/util": "^17.65.0"
}, },
@ -33560,20 +33628,20 @@
} }
}, },
"node_modules/ts-checker-rspack-plugin/node_modules/memfs": { "node_modules/ts-checker-rspack-plugin/node_modules/memfs": {
"version": "4.57.8", "version": "4.64.0",
"resolved": "https://registry.npmjs.org/memfs/-/memfs-4.57.8.tgz", "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.64.0.tgz",
"integrity": "sha512-bApYhn8BLpFAnAQmFfEl/NPN+8qx5Ar3V4Qt3ek23mVwBEElzV7c6XoPkb/PCG8ZFpowCEpHcPwMFTwHS7tSMA==", "integrity": "sha512-Kw72fgY7Wn+sD8KmtNWSafl1dz0UvAsE/PHs3YVfLiaZuA3HxNm9sRLqAu0ATiBGJvME1PxZXbBZPv5GycDeAw==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@jsonjoy.com/fs-core": "4.57.8", "@jsonjoy.com/fs-core": "4.64.0",
"@jsonjoy.com/fs-fsa": "4.57.8", "@jsonjoy.com/fs-fsa": "4.64.0",
"@jsonjoy.com/fs-node": "4.57.8", "@jsonjoy.com/fs-node": "4.64.0",
"@jsonjoy.com/fs-node-builtins": "4.57.8", "@jsonjoy.com/fs-node-builtins": "4.64.0",
"@jsonjoy.com/fs-node-to-fsa": "4.57.8", "@jsonjoy.com/fs-node-to-fsa": "4.64.0",
"@jsonjoy.com/fs-node-utils": "4.57.8", "@jsonjoy.com/fs-node-utils": "4.64.0",
"@jsonjoy.com/fs-print": "4.57.8", "@jsonjoy.com/fs-print": "4.64.0",
"@jsonjoy.com/fs-snapshot": "4.57.8", "@jsonjoy.com/fs-snapshot": "4.64.0",
"@jsonjoy.com/json-pack": "^1.11.0", "@jsonjoy.com/json-pack": "^1.11.0",
"@jsonjoy.com/util": "^1.9.0", "@jsonjoy.com/util": "^1.9.0",
"glob-to-regex.js": "^1.0.1", "glob-to-regex.js": "^1.0.1",

24
package.json

@ -107,7 +107,7 @@
"color": "5.0.3", "color": "5.0.3",
"cookie-parser": "1.4.7", "cookie-parser": "1.4.7",
"countries-and-timezones": "3.9.0", "countries-and-timezones": "3.9.0",
"countries-list": "3.3.0", "countries-list": "3.4.0",
"countup.js": "2.10.0", "countup.js": "2.10.0",
"date-fns": "4.4.0", "date-fns": "4.4.0",
"dotenv": "17.2.3", "dotenv": "17.2.3",
@ -161,16 +161,16 @@
"@eslint/js": "9.35.0", "@eslint/js": "9.35.0",
"@nestjs/schematics": "11.1.0", "@nestjs/schematics": "11.1.0",
"@nestjs/testing": "11.1.27", "@nestjs/testing": "11.1.27",
"@nx/angular": "23.0.1", "@nx/angular": "23.0.2",
"@nx/eslint-plugin": "23.0.1", "@nx/eslint-plugin": "23.0.2",
"@nx/jest": "23.0.1", "@nx/jest": "23.0.2",
"@nx/js": "23.0.1", "@nx/js": "23.0.2",
"@nx/module-federation": "23.0.1", "@nx/module-federation": "23.0.2",
"@nx/nest": "23.0.1", "@nx/nest": "23.0.2",
"@nx/node": "23.0.1", "@nx/node": "23.0.2",
"@nx/storybook": "23.0.1", "@nx/storybook": "23.0.2",
"@nx/web": "23.0.1", "@nx/web": "23.0.2",
"@nx/workspace": "23.0.1", "@nx/workspace": "23.0.2",
"@prisma/config": "7.8.0", "@prisma/config": "7.8.0",
"@schematics/angular": "21.2.6", "@schematics/angular": "21.2.6",
"@storybook/addon-docs": "10.1.10", "@storybook/addon-docs": "10.1.10",
@ -198,7 +198,7 @@
"jest": "30.3.0", "jest": "30.3.0",
"jest-environment-jsdom": "30.2.0", "jest-environment-jsdom": "30.2.0",
"jest-preset-angular": "16.0.0", "jest-preset-angular": "16.0.0",
"nx": "23.0.1", "nx": "23.0.2",
"prettier": "3.8.4", "prettier": "3.8.4",
"prettier-plugin-organize-attributes": "1.0.0", "prettier-plugin-organize-attributes": "1.0.0",
"prisma": "7.8.0", "prisma": "7.8.0",

Loading…
Cancel
Save