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'); }); });