refactor: retire legacy tbank-sync status and sync=observe (WealthLedger/backend#196)
Backend verify / commits (pull_request) Successful in 1m25s
Backend verify / security (pull_request) Successful in 1m8s
Backend verify / verify (pull_request) Successful in 6m20s

This commit is contained in:
2026-11-05 22:22:48 +03:00
parent 83b515a453
commit d1c5f575cf
36 changed files with 182 additions and 832 deletions
+23 -23
View File
@@ -153,19 +153,19 @@ describe('local accounts and portfolio HTTP (SQLite)', () => {
encryptedToken: app.get(TbankTokenService).encrypt('synthetic'),
},
});
const cold = await get('brokerage-accounts/unknown/portfolio?sync=observe').expect(200);
const cold = await get('brokerage-accounts/unknown/portfolio').expect(200);
expect((cold.body as PortfolioResourceDto).data).toBeNull();
expect((cold.body as PortfolioResourceDto).meta.availability).toBe(
'temporarily-unavailable',
);
await get('brokerage-accounts?sync=observe').expect(200);
await get('brokerage-accounts').expect(200);
expect(upstream.getOpenAccounts).not.toHaveBeenCalled();
expect(upstream.getPortfolio).not.toHaveBeenCalled();
const connection = await prisma.tbankConnection.findUniqueOrThrow({ where: { userId: 'owner' } });
await prisma.tbankAccount.create({ data: { userId: 'owner', revision: connection.revision, accountId: 'account', name: 'Synthetic', type: 'ACCOUNT_TYPE_TINKOFF', upstreamOrder: 0 } });
await prisma.tbankConnection.update({ where: { userId: 'owner' }, data: { membershipAt: new Date() } });
await get('brokerage-accounts/unknown/portfolio?sync=observe').expect(404);
await get('brokerage-accounts?sync=observe', 'other').expect(409);
await get('brokerage-accounts/unknown/portfolio').expect(404);
await get('brokerage-accounts', 'other').expect(409);
await get('brokerage-accounts?sync=unexpected').expect(200);
});
@@ -192,7 +192,7 @@ describe('local accounts and portfolio HTTP (SQLite)', () => {
await seedMembership();
const blocked = deferred<TbankPortfolio>();
upstream.getPortfolio.mockReturnValueOnce(blocked.promise);
const cold = await get('brokerage-accounts?sync=observe').expect(200);
const cold = await get('brokerage-accounts').expect(200);
expect(
(cold.body as BrokerageAccountsResourceDto).data?.[0].portfolioSummary,
).toBeNull();
@@ -270,7 +270,7 @@ describe('local accounts and portfolio HTTP (SQLite)', () => {
if (work) await work.done;
await settled();
const before = (
await get('brokerage-accounts/account/portfolio?sync=observe').expect(200)
await get('brokerage-accounts/account/portfolio').expect(200)
).body as PortfolioResourceDto;
const fetchedAt = new Date();
await prisma.tbankInstrument.create({
@@ -307,7 +307,7 @@ describe('local accounts and portfolio HTTP (SQLite)', () => {
update: { matchedUids: ['enriched-stock'], fetchedAt },
});
const after = (
await get('brokerage-accounts/account/portfolio?sync=observe').expect(200)
await get('brokerage-accounts/account/portfolio').expect(200)
).body as PortfolioResourceDto;
expect(after.data?.positions[0]).toMatchObject({
displayName: 'Enriched name',
@@ -333,7 +333,7 @@ describe('local accounts and portfolio HTTP (SQLite)', () => {
where: { userId: 'owner', scopeKey: 'portfolio:account' },
data: { state: 'complete' },
});
const stale = await get('brokerage-accounts/account/portfolio?sync=observe').expect(200);
const stale = await get('brokerage-accounts/account/portfolio').expect(200);
const previous = (stale.body as PortfolioResourceDto).data;
expect(previous?.isStale).toBe(true);
expect(previous?.lastSuccessfulAt).toBe(new Date(0).toISOString());
@@ -343,7 +343,7 @@ describe('local accounts and portfolio HTTP (SQLite)', () => {
data: { state: 'failed', errorCode: 'TBANK_UNAVAILABLE' },
});
const failed = await get(
'brokerage-accounts/account/portfolio?sync=observe',
'brokerage-accounts/account/portfolio',
).expect(200);
expect((failed.body as PortfolioResourceDto).data).toEqual(previous);
expect((failed.body as PortfolioResourceDto).meta.lastAttempt.state).toBe('none');
@@ -403,7 +403,7 @@ describe('local accounts and portfolio HTTP (SQLite)', () => {
},
]);
const income = await get(
'brokerage-accounts/account/income-analytics?sync=observe',
'brokerage-accounts/account/income-analytics',
).expect(200);
const incomeMeta = (income.body as IncomeAnalyticsResourceDto).meta;
expect(incomeMeta.lastAttempt.state).toBe('partial');
@@ -443,7 +443,7 @@ describe('local accounts and portfolio HTTP (SQLite)', () => {
});
const blocked = deferred<TbankAccount[]>();
upstream.getOpenAccounts.mockReturnValueOnce(blocked.promise);
await get('brokerage-accounts?sync=observe').expect(200);
await get('brokerage-accounts').expect(200);
expect(upstream.getOpenAccounts).not.toHaveBeenCalled();
expect(
(
@@ -468,9 +468,9 @@ describe('local accounts and portfolio HTTP (SQLite)', () => {
where: { id: 'abandoned-membership' },
data: { state: 'complete' },
});
expect((await get('brokerage-accounts/account/portfolio?sync=observe').expect(200)).body).toMatchObject({ data: null, meta: { availability: 'temporarily-unavailable', completeness: 'incomplete' } });
expect((await get('brokerage-accounts/account/portfolio').expect(200)).body).toMatchObject({ data: null, meta: { availability: 'temporarily-unavailable', completeness: 'incomplete' } });
expect(await prisma.tbankPortfolio.count()).toBe(0);
expect((await get('brokerage-accounts?sync=observe')).body).toMatchObject({
expect((await get('brokerage-accounts')).body).toMatchObject({
data: [{ id: 'account', portfolioSummary: null }],
});
});
@@ -510,7 +510,7 @@ describe('local accounts and portfolio HTTP (SQLite)', () => {
}
await delay(5);
}
const summaries = (await get('brokerage-accounts?sync=observe').expect(200))
const summaries = (await get('brokerage-accounts').expect(200))
.body as BrokerageAccountsResourceDto;
expect(summaries.data?.[0].portfolioSummary).toMatchObject({
totalsByCurrency: [{ currency: 'RUB', value: '21.1' }],
@@ -528,10 +528,10 @@ describe('local accounts and portfolio HTTP (SQLite)', () => {
expect(summaries.meta.availability).toBe('available');
expect(summaries.meta.completeness).toBe('complete');
expect(
(await get('brokerage-accounts/all/portfolio?sync=observe')).body,
(await get('brokerage-accounts/all/portfolio')).body,
).toMatchObject({ data: null, meta: { availability: 'temporarily-unavailable', completeness: 'incomplete' } });
expect(
(await get(`stocks/${uid}/position?sync=observe`).expect(200)).body,
(await get(`stocks/${uid}/position`).expect(200)).body,
).toMatchObject({ data: null, meta: { availability: 'temporarily-unavailable', completeness: 'incomplete' } });
expect(upstream.getPortfolio).toHaveBeenCalledTimes(2);
await prisma.tbankAccount.deleteMany({ where: { accountId: 'second' } });
@@ -542,7 +542,7 @@ describe('local accounts and portfolio HTTP (SQLite)', () => {
await prisma.tbankConnection.updateMany({
data: { membershipRevision: { increment: 1 } },
});
const position = await get(`stocks/${uid}/position?sync=observe`).expect(
const position = await get(`stocks/${uid}/position`).expect(
200,
);
expect(position.body).toMatchObject({
@@ -551,12 +551,12 @@ describe('local accounts and portfolio HTTP (SQLite)', () => {
});
await prisma.$disconnect();
expect(
(await get(`stocks/${uid}/position?sync=observe`).expect(200)).body,
(await get(`stocks/${uid}/position`).expect(200)).body,
).toEqual(position.body);
expect(upstream.getOpenAccounts).not.toHaveBeenCalled();
expect(upstream.getPortfolio).toHaveBeenCalledTimes(2);
await prisma.tbankConnection.updateMany({ data: { invalidToken: true } });
await get(`stocks/${uid}/position?sync=observe`).expect(400);
await get(`stocks/${uid}/position`).expect(400);
});
it('uses membership TTL for an empty aggregate without starting work through observation', async () => {
@@ -570,7 +570,7 @@ describe('local accounts and portfolio HTTP (SQLite)', () => {
data: { membershipAt: new Date(Date.now() - 120_000) },
});
const fresh = await get(
'brokerage-accounts/all/portfolio?sync=observe',
'brokerage-accounts/all/portfolio',
).expect(200);
expect((fresh.body as PortfolioResourceDto).meta.freshness).toBe('fresh');
expect((fresh.body as PortfolioResourceDto).data).toMatchObject({
@@ -580,7 +580,7 @@ describe('local accounts and portfolio HTTP (SQLite)', () => {
data: { membershipAt: new Date(Date.now() - 301_000) },
});
const stale = await get(
'brokerage-accounts/all/portfolio?sync=observe',
'brokerage-accounts/all/portfolio',
).expect(200);
expect((stale.body as PortfolioResourceDto).meta.freshness).toBe('fresh');
expect((stale.body as PortfolioResourceDto).data?.isStale).toBe(false);
@@ -595,7 +595,7 @@ describe('local accounts and portfolio HTTP (SQLite)', () => {
await prisma.tbankConnection.updateMany({
data: { membershipAt: new Date() },
});
const empty = await get('brokerage-accounts?sync=observe').expect(200);
const empty = await get('brokerage-accounts').expect(200);
expect((empty.body as BrokerageAccountsResourceDto).data).toEqual([]);
expect((empty.body as BrokerageAccountsResourceDto).meta.availability).toBe(
'available',
@@ -625,7 +625,7 @@ describe('local accounts and portfolio HTTP (SQLite)', () => {
await late.done;
expect(await prisma.tbankPortfolio.count()).toBe(0);
const replaced = await get(
'brokerage-accounts/account/portfolio?sync=observe',
'brokerage-accounts/account/portfolio',
).expect(200);
expect((replaced.body as PortfolioResourceDto).data).toBeNull();
});