feat(portfolio): store and publish instrument sector and issuer (WealthLedger/main#48) (#148)
This commit was merged in pull request #148.
This commit is contained in:
@@ -6,9 +6,10 @@ import { PortfolioDto } from '../presentation/portfolio.dto';
|
||||
import { PortfolioService } from './portfolio.service';
|
||||
|
||||
describe('persistent portfolio projection', () => {
|
||||
const access = { local: jest.fn(), tokenForAttempt: jest.fn() };
|
||||
const access = { local: jest.fn(), tokenForAttempt: jest.fn(), rejectMembershipToken: jest.fn() };
|
||||
const tbank = { getPortfolio: jest.fn(), getPositions: jest.fn() };
|
||||
const metadata = { savedMany: jest.fn(), request: jest.fn() };
|
||||
const classification = { refresh: jest.fn() };
|
||||
const repository = {
|
||||
read: jest.fn(),
|
||||
reserve: jest.fn(),
|
||||
@@ -23,6 +24,7 @@ describe('persistent portfolio projection', () => {
|
||||
access as never,
|
||||
tbank as never,
|
||||
metadata as never,
|
||||
classification as never,
|
||||
repository as never,
|
||||
{ request: jest.fn().mockResolvedValue(null) } as never,
|
||||
{ event: jest.fn() } as never,
|
||||
@@ -34,6 +36,7 @@ describe('persistent portfolio projection', () => {
|
||||
accounts: [{ accountId: 'a' }, { accountId: 'b' }],
|
||||
});
|
||||
access.tokenForAttempt.mockResolvedValue('synthetic');
|
||||
access.rejectMembershipToken.mockResolvedValue(undefined);
|
||||
repository.reserve.mockResolvedValue(null);
|
||||
repository.read.mockResolvedValue({
|
||||
membershipAt,
|
||||
@@ -44,6 +47,37 @@ describe('persistent portfolio projection', () => {
|
||||
repository.publish.mockResolvedValue(true);
|
||||
metadata.savedMany.mockResolvedValue(new Map());
|
||||
metadata.request.mockResolvedValue(null);
|
||||
classification.refresh.mockResolvedValue(new Map());
|
||||
});
|
||||
it('refreshes classification before publishing and passes the hourly walk time', async () => {
|
||||
const walkStartedAt = new Date('2026-10-31T12:00:00Z');
|
||||
const position = { instrumentType: 'share', instrumentUid: 'uid', figi: 'figi', quantity: { units: '1' } };
|
||||
repository.reserve.mockResolvedValue({ launch: true, work: { id: 'work', attemptId: 'attempt' } });
|
||||
tbank.getPortfolio.mockResolvedValue({ positions: [position] });
|
||||
tbank.getPositions.mockResolvedValue({ money: [] });
|
||||
classification.refresh.mockResolvedValue(new Map([[JSON.stringify(['uid', 'figi']), {
|
||||
sector: 'Energy', issuer: { id: 'issuer', name: 'Issuer' },
|
||||
}]]));
|
||||
const handle = await service.request('user', 'private', 'a', true, walkStartedAt);
|
||||
assert(handle);
|
||||
await handle.done;
|
||||
expect(classification.refresh).toHaveBeenCalledWith('synthetic', [position], walkStartedAt);
|
||||
expect(classification.refresh.mock.invocationCallOrder[0]).toBeLessThan(repository.publish.mock.invocationCallOrder[0]);
|
||||
const snapshot = (repository.publish.mock.calls as [unknown, string, PortfolioDto][])[0][2];
|
||||
expect(snapshot.positions[0]).toEqual(expect.objectContaining({
|
||||
sector: 'Energy', issuer: { id: 'issuer', name: 'Issuer' },
|
||||
}));
|
||||
});
|
||||
it('rejects an invalid classification token without publishing the snapshot', async () => {
|
||||
repository.reserve.mockResolvedValue({ launch: true, work: { id: 'work', attemptId: 'attempt' } });
|
||||
tbank.getPortfolio.mockResolvedValue({ positions: [] });
|
||||
tbank.getPositions.mockResolvedValue({ money: [] });
|
||||
classification.refresh.mockRejectedValue(new Error('INVALID_TBANK_TOKEN'));
|
||||
const handle = await service.request('user', 'private', 'a');
|
||||
assert(handle);
|
||||
await handle.done;
|
||||
expect(repository.publish).not.toHaveBeenCalled();
|
||||
expect(access.rejectMembershipToken).toHaveBeenCalled();
|
||||
});
|
||||
it('normalizes exact signed money, order, UID metadata and unknown types without metadata RPC', async () => {
|
||||
repository.reserve.mockResolvedValueOnce({
|
||||
|
||||
Reference in New Issue
Block a user