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
### Added
- Added the markets endpoint for the _Fear & Greed Index_ (market mood) to the `GHOSTFOLIO` data provider
### 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
- 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

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

@ -455,6 +455,23 @@ export class ActivitiesService {
userId,
accountId: account.id,
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,
createdAt: new Date(balanceItem.date),
currency: account.currency,
@ -823,6 +840,7 @@ export class ActivitiesService {
return {
...order,
assetProfile,
feeInAssetProfileCurrency,
feeInBaseCurrency,
unitPriceInAssetProfileCurrency,

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

@ -8,6 +8,7 @@ import {
DividendsResponse,
HistoricalResponse,
LookupResponse,
MarketDataOfMarketsResponse,
QuotesResponse
} from '@ghostfolio/common/interfaces';
import { permissions } from '@ghostfolio/common/permissions';
@ -19,6 +20,7 @@ import {
HttpException,
Inject,
Param,
ParseIntPipe,
Query,
UseGuards,
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')
@HasPermission(permissions.enableDataProviderGhostfolio)
@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 { SymbolModule } from '@ghostfolio/api/app/symbol/symbol.module';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { CryptocurrencyModule } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.module';
import { AlphaVantageService } from '@ghostfolio/api/services/data-provider/alpha-vantage/alpha-vantage.service';
@ -33,6 +34,7 @@ import { GhostfolioService } from './ghostfolio.service';
PrismaModule,
PropertyModule,
RedisCacheModule,
SymbolModule,
SymbolProfileModule
],
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 { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { GhostfolioService as GhostfolioDataProviderService } from '@ghostfolio/api/services/data-provider/ghostfolio/ghostfolio.service';
@ -25,6 +26,7 @@ import {
HistoricalResponse,
LookupItem,
LookupResponse,
MarketDataOfMarketsResponse,
QuotesResponse
} from '@ghostfolio/common/interfaces';
import { UserWithSettings } from '@ghostfolio/common/types';
@ -32,6 +34,7 @@ import { UserWithSettings } from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common';
import { DataSource, SymbolProfile } from '@prisma/client';
import { Big } from 'big.js';
import { isEmpty } from 'lodash';
@Injectable()
export class GhostfolioService {
@ -42,7 +45,8 @@ export class GhostfolioService {
private readonly dataProviderService: DataProviderService,
private readonly fetchService: FetchService,
private readonly prismaService: PrismaService,
private readonly propertyService: PropertyService
private readonly propertyService: PropertyService,
private readonly symbolService: SymbolService
) {}
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() {
return parseInt(
(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 { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
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 { 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 { getCurrencyFromSymbol, isCurrency } from '@ghostfolio/common/helper';
import { MarketDataOfMarketsResponse } from '@ghostfolio/common/interfaces';
@ -36,7 +30,6 @@ import { getReasonPhrase, StatusCodes } from 'http-status-codes';
@Controller('market-data')
export class MarketDataController {
public constructor(
private readonly dataProviderService: DataProviderService,
private readonly marketDataService: MarketDataService,
@Inject(REQUEST) private readonly request: RequestWithUser,
private readonly symbolProfileService: SymbolProfileService,
@ -50,39 +43,9 @@ export class MarketDataController {
@Query('includeHistoricalData', new ParseIntPipe({ optional: true }))
includeHistoricalData = 0
): Promise<MarketDataOfMarketsResponse> {
const [
marketDataFearAndGreedIndexCryptocurrencies,
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
}
}
};
return this.symbolService.getMarketDataOfMarkets({
includeHistoricalData
});
}
@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 { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.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';
@ -9,11 +8,6 @@ import { MarketDataController } from './market-data.controller';
@Module({
controllers: [MarketDataController],
imports: [
DataProviderModule,
MarketDataServiceModule,
SymbolModule,
SymbolProfileModule
]
imports: [MarketDataServiceModule, SymbolModule, SymbolProfileModule]
})
export class MarketDataModule {}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -829,10 +829,9 @@ export class PortfolioService {
timeWeightedInvestmentWithCurrencyEffect
} = holding;
const activitiesOfHolding = activities.filter(({ SymbolProfile }) => {
const activitiesOfHolding = activities.filter(({ assetProfile }) => {
return (
SymbolProfile.dataSource === dataSource &&
SymbolProfile.symbol === symbol
assetProfile.dataSource === dataSource && assetProfile.symbol === symbol
);
});
@ -2056,11 +2055,11 @@ export class PortfolioService {
.filter(({ isDraft, type }) => {
return isDraft === false && type === activityType;
})
.map(({ currency, quantity, SymbolProfile, unitPrice }) => {
.map(({ assetProfile, currency, quantity, unitPrice }) => {
return new Big(
this.exchangeRateDataService.toCurrency(
new Big(quantity).mul(unitPrice).toNumber(),
currency ?? SymbolProfile.currency,
currency ?? assetProfile.currency,
userCurrency
)
);
@ -2198,16 +2197,11 @@ export class PortfolioService {
}
}
for (const {
account,
quantity,
SymbolProfile,
type
} of ordersByAccount) {
for (const { account, assetProfile, quantity, type } of ordersByAccount) {
const currentValueOfSymbolInBaseCurrency =
getFactor(type) *
quantity *
(portfolioItemsNow[SymbolProfile.symbol]?.marketPriceInBaseCurrency ??
(portfolioItemsNow[assetProfile.symbol]?.marketPriceInBaseCurrency ??
0);
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 { DataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import {
ghostfolioFearAndGreedIndexDataSourceCryptocurrencies,
ghostfolioFearAndGreedIndexSymbolCryptocurrencies,
ghostfolioFearAndGreedIndexSymbolStocks
} from '@ghostfolio/common/config';
import {
DATE_FORMAT,
getAssetProfileIdentifier
@ -9,6 +14,7 @@ import {
DataProviderHistoricalResponse,
HistoricalDataItem,
LookupResponse,
MarketDataOfMarketsResponse,
SymbolItem
} from '@ghostfolio/common/interfaces';
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({
includeIndices = false,
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,
object: data,
paths: [
'activities[*].assetProfile.dataSource',
'activities[*].dataSource',
/* @deprecated */
'activities[*].SymbolProfile.dataSource',
'assetProfile.dataSource',
'benchmarks[*].dataSource',
'errors[*].dataSource',
@ -88,7 +92,10 @@ export class TransformDataSourceInResponseInterceptor<
'holdings[*].assetProfile.dataSource',
'holdings[*].dataSource',
'items[*].dataSource',
/* @deprecated */
'SymbolProfile.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
);
let status = `Current market mood is ${emoji} ${text.toLowerCase()} (${
let status = `Current market mood is ${emoji} ${text.toLowerCase()} (${round(
symbolItem.marketPrice
}/100)`;
)}/100)`;
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 {
HEADER_KEY_SKIP_INTERCEPTOR,
HEADER_KEY_TOKEN
@ -10,6 +11,7 @@ import {
DividendsResponse,
HistoricalResponse,
LookupResponse,
MarketDataOfMarketsResponse,
QuotesResponse
} from '@ghostfolio/common/interfaces';
@ -32,7 +34,12 @@ import { FetchFailure, FetchResult } from './interfaces/interfaces';
@Component({
host: { class: 'page' },
imports: [CommonModule, MatCardModule, NgxSkeletonLoaderModule],
imports: [
CommonModule,
GfFearAndGreedIndexComponent,
MatCardModule,
NgxSkeletonLoaderModule
],
selector: 'gf-api-page',
styleUrls: ['./api-page.scss'],
templateUrl: './api-page.html'
@ -48,6 +55,9 @@ export class GfApiPageComponent implements OnInit {
>;
protected isinLookupItems$: Observable<FetchResult<LookupResponse['items']>>;
protected lookupItems$: Observable<FetchResult<LookupResponse['items']>>;
protected marketDataOfMarkets$: Observable<
FetchResult<MarketDataOfMarketsResponse>
>;
protected quotes$: Observable<FetchResult<QuotesResponse['quotes']>>;
protected status$: Observable<
FetchResult<DataProviderGhostfolioStatusResponse>
@ -68,6 +78,7 @@ export class GfApiPageComponent implements OnInit {
this.historicalData$ = this.fetchHistoricalData({ symbol: 'AAPL' });
this.isinLookupItems$ = this.fetchLookupItems({ query: 'US0378331005' });
this.lookupItems$ = this.fetchLookupItems({ query: 'apple' });
this.marketDataOfMarkets$ = this.fetchMarketDataOfMarkets();
this.quotes$ = this.fetchQuotes({ symbols: ['AAPL', 'VOO'] });
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[] }) {
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>
<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>

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

@ -409,10 +409,10 @@ export class GfActivitiesPageComponent implements OnInit {
activity: {
...aActivity,
accountId: aActivity?.accountId,
assetProfile: null,
date: new Date(),
id: null,
fee: 0,
SymbolProfile: null,
type: aActivity?.type ?? 'BUY',
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() {
this.currencyOfAssetProfile = this.data.activity?.SymbolProfile?.currency;
this.currencyOfAssetProfile = this.data.activity?.assetProfile?.currency;
this.hasPermissionToCreateOwnTag =
this.data.user?.settings?.isExperimentalFeatures &&
hasPermission(this.data.user?.permissions, permissions.createOwnTag);
@ -185,31 +185,31 @@ export class GfCreateOrUpdateActivityDialogComponent {
? this.data.accounts[0].id
: this.data.activity?.accountId
],
assetClass: [this.data.activity?.SymbolProfile?.assetClass],
assetSubClass: [this.data.activity?.SymbolProfile?.assetSubClass],
assetClass: [this.data.activity?.assetProfile?.assetClass],
assetSubClass: [this.data.activity?.assetProfile?.assetSubClass],
comment: [this.data.activity?.comment],
currency: [
this.data.activity?.SymbolProfile?.currency,
this.data.activity?.assetProfile?.currency,
Validators.required
],
currencyOfUnitPrice: [
this.data.activity?.currency ??
this.data.activity?.SymbolProfile?.currency,
this.data.activity?.assetProfile?.currency,
Validators.required
],
dataSource: [
this.data.activity?.SymbolProfile?.dataSource,
this.data.activity?.assetProfile?.dataSource,
Validators.required
],
date: [this.data.activity?.date, 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],
searchSymbol: [
this.data.activity?.SymbolProfile
this.data.activity?.assetProfile
? {
dataSource: this.data.activity?.SymbolProfile?.dataSource,
symbol: this.data.activity?.SymbolProfile?.symbol
dataSource: this.data.activity?.assetProfile?.dataSource,
symbol: this.data.activity?.assetProfile?.symbol
}
: null,
Validators.required
@ -310,7 +310,7 @@ export class GfCreateOrUpdateActivityDialogComponent {
this.activityForm.get('searchSymbol')?.valueChanges.subscribe(() => {
if (this.activityForm.get('searchSymbol')?.invalid) {
this.data.activity.SymbolProfile = null;
this.data.activity.assetProfile = null;
} else if (
['BUY', 'DIVIDEND', 'SELL'].includes(
this.activityForm.get('type')?.value
@ -454,11 +454,11 @@ export class GfCreateOrUpdateActivityDialogComponent {
this.activityForm.get('type')?.disable();
}
if (this.data.activity?.SymbolProfile?.symbol) {
if (this.data.activity?.assetProfile?.symbol) {
this.dataService
.fetchSymbolItem({
dataSource: this.data.activity?.SymbolProfile?.dataSource,
symbol: this.data.activity?.SymbolProfile?.symbol
dataSource: this.data.activity?.assetProfile?.dataSource,
symbol: this.data.activity?.assetProfile?.symbol
})
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ marketPrice }) => {
@ -569,7 +569,7 @@ export class GfCreateOrUpdateActivityDialogComponent {
})
.pipe(
catchError(() => {
this.data.activity.SymbolProfile = null;
this.data.activity.assetProfile = null;
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 {
accounts: Account[];
activity: Partial<Omit<Activity, 'id' | 'SymbolProfile' | 'unitPrice'>> & {
activity: Partial<Omit<Activity, 'assetProfile' | 'id' | 'unitPrice'>> & {
assetProfile: Activity['assetProfile'] | null;
id: string | null;
SymbolProfile: Activity['SymbolProfile'] | null;
unitPrice: number | null;
};
user: User;

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

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

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

@ -106,13 +106,20 @@ export const DEFAULT_REDACTED_PATHS = [
'accounts[*].valueInBaseCurrency',
'activities[*].account.balance',
'activities[*].account.comment',
'activities[*].assetProfile.symbolMapping',
'activities[*].assetProfile.watchedByCount',
'activities[*].comment',
'activities[*].fee',
'activities[*].feeInAssetProfileCurrency',
'activities[*].feeInBaseCurrency',
'activities[*].quantity',
/* @deprecated */
'activities[*].SymbolProfile.symbolMapping',
/* @deprecated */
'activities[*].SymbolProfile.watchedByCount',
'activities[*].value',
'activities[*].valueInBaseCurrency',
'balance',
@ -143,8 +150,13 @@ export const DEFAULT_REDACTED_PATHS = [
'platforms[*].balance',
'platforms[*].valueInBaseCurrency',
'quantity',
/* @deprecated */
'SymbolProfile.symbolMapping',
/* @deprecated */
'SymbolProfile.watchedByCount',
'totalBalanceInBaseCurrency',
'totalDividendInBaseCurrency',
'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 { AssetClass, AssetSubClass, DataSource, Prisma } from '@prisma/client';
import { Type } from 'class-transformer';
import {
IsArray,
IsBoolean,
IsEnum,
IsOptional,
IsString,
IsUrl
IsUrl,
ValidateNested
} from 'class-validator';
import { CountryDto } from './country.dto';
import { HoldingDto } from './holding.dto';
import { SectorDto } from './sector.dto';
export class CreateAssetProfileDto {
@IsEnum(AssetClass)
@IsOptional()
@ -25,6 +31,8 @@ export class CreateAssetProfileDto {
@IsArray()
@IsOptional()
@Type(() => CountryDto)
@ValidateNested({ each: true })
countries?: Prisma.InputJsonArray;
@IsCurrencyCode()
@ -51,6 +59,8 @@ export class CreateAssetProfileDto {
@IsArray()
@IsOptional()
@Type(() => HoldingDto)
@ValidateNested({ each: true })
holdings?: Prisma.InputJsonArray;
@IsBoolean()
@ -67,6 +77,8 @@ export class CreateAssetProfileDto {
@IsArray()
@IsOptional()
@Type(() => SectorDto)
@ValidateNested({ each: true })
sectors?: Prisma.InputJsonArray;
@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 { CountryDto } from './country.dto';
import { CreateAccessDto } from './create-access.dto';
import { CreateAccountBalanceDto } from './create-account-balance.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 { CreateWatchlistItemDto } from './create-watchlist-item.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 { UpdateAccessDto } from './update-access.dto';
import { UpdateAccountDto } from './update-account.dto';
@ -26,6 +30,7 @@ import { UpdateUserSettingDto } from './update-user-setting.dto';
export {
AuthDeviceDto,
CountryDto,
CreateAccessDto,
CreateAccountBalanceDto,
CreateAccountDto,
@ -37,6 +42,9 @@ export {
CreateTagDto,
CreateWatchlistItemDto,
DeleteOwnUserDto,
HoldingDto,
ScraperConfigurationDto,
SectorDto,
TransferBalanceDto,
UpdateAccessDto,
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,
Prisma
} from '@prisma/client';
import { Type } from 'class-transformer';
import {
IsArray,
IsBoolean,
@ -14,9 +15,15 @@ import {
IsObject,
IsOptional,
IsString,
IsUrl
IsUrl,
ValidateNested
} 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 {
@IsEnum(AssetClass)
@IsOptional()
@ -32,6 +39,8 @@ export class UpdateAssetProfileDto {
@IsArray()
@IsOptional()
@Type(() => CountryDto)
@ValidateNested({ each: true })
countries?: Prisma.InputJsonArray;
@IsCurrencyCode()
@ -48,6 +57,8 @@ export class UpdateAssetProfileDto {
@IsArray()
@IsOptional()
@Type(() => HoldingDto)
@ValidateNested({ each: true })
holdings?: Prisma.InputJsonArray;
@IsBoolean()
@ -60,10 +71,14 @@ export class UpdateAssetProfileDto {
@IsObject()
@IsOptional()
@Type(() => ScraperConfigurationDto)
@ValidateNested()
scraperConfiguration?: Prisma.InputJsonObject;
@IsArray()
@IsOptional()
@Type(() => SectorDto)
@ValidateNested({ each: true })
sectors?: Prisma.InputJsonArray;
@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 {
account?: AccountWithPlatform;
assetProfile: EnhancedSymbolProfile;
error?: ActivityError;
feeInAssetProfileCurrency: number;
feeInBaseCurrency: number;
SymbolProfile: EnhancedSymbolProfile;
/**
* @deprecated Use `assetProfile` instead
*/
SymbolProfile?: EnhancedSymbolProfile;
tagIds?: string[];
tags?: Tag[];
unitPriceInAssetProfileCurrency: number;

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

@ -26,7 +26,13 @@ export interface PublicPortfolioResponse extends PublicPortfolioResponseV1 {
Order,
'currency' | 'date' | 'fee' | 'quantity' | 'type' | 'unitPrice'
> & {
assetProfile?: EnhancedSymbolProfile;
/**
* @deprecated Use `assetProfile` instead
*/
SymbolProfile?: EnhancedSymbolProfile;
value: 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>
<td *matCellDef="let element" class="px-1 text-center" mat-cell>
<gf-entity-logo
[dataSource]="element.SymbolProfile?.dataSource"
[symbol]="element.SymbolProfile?.symbol"
[tooltip]="element.SymbolProfile?.name"
[dataSource]="element.assetProfile?.dataSource"
[symbol]="element.assetProfile?.symbol"
[tooltip]="element.assetProfile?.name"
/>
</td>
</ng-container>
@ -162,18 +162,18 @@
</th>
<td *matCellDef="let element" class="px-1" mat-cell>
<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) {
<span class="badge badge-secondary ml-1" i18n>Draft</span>
}
</div>
@if (
element.SymbolProfile?.dataSource !== 'MANUAL' &&
!isUUID(element.SymbolProfile?.symbol)
element.assetProfile?.dataSource !== 'MANUAL' &&
!isUUID(element.assetProfile?.symbol)
) {
<div>
<small class="text-muted">{{
element.SymbolProfile?.symbol | gfSymbol
element.assetProfile?.symbol | gfSymbol
}}</small>
</div>
}
@ -309,7 +309,7 @@
class="d-none d-lg-table-cell px-1"
mat-cell
>
{{ element.currency ?? element.SymbolProfile?.currency }}
{{ element.currency ?? element.assetProfile?.currency }}
</td>
</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'
}
},
SymbolProfile: {
assetProfile: {
assetClass: 'EQUITY',
assetSubClass: 'ETF',
comment: undefined,
@ -123,7 +123,7 @@ const activities: Activity[] = [
url: 'https://interactivebrokers.com'
}
},
SymbolProfile: {
assetProfile: {
assetClass: 'EQUITY',
assetSubClass: 'ETF',
comment: undefined,
@ -190,7 +190,7 @@ const activities: Activity[] = [
url: 'https://interactivebrokers.com'
}
},
SymbolProfile: {
assetProfile: {
assetClass: 'LIQUIDITY',
assetSubClass: 'CRYPTOCURRENCY',
comment: undefined,
@ -257,7 +257,7 @@ const activities: Activity[] = [
url: 'https://interactivebrokers.com'
}
},
SymbolProfile: {
assetProfile: {
assetClass: 'FIXED_INCOME',
assetSubClass: 'BOND',
comment: 'No data',
@ -324,7 +324,7 @@ const activities: Activity[] = [
url: 'https://interactivebrokers.com'
}
},
SymbolProfile: {
assetProfile: {
assetClass: 'EQUITY',
assetSubClass: 'ETF',
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)) {
this.activityClicked.emit({
dataSource: activity.SymbolProfile.dataSource,
symbol: activity.SymbolProfile.symbol
dataSource: activity.assetProfile.dataSource,
symbol: activity.assetProfile.symbol
});
}
}

536
package-lock.json

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

24
package.json

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

Loading…
Cancel
Save