jest.mock('../../prisma/prisma.service', () => ({ PrismaService: class {}, })); import { investmentResourceMeta } from './investment-resource.mapper'; import { hourlyAttempt } from './tbank-sync/tbank-sync.repository'; describe('investment resource source freshness', () => { afterEach(() => jest.useRealTimers()); it('uses each required source TTL and oldest successful timestamp', () => { jest.useFakeTimers().setSystemTime(new Date('2026-09-25T12:00:00Z')); const membership = investmentResourceMeta({ resourceKey: 'membership', dataRevision: '1', availability: 'complete', fetchedAt: new Date('2026-09-25T11:58:00Z'), freshForMs: 300_000, work: null, }); const input = { resourceKey: 'portfolio', dataRevision: '1', availability: 'complete' as const, fetchedAt: new Date('2026-09-25T11:59:30Z'), freshForMs: 60_000, work: null, dependencies: [membership], }; const fresh = investmentResourceMeta(input); expect(fresh.freshness).toBe('fresh'); expect(fresh.lastSuccessfulAt).toBe('2026-09-25T11:58:00.000Z'); jest.advanceTimersByTime(30_000); const stale = investmentResourceMeta(input); expect(stale.freshness).toBe('stale'); expect(stale.lastSuccessfulAt).toBe(fresh.lastSuccessfulAt); expect( investmentResourceMeta({ ...input, dependencies: [{ ...membership, freshness: 'unknown' }], }).freshness, ).toBe('unknown'); }); it('keeps confirmed data state independent from a failed hourly attempt', () => { jest.useFakeTimers().setSystemTime(new Date('2026-09-25T11:30:00Z')); const meta = investmentResourceMeta({ resourceKey: 'portfolio/a', dataRevision: '1', availability: 'complete', fetchedAt: new Date('2026-09-25T11:00:00Z'), freshForMs: 3_600_000, work: null, lastAttempt: { startedAt: '2026-09-25T12:00:00.000Z', finishedAt: '2026-09-25T12:01:00.000Z', state: 'partial', problems: [ { section: 'portfolio', accountId: 'a', errorCode: 'TBANK_UNAVAILABLE', }, ], }, }); expect(meta).toMatchObject({ availability: 'available', completeness: 'complete', freshness: 'fresh', lastSuccessfulAt: '2026-09-25T11:00:00.000Z', lastAttempt: { state: 'partial', startedAt: '2026-09-25T12:00:00.000Z', finishedAt: '2026-09-25T12:01:00.000Z', }, }); }); it('projects only the matching hourly section into a resource attempt', () => { const attempt = hourlyAttempt( { startedAt: new Date('2026-09-25T12:00:00Z'), finishedAt: new Date('2026-09-25T12:01:00Z'), state: 'partial', sections: [ { section: 'portfolio', accountId: 'a', errorCode: 'TBANK_UNAVAILABLE' }, { section: 'operations', accountId: 'a', errorCode: null }, { section: 'events', errorCode: 'CALENDAR_UNAVAILABLE' }, ], }, 'portfolio', 'a', ); const meta = investmentResourceMeta({ resourceKey: 'portfolio/a', dataRevision: '1', availability: 'complete', fetchedAt: new Date('2026-09-25T11:00:00Z'), freshForMs: 3_600_000, work: { id: 'legacy-work', state: 'failed', errorCode: 'LEGACY_FAILURE' }, lastAttempt: attempt, }); expect(meta).toMatchObject({ availability: 'available', completeness: 'complete', lastAttempt: { state: 'partial', startedAt: '2026-09-25T12:00:00.000Z', finishedAt: '2026-09-25T12:01:00.000Z', problems: [{ section: 'portfolio', accountId: 'a', errorCode: 'TBANK_UNAVAILABLE' }], }, }); expect(JSON.stringify(meta)).not.toContain('legacy-work'); }); it('projects no confirmed result as incomplete without inventing a successful attempt', () => { expect( investmentResourceMeta({ resourceKey: 'accounts', dataRevision: '0', availability: 'missing', fetchedAt: null, freshForMs: 3_600_000, work: { id: 'work', state: 'running', errorCode: null }, }), ).toMatchObject({ availability: 'initial-loading', completeness: 'incomplete', freshness: 'unknown', lastSuccessfulAt: null, lastAttempt: { state: 'running', startedAt: null, finishedAt: null, problems: [], }, }); }); }); describe('dependent resource lifecycle', () => { const input = { resourceKey: 'source', dataRevision: '1', availability: 'complete' as const, fetchedAt: new Date(), freshForMs: 60_000, work: null, }; it('exposes dependency work without losing saved availability or timestamps', () => { const failed = investmentResourceMeta({ ...input, work: { id: 'failed', state: 'failed', errorCode: 'TBANK_UNAVAILABLE' }, }); const running = investmentResourceMeta({ ...input, work: { id: 'running', state: 'running', errorCode: null }, }); const parent = investmentResourceMeta({ ...input, dependencies: [failed, running], }); expect(parent.lastAttempt.state).toBe('none'); expect(parent.availability).toBe('available'); const terminal = investmentResourceMeta({ ...input, dependencies: [failed], }); expect(terminal.sync).toEqual(failed.sync); expect(terminal.lastSuccessfulAt).toBe(parent.lastSuccessfulAt); expect( investmentResourceMeta({ ...input, dependencies: [{ ...failed, unavailableReason: 'calendar_boundary' }], }).sync.state, ).toBe('idle'); }); });