|
|
@ -773,6 +773,27 @@ describe('PortfolioService', () => { |
|
|
).getSummary(args); |
|
|
).getSummary(args); |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
function createExcludedActivity({ |
|
|
|
|
|
currency = 'CHF', |
|
|
|
|
|
quantity, |
|
|
|
|
|
symbol, |
|
|
|
|
|
type, |
|
|
|
|
|
unitPrice |
|
|
|
|
|
}: Pick<Activity, 'quantity' | 'type' | 'unitPrice'> & { |
|
|
|
|
|
currency?: string; |
|
|
|
|
|
symbol: string; |
|
|
|
|
|
}) { |
|
|
|
|
|
return { |
|
|
|
|
|
currency, |
|
|
|
|
|
quantity, |
|
|
|
|
|
type, |
|
|
|
|
|
unitPrice, |
|
|
|
|
|
account: { tags: [{ id: TAG_ID_EXCLUDE_FROM_ANALYSIS }] }, |
|
|
|
|
|
assetProfile: { currency, symbol, dataSource: DataSource.YAHOO }, |
|
|
|
|
|
tags: [] |
|
|
|
|
|
} as unknown as Activity; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
function createPortfolioCalculator() { |
|
|
function createPortfolioCalculator() { |
|
|
return { |
|
|
return { |
|
|
getDividendInBaseCurrency: jest.fn().mockResolvedValue(new Big(0)), |
|
|
getDividendInBaseCurrency: jest.fn().mockResolvedValue(new Big(0)), |
|
|
@ -791,9 +812,13 @@ describe('PortfolioService', () => { |
|
|
|
|
|
|
|
|
beforeEach(() => { |
|
|
beforeEach(() => { |
|
|
jest |
|
|
jest |
|
|
.spyOn(activitiesService, 'getActivities') |
|
|
.spyOn(activitiesService, 'getActivitiesForPortfolioCalculator') |
|
|
.mockResolvedValue({ activities: [], count: 0 }); |
|
|
.mockResolvedValue({ activities: [], count: 0 }); |
|
|
|
|
|
|
|
|
|
|
|
jest |
|
|
|
|
|
.spyOn(exchangeRateDataService, 'toCurrency') |
|
|
|
|
|
.mockImplementation((aValue) => aValue); |
|
|
|
|
|
|
|
|
jest.spyOn(portfolioService, 'getPerformance').mockResolvedValue({ |
|
|
jest.spyOn(portfolioService, 'getPerformance').mockResolvedValue({ |
|
|
performance: { |
|
|
performance: { |
|
|
currentValueInBaseCurrency: 3000, |
|
|
currentValueInBaseCurrency: 3000, |
|
|
@ -837,6 +862,177 @@ describe('PortfolioService', () => { |
|
|
expect(summary.totalAssetsInBaseCurrency).toBe(3000); |
|
|
expect(summary.totalAssetsInBaseCurrency).toBe(3000); |
|
|
expect(summary.totalValueInBaseCurrency).toBe(3000); |
|
|
expect(summary.totalValueInBaseCurrency).toBe(3000); |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
it('should value the open holdings of excluded accounts at the current market price', async () => { |
|
|
|
|
|
jest.spyOn(accountService, 'getCashDetails').mockResolvedValue({ |
|
|
|
|
|
accounts: [], |
|
|
|
|
|
balanceInBaseCurrency: 1500 |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
jest |
|
|
|
|
|
.spyOn(activitiesService, 'getActivitiesForPortfolioCalculator') |
|
|
|
|
|
.mockResolvedValue({ |
|
|
|
|
|
activities: [ |
|
|
|
|
|
createExcludedActivity({ |
|
|
|
|
|
quantity: 10, |
|
|
|
|
|
symbol: 'AAPL', |
|
|
|
|
|
type: 'BUY', |
|
|
|
|
|
unitPrice: 100 |
|
|
|
|
|
}), |
|
|
|
|
|
createExcludedActivity({ |
|
|
|
|
|
quantity: 4, |
|
|
|
|
|
symbol: 'AAPL', |
|
|
|
|
|
type: 'SELL', |
|
|
|
|
|
unitPrice: 150 |
|
|
|
|
|
}), |
|
|
|
|
|
createExcludedActivity({ |
|
|
|
|
|
quantity: 5, |
|
|
|
|
|
symbol: 'MSFT', |
|
|
|
|
|
type: 'BUY', |
|
|
|
|
|
unitPrice: 100 |
|
|
|
|
|
}), |
|
|
|
|
|
createExcludedActivity({ |
|
|
|
|
|
quantity: 5, |
|
|
|
|
|
symbol: 'MSFT', |
|
|
|
|
|
type: 'SELL', |
|
|
|
|
|
unitPrice: 120 |
|
|
|
|
|
}) |
|
|
|
|
|
], |
|
|
|
|
|
count: 4 |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
const getQuotes = jest |
|
|
|
|
|
.spyOn(dataProviderService, 'getQuotes') |
|
|
|
|
|
.mockResolvedValue({ |
|
|
|
|
|
'YAHOO-AAPL': { |
|
|
|
|
|
currency: 'CHF', |
|
|
|
|
|
dataSource: DataSource.YAHOO, |
|
|
|
|
|
marketPrice: 200, |
|
|
|
|
|
marketState: 'open' |
|
|
|
|
|
} |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
const summary = await getSummary({ |
|
|
|
|
|
balanceInBaseCurrency: 1000, |
|
|
|
|
|
emergencyFundHoldingsValueInBaseCurrency: 0, |
|
|
|
|
|
filteredValueInBaseCurrency: new Big(3000), |
|
|
|
|
|
portfolioCalculator: createPortfolioCalculator(), |
|
|
|
|
|
userCurrency: 'CHF', |
|
|
|
|
|
userId: userDummyData.id |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
// The closed holding (MSFT) does not need a quote
|
|
|
|
|
|
expect(getQuotes).toHaveBeenCalledWith( |
|
|
|
|
|
expect.objectContaining({ |
|
|
|
|
|
items: [{ dataSource: DataSource.YAHOO, symbol: 'AAPL' }] |
|
|
|
|
|
}) |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
// 500 (balance of excluded accounts) + 6 * 200 (AAPL) + 0 (MSFT)
|
|
|
|
|
|
expect(summary.excludedAccountsAndActivities).toBe(1700); |
|
|
|
|
|
expect(summary.totalValueInBaseCurrency).toBe(4700); |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
it('should convert the market value of an excluded holding from the currency of the asset profile to the user currency', async () => { |
|
|
|
|
|
jest.spyOn(accountService, 'getCashDetails').mockResolvedValue({ |
|
|
|
|
|
accounts: [], |
|
|
|
|
|
balanceInBaseCurrency: 1000 |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
jest |
|
|
|
|
|
.spyOn(activitiesService, 'getActivitiesForPortfolioCalculator') |
|
|
|
|
|
.mockResolvedValue({ |
|
|
|
|
|
activities: [ |
|
|
|
|
|
createExcludedActivity({ |
|
|
|
|
|
currency: 'USD', |
|
|
|
|
|
quantity: 10, |
|
|
|
|
|
symbol: 'AAPL', |
|
|
|
|
|
type: 'BUY', |
|
|
|
|
|
unitPrice: 100 |
|
|
|
|
|
}), |
|
|
|
|
|
createExcludedActivity({ |
|
|
|
|
|
currency: 'USD', |
|
|
|
|
|
quantity: 4, |
|
|
|
|
|
symbol: 'AAPL', |
|
|
|
|
|
type: 'SELL', |
|
|
|
|
|
unitPrice: 150 |
|
|
|
|
|
}) |
|
|
|
|
|
], |
|
|
|
|
|
count: 2 |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
jest.spyOn(dataProviderService, 'getQuotes').mockResolvedValue({ |
|
|
|
|
|
'YAHOO-AAPL': { |
|
|
|
|
|
currency: 'USD', |
|
|
|
|
|
dataSource: DataSource.YAHOO, |
|
|
|
|
|
marketPrice: 200, |
|
|
|
|
|
marketState: 'open' |
|
|
|
|
|
} |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
jest |
|
|
|
|
|
.spyOn(exchangeRateDataService, 'toCurrency') |
|
|
|
|
|
.mockImplementation((aValue, aFromCurrency, aToCurrency) => { |
|
|
|
|
|
return aFromCurrency === 'USD' && aToCurrency === 'CHF' |
|
|
|
|
|
? aValue * 0.8 |
|
|
|
|
|
: aValue; |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
const summary = await getSummary({ |
|
|
|
|
|
balanceInBaseCurrency: 1000, |
|
|
|
|
|
emergencyFundHoldingsValueInBaseCurrency: 0, |
|
|
|
|
|
filteredValueInBaseCurrency: new Big(3000), |
|
|
|
|
|
portfolioCalculator: createPortfolioCalculator(), |
|
|
|
|
|
userCurrency: 'CHF', |
|
|
|
|
|
userId: userDummyData.id |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
// 6 * 200 USD (AAPL) * 0.8 (USDCHF)
|
|
|
|
|
|
expect(summary.excludedAccountsAndActivities).toBe(960); |
|
|
|
|
|
expect(summary.totalValueInBaseCurrency).toBe(3960); |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
it('should fall back to the unit price of the latest activity of an excluded holding without a market price', async () => { |
|
|
|
|
|
jest.spyOn(accountService, 'getCashDetails').mockResolvedValue({ |
|
|
|
|
|
accounts: [], |
|
|
|
|
|
balanceInBaseCurrency: 1000 |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
jest |
|
|
|
|
|
.spyOn(activitiesService, 'getActivitiesForPortfolioCalculator') |
|
|
|
|
|
.mockResolvedValue({ |
|
|
|
|
|
activities: [ |
|
|
|
|
|
createExcludedActivity({ |
|
|
|
|
|
quantity: 10, |
|
|
|
|
|
symbol: 'AAPL', |
|
|
|
|
|
type: 'BUY', |
|
|
|
|
|
unitPrice: 100 |
|
|
|
|
|
}), |
|
|
|
|
|
createExcludedActivity({ |
|
|
|
|
|
quantity: 4, |
|
|
|
|
|
symbol: 'AAPL', |
|
|
|
|
|
type: 'SELL', |
|
|
|
|
|
unitPrice: 150 |
|
|
|
|
|
}) |
|
|
|
|
|
], |
|
|
|
|
|
count: 2 |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
jest.spyOn(dataProviderService, 'getQuotes').mockResolvedValue({}); |
|
|
|
|
|
|
|
|
|
|
|
const summary = await getSummary({ |
|
|
|
|
|
balanceInBaseCurrency: 1000, |
|
|
|
|
|
emergencyFundHoldingsValueInBaseCurrency: 0, |
|
|
|
|
|
filteredValueInBaseCurrency: new Big(3000), |
|
|
|
|
|
portfolioCalculator: createPortfolioCalculator(), |
|
|
|
|
|
userCurrency: 'CHF', |
|
|
|
|
|
userId: userDummyData.id |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
// 6 * 150 (unit price of the latest activity)
|
|
|
|
|
|
expect(summary.excludedAccountsAndActivities).toBe(900); |
|
|
|
|
|
expect(summary.totalValueInBaseCurrency).toBe(3900); |
|
|
|
|
|
}); |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
describe('getValueOfAccountsAndPlatforms', () => { |
|
|
describe('getValueOfAccountsAndPlatforms', () => { |
|
|
|