refactor: retire legacy tbank-sync status and sync=observe (WealthLedger/backend#196)
Backend verify / commits (push) Skipped
Backend verify / security (push) Successful in 1m7s
Backend verify / verify (push) Failing after 7m39s

This commit was merged in pull request #197.
This commit is contained in:
2026-11-05 22:33:14 +03:00
parent 83b515a453
commit d792baaf59
36 changed files with 182 additions and 832 deletions
+20 -47
View File
@@ -34,12 +34,9 @@ describe('local income completeness and exact money (SQLite)', () => {
const get = (
account = 'all',
period = 'from=2025-01-01&to=2025-01-31',
observe = true,
) =>
request(app.getHttpServer())
.get(
`/api/v1/brokerage-accounts/${account}/income-analytics?${period}${observe ? '&sync=observe' : ''}`,
)
.get(`/api/v1/brokerage-accounts/${account}/income-analytics?${period}`)
.set('Authorization', authorization)
.timeout(2000);
const read = async (account = 'all', period?: string) =>
@@ -127,7 +124,7 @@ describe('local income completeness and exact money (SQLite)', () => {
}
}
});
it('returns exact selected sums; reopen and observe need no RPC', async () => {
it('returns exact selected sums and reuses covered reads without RPC', async () => {
for (let index = 0; index < 2; index++) {
const result = await read();
assert(result.data);
@@ -210,7 +207,9 @@ describe('local income completeness and exact money (SQLite)', () => {
expect(
(await read('a', 'from=2025-03-01&to=2025-03-31')).data?.totalsByCurrency,
).toEqual([]);
expect(upstream.getOperationsByCursor).not.toHaveBeenCalled();
expect(upstream.getOperationsByCursor).toHaveBeenCalledWith(
'synthetic', expect.objectContaining({ accountId: 'b' }),
);
});
it('counts corrected identities once for the selected period', async () => {
await prisma.tbankOperation.updateMany({
@@ -274,7 +273,7 @@ describe('local income completeness and exact money (SQLite)', () => {
expect(after.meta.lastAttempt.state).toBe('none');
expect(upstream.getOperationsByCursor).not.toHaveBeenCalled();
});
it('retains current-month observed completeness and does not renew source timestamps', async () => {
it('retains current-month completeness and does not renew source timestamps', async () => {
const now = new Date();
const today = moscowDate(now);
const start = new Date(`${today.slice(0, 7)}-01T00:00:00+03:00`);
@@ -283,7 +282,7 @@ describe('local income completeness and exact money (SQLite)', () => {
await coverage(accountId, start, asOf);
}
const response = await request(app.getHttpServer())
.get('/api/v1/brokerage-accounts/all/income-summary?sync=observe')
.get('/api/v1/brokerage-accounts/all/income-summary')
.set('Authorization', authorization)
.expect(200);
const result = response.body as IncomeSummaryResourceDto;
@@ -302,58 +301,32 @@ describe('local income completeness and exact money (SQLite)', () => {
where: { userId: 'income-owner' },
data: { membershipAt: new Date('2020-01-01Z') },
});
const response = await get('a', undefined, false).expect(200);
const response = await get('a').expect(200);
const result = response.body as IncomeAnalyticsResourceDto;
expect(result.data).not.toBeNull();
expect(result.meta.completeness).toBe('complete');
expect(upstream.getOpenAccounts).not.toHaveBeenCalled();
expect(upstream.getOperationsByCursor).not.toHaveBeenCalled();
});
it('cold income imports only the missing requested interval', async () => {
it('ignores retired sync=observe on income reads and keeps the cold response incomplete', async () => {
await prisma.tbankConnection.update({
where: { userId: 'income-owner' },
data: { membershipAt: new Date('2020-01-01Z') },
});
let release: () => void = () => {
throw new Error('Upstream not initialized');
};
const blocked = new Promise<{ hasNext: boolean; items: never[] }>((resolve) => {
release = () => resolve({ hasNext: false, items: [] });
});
upstream.getOperationsByCursor.mockReturnValue(blocked);
const first = await get('a', 'from=2025-02-01&to=2025-02-28', false).expect(200);
const cold = first.body as IncomeAnalyticsResourceDto;
release();
expect(cold.data).toBeNull();
expect(cold.meta.completeness).toBe('incomplete');
let result: IncomeAnalyticsResourceDto = cold;
for (let attempt = 0; attempt < 100; attempt++) {
const response = await get('a', 'from=2025-02-01&to=2025-02-28').expect(200);
result = response.body as IncomeAnalyticsResourceDto;
if (result.meta.completeness === 'complete') break;
await new Promise((resolve) => setTimeout(resolve, 10));
}
expect(result.data).not.toBeNull();
expect(result.meta.completeness).toBe('complete');
const response = await request(app.getHttpServer())
.get('/api/v1/brokerage-accounts/a/income-analytics?from=2025-02-01&to=2025-02-28&sync=observe')
.set('Authorization', authorization)
.expect(200);
const result = response.body as IncomeAnalyticsResourceDto;
expect(result.data).toBeNull();
expect(result.meta.completeness).toBe('incomplete');
expect(upstream.getOpenAccounts).not.toHaveBeenCalled();
expect(upstream.getOperationsByCursor.mock.calls).toEqual([
['synthetic', expect.objectContaining({
accountId: 'a',
from: { seconds: '1738357199' },
to: { seconds: '1738368001' },
})],
['synthetic', expect.objectContaining({
accountId: 'a',
from: { seconds: '1738367999' },
to: { seconds: '1740776401' },
})],
]);
});
it('returns while a cold upstream is blocked and validates access/period before scheduling', async () => {
await get('absent', undefined, false).expect(404);
await get('a', 'from=2025-02-31&to=2025-03-01', false).expect(400);
await get('absent').expect(404);
await get('a', 'from=2025-02-31&to=2025-03-01').expect(400);
expect(upstream.getOperationsByCursor).not.toHaveBeenCalled();
const response = await get('all', undefined, false).expect(200);
const response = await get('all').expect(200);
expect((response.body as IncomeAnalyticsResourceDto).data).not.toBeNull();
});
it('plans only the selected period for a leap day without scheduling it', async () => {
@@ -361,7 +334,7 @@ describe('local income completeness and exact money (SQLite)', () => {
expect(result.meta.dependencies.map(({ role }) => role)).toEqual([
'selected',
]);
expect(result.meta.dependencies[0].availability).toBe('temporarily-unavailable');
expect(result.meta.dependencies[0].availability).toBe('initial-loading');
expect(result.data).toBeNull();
expect(upstream.getOperationsByCursor).not.toHaveBeenCalled();
});