fix(investments): align roles and date contracts
Backend security / security (push) Successful in 9s
Backend verify / verify (push) Successful in 10m23s
Backend security / security (pull_request) Successful in 8s
Backend verify / verify (pull_request) Successful in 10m39s
Conventional commits / commits (pull_request) Successful in 29s

Identify income dependencies by role and document screen-specific dates.
Convert inclusive calendar days into half-open source ranges while
preserving operation timestamps and existing stored coverage.
This commit is contained in:
2026-09-27 11:38:13 +03:00
parent e4232138d0
commit 516e43203e
18 changed files with 500 additions and 74 deletions
@@ -10,6 +10,7 @@ import {
import { investmentResourceMeta } from '../investment-resource.mapper';
import {
IncomeAnalyticsResourceDto,
IncomeDependencyDto,
IncomeSummaryResourceDto,
} from './income-analytics.dto';
import { DateOnlyRange, moscowDate } from '../investment-calendar';
@@ -90,35 +91,44 @@ export class IncomeAnalyticsService {
observe,
);
const [current, prior, year] = periods;
const dependencies = periods.map((period) => period.meta);
if (!plan.yearAgo)
dependencies.push({
...investmentResourceMeta({
resourceKey: 'income/yearAgo', // gitleaks:allow public resource identifier
dataRevision: 'unavailable',
availability: 'missing',
fetchedAt: null,
freshForMs: 0,
work: null,
unavailableReason: 'CALENDAR_OR_ACCOUNT_HISTORY',
}),
});
// Preserve the legacy order while consumers migrate to explicit roles.
const dependencies: IncomeDependencyDto[] = [
{ ...current.meta, role: 'selected' },
{ ...prior.meta, role: 'previous' },
{
...(plan.yearAgo && year
? year.meta
: investmentResourceMeta({
resourceKey: 'income/yearAgo', // gitleaks:allow public resource identifier
dataRevision: 'unavailable',
availability: 'missing',
fetchedAt: null,
freshForMs: 0,
work: null,
unavailableReason: 'CALENDAR_OR_ACCOUNT_HISTORY',
})),
role: 'year',
},
];
const states = periods.map((period) => period.meta.sync);
const work =
states.find((state) => state.state === 'running') ??
states.find((state) => state.state === 'failed');
const meta = investmentResourceMeta({
resourceKey: `income/${accountId}/${plan.selected.from}/${plan.selected.to}`,
dataRevision: current.meta.dataRevision,
availability: current.meta.availability,
fetchedAt: current.meta.lastSuccessfulAt
? new Date(current.meta.lastSuccessfulAt)
: null,
freshForMs: current.meta.freshness === 'fresh' ? Infinity : 0,
work: work?.id ? { ...work, id: work.id } : null,
coverage: current.meta.coverage,
const meta = {
...investmentResourceMeta({
resourceKey: `income/${accountId}/${plan.selected.from}/${plan.selected.to}`,
dataRevision: current.meta.dataRevision,
availability: current.meta.availability,
fetchedAt: current.meta.lastSuccessfulAt
? new Date(current.meta.lastSuccessfulAt)
: null,
freshForMs: current.meta.freshness === 'fresh' ? Infinity : 0,
work: work?.id ? { ...work, id: work.id } : null,
coverage: current.meta.coverage,
dependencies,
}),
dependencies,
});
};
if (current.items === null) return { data: null, meta };
assert(current.meta.lastSuccessfulAt);
const selected = qualifying(current.items).map((entry) => ({