From 586e9fdd3924423be6ef309cd4849111e9dac1ea Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Wed, 11 Nov 2026 09:14:34 +0300 Subject: [PATCH] fix(sync): start hourly operations after portfolio works finish (#201) --- .../tbank-sync/tbank-hourly.scheduler.ts | 9 +++++++- test/tbank-hourly-lifecycle.e2e-spec.ts | 23 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/modules/investments/tbank-sync/tbank-hourly.scheduler.ts b/src/modules/investments/tbank-sync/tbank-hourly.scheduler.ts index 1b72823..349ee48 100644 --- a/src/modules/investments/tbank-sync/tbank-hourly.scheduler.ts +++ b/src/modules/investments/tbank-sync/tbank-hourly.scheduler.ts @@ -244,16 +244,23 @@ export class TbankHourlyScheduler implements OnModuleDestroy, OnModuleInit { dispatchProblems: HourlySection[], walkStartedAt: Date, ) { + const portfolios: string[] = []; for (const account of accounts) { await this.dispatchPortfolio( userId, revision, account.accountId, - dispatch, + async (child) => { + portfolios.push(child.id); + await dispatch(child); + }, dispatchProblems, walkStartedAt, ); } + // Operations coverage must end after the Portfolio valuation it explains: + // performance requires coverage up to the snapshot fetchedAt. + await this.waitWorks(userId, portfolios); for (const account of accounts) { await this.dispatchOperations( userId, diff --git a/test/tbank-hourly-lifecycle.e2e-spec.ts b/test/tbank-hourly-lifecycle.e2e-spec.ts index 60b0988..0368fae 100644 --- a/test/tbank-hourly-lifecycle.e2e-spec.ts +++ b/test/tbank-hourly-lifecycle.e2e-spec.ts @@ -167,6 +167,29 @@ describe('hourly attempt lifecycle (SQLite)', () => { expect(read?.state).toBe('succeeded'); }); + it('starts operations only after every portfolio work finished', async () => { + const portfolios: string[] = []; + portfolio.request.mockImplementation( + async (_userId: string, _revision: string, accountId: string) => { + const row = await child('running', null, `portfolio:${accountId}`); + portfolios.push(row.id); + return { id: row.id, done: Promise.resolve() }; + }, + ); + const scheduler = createScheduler(); + await scheduler.tick(); + await pump(); + expect(portfolios).toHaveLength(2); + expect(operations.request).not.toHaveBeenCalled(); + await prisma.tbankSyncWork.updateMany({ + where: { id: { in: portfolios } }, + data: { state: 'complete' }, + }); + const row = await runWalk(scheduler); + expect(operations.request).toHaveBeenCalledTimes(2); + expect(row.state).toBe('complete'); + }); + it('settles partial and keeps successful steps when one account fails', async () => { portfolio.request.mockImplementation( async (_userId: string, _revision: string, accountId: string) => {