156 lines
4.9 KiB
TypeScript
156 lines
4.9 KiB
TypeScript
import { PrismaService } from '../../prisma/prisma.service';
|
|
import { BrokerOperationSyncService } from './broker-operation-sync.service';
|
|
import { BrokerOperationsService } from './broker-operations.service';
|
|
|
|
describe('BrokerOperationSyncService', () => {
|
|
const operations = { getOperations: vi.fn() } as unknown as BrokerOperationsService;
|
|
const prisma = {
|
|
brokerOperation: { upsert: vi.fn() },
|
|
brokerOperationSyncState: { upsert: vi.fn() },
|
|
} as unknown as PrismaService;
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it('syncs operation pages and stores raw payload', async () => {
|
|
vi.mocked(operations.getOperations)
|
|
.mockResolvedValueOnce({
|
|
data: {
|
|
accountId: 'acc-1',
|
|
hasNext: true,
|
|
nextCursor: 'next',
|
|
asOf: '2026-06-16T00:00:00.000Z',
|
|
items: [
|
|
{
|
|
cursor: 'c1',
|
|
accountId: 'acc-1',
|
|
id: 'op-1',
|
|
parentOperationId: null,
|
|
date: '2026-06-16T00:00:00.000Z',
|
|
type: 'OPERATION_TYPE_BUY',
|
|
category: 'trade',
|
|
description: null,
|
|
state: 'OPERATION_STATE_EXECUTED',
|
|
instrumentUid: 'uid-1',
|
|
figi: null,
|
|
ticker: 'SBER',
|
|
classCode: 'TQBR',
|
|
instrumentType: 'share',
|
|
payment: { currency: 'RUB', units: '-1000', nano: 0, value: -1000 },
|
|
price: null,
|
|
commission: null,
|
|
yield: null,
|
|
accruedInt: null,
|
|
quantity: 10,
|
|
quantityDone: 10,
|
|
},
|
|
],
|
|
},
|
|
meta: { fromCache: false, cachedAt: null },
|
|
})
|
|
.mockResolvedValueOnce({
|
|
data: { accountId: 'acc-1', hasNext: false, nextCursor: null, asOf: 'now', items: [] },
|
|
meta: { fromCache: false, cachedAt: null },
|
|
});
|
|
|
|
const service = new BrokerOperationSyncService(operations, prisma);
|
|
const result = await service.syncAccount('acc-1', {
|
|
from: '2026-06-01T00:00:00.000Z',
|
|
to: '2026-06-16T00:00:00.000Z',
|
|
});
|
|
|
|
expect(result.upserted).toBe(1);
|
|
expect(operations.getOperations).toHaveBeenNthCalledWith(2, 'acc-1', {
|
|
from: '2026-06-01T00:00:00.000Z',
|
|
to: '2026-06-16T00:00:00.000Z',
|
|
cursor: 'next',
|
|
limit: 1000,
|
|
state: 'OPERATION_STATE_EXECUTED',
|
|
});
|
|
expect(prisma.brokerOperation.upsert).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
where: { accountId_cursor: { accountId: 'acc-1', cursor: 'c1' } },
|
|
}),
|
|
);
|
|
expect(prisma.brokerOperationSyncState.upsert).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
update: expect.objectContaining({ lastCursor: 'next' }),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('stores operations under the synced account id', async () => {
|
|
vi.mocked(operations.getOperations).mockResolvedValueOnce({
|
|
data: {
|
|
accountId: 'acc-1',
|
|
hasNext: false,
|
|
nextCursor: null,
|
|
asOf: '2026-06-16T00:00:00.000Z',
|
|
items: [
|
|
{
|
|
cursor: 'c1',
|
|
accountId: 'unexpected-account',
|
|
id: 'op-1',
|
|
parentOperationId: null,
|
|
date: null,
|
|
type: 'OPERATION_TYPE_BUY',
|
|
category: 'trade',
|
|
description: null,
|
|
state: null,
|
|
instrumentUid: null,
|
|
figi: null,
|
|
ticker: null,
|
|
classCode: null,
|
|
instrumentType: null,
|
|
payment: null,
|
|
price: null,
|
|
commission: null,
|
|
yield: null,
|
|
accruedInt: null,
|
|
quantity: null,
|
|
quantityDone: null,
|
|
},
|
|
],
|
|
},
|
|
meta: { fromCache: false, cachedAt: null },
|
|
});
|
|
|
|
const service = new BrokerOperationSyncService(operations, prisma);
|
|
await service.syncAccount('acc-1', {
|
|
from: '2026-06-01T00:00:00.000Z',
|
|
to: '2026-06-16T00:00:00.000Z',
|
|
});
|
|
|
|
expect(prisma.brokerOperation.upsert).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
where: { accountId_cursor: { accountId: 'acc-1', cursor: 'c1' } },
|
|
create: expect.objectContaining({ accountId: 'acc-1' }),
|
|
update: expect.objectContaining({ accountId: 'acc-1' }),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('fails when a page claims more data without a next cursor', async () => {
|
|
vi.mocked(operations.getOperations).mockResolvedValueOnce({
|
|
data: {
|
|
accountId: 'acc-1',
|
|
hasNext: true,
|
|
nextCursor: null,
|
|
asOf: '2026-06-16T00:00:00.000Z',
|
|
items: [],
|
|
},
|
|
meta: { fromCache: false, cachedAt: null },
|
|
});
|
|
|
|
const service = new BrokerOperationSyncService(operations, prisma);
|
|
|
|
await expect(
|
|
service.syncAccount('acc-1', {
|
|
from: '2026-06-01T00:00:00.000Z',
|
|
to: '2026-06-16T00:00:00.000Z',
|
|
}),
|
|
).rejects.toThrow('T-Bank returned hasNext without nextCursor');
|
|
});
|
|
});
|