refactor: isolate operation ranges and intents

This commit is contained in:
2026-10-01 20:53:01 +03:00
parent 7fe88a12c7
commit a13cf84ead
5 changed files with 125 additions and 76 deletions
@@ -19,6 +19,7 @@ import {
} from '../../investment-sync-plan';
import { OperationErrorCode } from '../domain/operation-error-code';
import {
confirmedOperationRanges,
operationIntervalsOverlap,
operationsSyncPriority,
planOperationRead,
@@ -196,22 +197,7 @@ export class BrokerageOperationsService {
const plan = planOperationRead(targets, saved?.coverage ?? [], now, {
movingEnd,
});
const confirmed = plan.intervals.flatMap((target) =>
(saved?.coverage ?? [])
.filter(
(row) =>
row.accountId === target.accountId &&
(!row.instrumentUid ||
row.instrumentUid === target.instrumentUid) &&
row.from < target.to &&
target.from < row.to,
)
.map((row) => ({
accountId: target.accountId,
from: new Date(Math.max(row.from.valueOf(), target.from.valueOf())),
to: new Date(Math.min(row.to.valueOf(), target.to.valueOf())),
})),
);
const confirmed = confirmedOperationRanges(plan.intervals, saved?.coverage ?? []);
const missing = uncoveredOperationIntervals(
plan.intervals,
confirmed.map((range) => ({
@@ -544,19 +530,7 @@ export class BrokerageOperationsService {
.sort((a, b) => a.valueOf() - b.valueOf())[0] ??
(complete ? saved?.connection.membershipAt : null) ??
null;
const confirmed = plan.intervals.flatMap((target) =>
sources
.filter(
(row) =>
row.accountId === target.accountId &&
operationIntervalsOverlap(row, target),
)
.map((row) => ({
accountId: target.accountId,
from: new Date(Math.max(row.from.valueOf(), target.from.valueOf())),
to: new Date(Math.min(row.to.valueOf(), target.to.valueOf())),
})),
);
const confirmed = confirmedOperationRanges(plan.intervals, sources);
const meta = investmentResourceMeta({
// eslint-disable-next-line security/detect-object-injection -- The numeric index enumerates the three comparison periods.
resourceKey: `income/${accountId}/${['selected', 'previous', 'yearAgo'][index]}/${period.key}`,
@@ -1,4 +1,30 @@
import { operationIntervals, planOperationRead } from './operations-sync-plan';
import { confirmedOperationRanges, operationIntervals, planOperationRead } from './operations-sync-plan';
describe('confirmed operation range projection', () => {
it('clips overlaps in source order without merging and respects account and instrument scope', () => {
const day = (value: number) => new Date(Date.UTC(2026, 0, value));
const target = { accountId: 'a', from: day(2), to: day(5) };
const sources = [
{ ...target, from: day(3), to: day(6) },
{ ...target, from: day(1), to: day(4) },
{ ...target, instrumentUid: 'uid' },
{ ...target, accountId: 'other' },
{ ...target, from: day(5), to: day(6) },
{ ...target, from: day(1), to: day(2) },
];
const before = structuredClone(sources);
expect(confirmedOperationRanges([target], sources)).toEqual([
{ ...target, from: day(3) },
{ ...target, to: day(4) },
]);
expect(confirmedOperationRanges([{ ...target, instrumentUid: 'uid' }], sources)).toEqual([
{ ...target, from: day(3) },
{ ...target, to: day(4) },
target,
]);
expect(sources).toEqual(before);
});
});
describe('operations calendar windows', () => {
it('merges overlaps before splitting leap February and keeps precise Moscow-derived boundaries', () => {
@@ -106,6 +106,24 @@ export function uncoveredOperationIntervals(
});
}
export function confirmedOperationRanges(
targets: OperationInterval[],
coverage: OperationInterval[],
): Omit<OperationInterval, 'instrumentUid'>[] {
return targets.flatMap((target) => coverage
.filter((row) =>
row.accountId === target.accountId &&
(!row.instrumentUid || row.instrumentUid === target.instrumentUid) &&
row.from < target.to && target.from < row.to,
)
.map((row) => ({
accountId: target.accountId,
from: new Date(Math.max(row.from.valueOf(), target.from.valueOf())),
to: new Date(Math.min(row.to.valueOf(), target.to.valueOf())),
})),
);
}
export type OperationCoverage = OperationInterval & {
asOf: Date;
fetchedAt: Date;
@@ -392,52 +392,15 @@ export class BrokerageOperationsRepository {
const work = await tx.tbankSyncWork.update({
where: { id: previous.id },
data: {
intent: {
atomic: previousIntent?.atomic ?? publicationAtomic,
atomicIntervals: previousIntent?.atomic
? [
...new Set([
...(previousIntent.atomicIntervals ??
previousIntent.intervals),
...additions.map(intervalKey),
]),
]
: [],
screenIntervals: [
...new Set([
...(previousIntent?.screenIntervals ??
(previous.priority === operationsSyncPriority.screen
? (previousIntent?.intervals ?? [])
: [])),
...(priority === operationsSyncPriority.screen
? [...pending, ...additions]
.filter((item) =>
intervals.some((target) =>
operationIntervalsOverlap(item, target),
),
)
.map(intervalKey)
: []),
]),
],
intervals: (priority < previous.priority
? [
...pending.filter((item) =>
intervals.some((target) =>
operationIntervalsOverlap(item, target),
),
),
...additions,
...pending.filter(
(item) =>
!intervals.some((target) =>
operationIntervalsOverlap(item, target),
),
),
]
: [...pending, ...additions]
).map(intervalKey),
},
intent: extendRunningIntent({
previous: previousIntent,
publicationAtomic,
pending,
additions,
intervals,
priority,
previousPriority: previous.priority,
}),
priority: Math.min(priority, previous.priority),
},
});
@@ -919,6 +882,40 @@ export class BrokerageOperationsRepository {
}
}
function extendRunningIntent(input: {
additions: OperationInterval[];
intervals: OperationInterval[];
pending: OperationInterval[];
previous: OperationsIntent | undefined;
previousPriority: number;
priority: number;
publicationAtomic: boolean;
}): OperationsIntent {
const { previous, pending, additions, intervals, priority, previousPriority, publicationAtomic } = input;
const overlapsRequested = (item: OperationInterval) =>
intervals.some((target) => operationIntervalsOverlap(item, target));
const retainedScreen = previous?.screenIntervals ??
(previousPriority === operationsSyncPriority.screen ? (previous?.intervals ?? []) : []);
const addedScreen = priority === operationsSyncPriority.screen
? [...pending, ...additions].filter(overlapsRequested).map(intervalKey)
: [];
const ordered = priority < previousPriority
? [
...pending.filter(overlapsRequested),
...additions,
...pending.filter((item) => !overlapsRequested(item)),
]
: [...pending, ...additions];
return {
atomic: previous?.atomic ?? publicationAtomic,
atomicIntervals: previous?.atomic
? [...new Set([...(previous.atomicIntervals ?? previous.intervals), ...additions.map(intervalKey)])]
: [],
screenIntervals: [...new Set([...retainedScreen, ...addedScreen])],
intervals: ordered.map(intervalKey),
};
}
function intervalKey(interval: OperationInterval): string {
// eslint-disable-next-line sonarjs/no-inverted-boolean-check -- Negation also rejects Invalid Date; >= would silently accept NaN.
if (!(interval.from < interval.to)) {