diff --git a/apps/api/src/app/portfolio/portfolio.service.spec.ts b/apps/api/src/app/portfolio/portfolio.service.spec.ts index 2d73bce3d..e0e7a8255 100644 --- a/apps/api/src/app/portfolio/portfolio.service.spec.ts +++ b/apps/api/src/app/portfolio/portfolio.service.spec.ts @@ -9,6 +9,7 @@ import { DataProviderService } from '@ghostfolio/api/services/data-provider/data import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; +import { UNKNOWN_KEY } from '@ghostfolio/common/config'; import { parseDate } from '@ghostfolio/common/helper'; import { Account, DataSource } from '@prisma/client'; @@ -108,6 +109,67 @@ describe('PortfolioService', () => { ); }); + describe('getAggregatedMarkets', () => { + const getAggregatedMarkets = (holdings: object) => { + return ( + portfolioService as unknown as { + getAggregatedMarkets: (aHoldings: object) => { + markets: Record< + string, + { valueInBaseCurrency: number; valueInPercentage: number } + >; + marketsAdvanced: Record; + }; + } + ).getAggregatedMarkets(holdings); + }; + + it('should distribute holdings with countries to their market and route holdings without countries (e.g. commodities, cryptocurrencies) to the unknown bucket', () => { + const holdings = { + 'GC=F': { + // Gold + assetProfile: { countries: [] }, + markets: { developedMarkets: 0, emergingMarkets: 0, otherMarkets: 0 }, + marketsAdvanced: { + asiaPacific: 0, + emergingMarkets: 0, + europe: 0, + japan: 0, + northAmerica: 0, + otherMarkets: 0 + }, + valueInBaseCurrency: 500 + }, + MSFT: { + assetProfile: { countries: [{ code: 'US', weight: 1 }] }, + markets: { developedMarkets: 1, emergingMarkets: 0, otherMarkets: 0 }, + marketsAdvanced: { + asiaPacific: 0, + emergingMarkets: 0, + europe: 0, + japan: 0, + northAmerica: 1, + otherMarkets: 0 + }, + valueInBaseCurrency: 1000 + } + }; + + const { markets, marketsAdvanced } = getAggregatedMarkets(holdings); + + expect(markets.developedMarkets.valueInBaseCurrency).toBe(1000); + expect(markets[UNKNOWN_KEY].valueInBaseCurrency).toBe(500); + + expect(markets.developedMarkets.valueInPercentage).toBeCloseTo( + 1000 / 1500 + ); + expect(markets[UNKNOWN_KEY].valueInPercentage).toBeCloseTo(500 / 1500); + + expect(marketsAdvanced.northAmerica.valueInBaseCurrency).toBe(1000); + expect(marketsAdvanced[UNKNOWN_KEY].valueInBaseCurrency).toBe(500); + }); + }); + describe('getCashSymbolProfiles', () => { it('should use the exchange-rate data source so the symbol-profile join in getDetails matches the calculator positions', () => { jest @@ -271,4 +333,96 @@ describe('PortfolioService', () => { expect(holdings['USD'].assetProfile.symbol).toBe('USD'); }); }); + + describe('getValueOfAccountsAndPlatforms', () => { + const getValueOfAccountsAndPlatforms = (args: object) => { + return ( + portfolioService as unknown as { + getValueOfAccountsAndPlatforms: (aArgs: object) => Promise<{ + accounts: Record; + platforms: Record; + }>; + } + ).getValueOfAccountsAndPlatforms(args); + }; + + const account = { + balance: 100, + currency: 'USD', + id: randomUUID(), + isExcluded: false, + name: 'Account 1', + platform: { name: 'Platform 1' }, + platformId: randomUUID() + }; + + beforeEach(() => { + jest + .spyOn(accountService, 'getAccounts') + .mockResolvedValue([account] as unknown as Account[]); + + jest + .spyOn(exchangeRateDataService, 'toCurrency') + .mockImplementation((aValue) => aValue); + }); + + it('should group activities without an account into the unknown bucket of accounts and platforms', async () => { + const { accounts, platforms } = await getValueOfAccountsAndPlatforms({ + activities: [ + { + account, + accountId: account.id, + quantity: 1, + SymbolProfile: { symbol: 'AAPL' }, + type: 'BUY' + }, + { + account: null, + accountId: null, + quantity: 2, + SymbolProfile: { symbol: 'BABA' }, + type: 'BUY' + } + ], + filters: [], + portfolioItemsNow: { + AAPL: { marketPriceInBaseCurrency: 10 }, + BABA: { marketPriceInBaseCurrency: 20 } + }, + userCurrency: 'USD', + userId: userDummyData.id + }); + + // 100 (balance) + 1 * 10 (activity) + expect(accounts[account.id].valueInBaseCurrency).toBe(110); + expect(platforms[account.platformId].valueInBaseCurrency).toBe(110); + + // 2 * 20 (activity without an account) + expect(accounts[UNKNOWN_KEY].valueInBaseCurrency).toBe(40); + expect(platforms[UNKNOWN_KEY].valueInBaseCurrency).toBe(40); + }); + + it('should not create an unknown bucket when every activity has an account', async () => { + const { accounts, platforms } = await getValueOfAccountsAndPlatforms({ + activities: [ + { + account, + accountId: account.id, + quantity: 1, + SymbolProfile: { symbol: 'AAPL' }, + type: 'BUY' + } + ], + filters: [], + portfolioItemsNow: { + AAPL: { marketPriceInBaseCurrency: 10 } + }, + userCurrency: 'USD', + userId: userDummyData.id + }); + + expect(accounts[UNKNOWN_KEY]).toBeUndefined(); + expect(platforms[UNKNOWN_KEY]).toBeUndefined(); + }); + }); });