fix: scope income and ledger sync to requested intervals (WealthLedger/backend#57) (#147)
Backend verify / commits (push) Skipped
Backend verify / security (push) Successful in 17s
Backend verify / verify (push) Failing after 5m52s

This commit was merged in pull request #147.
This commit is contained in:
2026-10-28 21:29:53 +03:00
parent e350146749
commit 5e84e1e5f1
7 changed files with 114 additions and 525 deletions
@@ -6,13 +6,11 @@ import { MILLISECONDS_PER_SECOND } from '../../../../common/time';
import { isTbankErrorCode, TbankErrorCode } from '../../../../integrations/tbank/tbank-error-code';
import { TbankOperationState } from '../../../../integrations/tbank/tbank-protocol';
import { TbankAccessService, TbankClient } from '../../../../integrations/tbank/tbank.api';
import { accountHistory } from '../../investment-sync-plan';
import {
type OperationInterval,
operationIntervals,
operationsSyncPriority,
planOperationRead,
uncoveredOperationIntervals,
} from '../domain/operations-sync-plan';
import {
BrokerageOperationsRepository,
@@ -48,7 +46,6 @@ export class BrokerageOperationsSyncService {
intervals: OperationInterval[],
options: {
force?: boolean;
history?: boolean;
movingEnd?: boolean;
observe?: boolean;
revision?: string;
@@ -99,30 +96,6 @@ export class BrokerageOperationsSyncService {
connection.revision,
)
: null;
if (!options.observe && !options.force && options.history) {
const accountIds = new Set(
intervals.map((interval) => interval.accountId),
);
const history = accountHistory(
connection.accounts.filter((account) =>
accountIds.has(account.accountId),
),
now,
);
const missing = planOperationRead(history, coverage, now, {
movingEnd: true,
}).load;
const remaining = uncoveredOperationIntervals(missing, targets);
if (remaining.length) {
await this.request(
userId,
remaining,
false,
'history',
connection.revision,
);
}
}
return { plan, handle };
}
@@ -6,17 +6,12 @@ import {
InstrumentMetadataService,
metadataKey,
TbankAccessService,
TbankMembershipService,
type TbankOperationItem,
tbankSourceTtlMs,
} from '../../../../integrations/tbank/tbank.api';
import { dateOnlyBounds, DateOnlyRange } from '../../investment-calendar';
import { investmentResourceMeta } from '../../investment-resource.mapper';
import {
accountHistory,
ledgerRange,
planInvestmentScreen,
} from '../../investment-sync-plan';
import { accountHistory, ledgerRange } from '../../investment-sync-plan';
import {
hourlyAttempt,
TbankSyncRepository,
@@ -37,7 +32,6 @@ import {
} from '../presentation/brokerage-operation.dto';
import { BrokerageOperationsSyncService } from './brokerage-operations-sync.service';
const EXCLUSIVE_END_OFFSET_MS = 1;
const FIRST_RESULT_OFFSET = 0;
const MISSING_MEMBERSHIP_REVISION = 0;
const STALE_FRESHNESS_MS = 0;
@@ -61,7 +55,6 @@ export class BrokerageOperationsService {
private readonly metadata: InstrumentMetadataService,
private readonly repository: BrokerageOperationsRepository,
private readonly sync: BrokerageOperationsSyncService,
private readonly membership: TbankMembershipService,
private readonly config: ConfigService,
private readonly hourlySync: TbankSyncRepository,
) {}
@@ -117,35 +110,14 @@ export class BrokerageOperationsService {
cursor,
};
if (cursor) await this.repository.readPage(readInput);
const screen = planInvestmentScreen(
{
screen: 'operations',
accountId,
from: from.toISOString(),
to: new Date(to.valueOf() - EXCLUSIVE_END_OFFSET_MS).toISOString(),
},
{
accounts: selected,
instruments: [],
membershipKnown: Boolean(connection.membershipAt),
},
now,
);
const intervals = screen.sources.flatMap((source) =>
source.kind === 'operations' &&
source.priority === 'screen' &&
source.accountId &&
source.from &&
source.to
? [{ accountId: source.accountId, from: source.from, to: source.to }]
: [],
const intervals = selected.flatMap((account) =>
from < to ? [{ accountId: account.accountId, from, to }] : [],
);
// The ledger only starts the first-time load of missing intervals; the
// mutable tail belongs to the hourly walk and membership is not rechecked.
if (intervals.length) {
await this.sync.readPlan(userId, intervals, {
movingEnd,
history: true,
revision: connection.revision,
});
}
@@ -406,32 +378,19 @@ export class BrokerageOperationsService {
periods: ranges,
};
let saved = await this.repository.readPage(input);
const schedule = async () => {
const current = await this.access.local(userId, accountId);
if (current.revision !== connection.revision) return;
const snapshot = await this.repository.readPage(input);
if (!snapshot?.connection.membershipAt) return;
const targets = snapshot.connection.accounts.flatMap((account) =>
(snapshot.periods ?? []).map((period) => ({
if (!observe) {
const targets = (saved?.connection.accounts ?? []).flatMap((account) =>
(saved?.periods ?? []).map((period) => ({
accountId: account.accountId,
...period,
})),
);
if (targets.length) {
await this.sync.readPlan(userId, targets, {
movingEnd: snapshot.periods?.some((period) => period.movingEnd),
history: true,
movingEnd: saved?.periods?.some((period) => period.movingEnd),
revision: connection.revision,
});
}
};
if (!observe) {
const discovery = await this.membership.request(
userId,
connection.revision,
);
if (discovery) void discovery.done.then(schedule).catch(() => undefined);
else await schedule();
saved = await this.repository.readPage(input);
}
const now = new Date();