feat: reconcile hourly investment resource states

This commit is contained in:
2026-10-09 23:10:50 +03:00
committed by ksv741
parent 5d857de7a5
commit 323b83b582
25 changed files with 445 additions and 86 deletions
@@ -4,11 +4,12 @@ import { TbankModule } from '../../../integrations/tbank/tbank.module';
import { AuthModule } from '../../auth/auth.module';
import { InstrumentSourcesModule } from '../instruments/instrument-sources.module';
import { PortfolioModule } from '../portfolio/portfolio.module';
import { TbankSyncRepositoryModule } from '../tbank-sync/tbank-sync-repository.module';
import { UpcomingEventsController } from './upcoming-events.controller';
import { UpcomingEventsService } from './upcoming-events.service';
@Module({
imports: [AuthModule, TbankModule, InstrumentSourcesModule, PortfolioModule],
imports: [AuthModule, TbankModule, InstrumentSourcesModule, PortfolioModule, TbankSyncRepositoryModule],
controllers: [UpcomingEventsController],
providers: [UpcomingEventsService],
exports: [UpcomingEventsService],
@@ -20,6 +20,10 @@ import {
type PortfolioPositionDto,
PortfolioService,
} from '../portfolio/portfolio.api';
import {
hourlyAttempt,
TbankSyncRepository,
} from '../tbank-sync/tbank-sync.repository';
import { bondEvent, dividendEvent } from './upcoming-event.mapper';
import {
UpcomingEventDto,
@@ -43,6 +47,7 @@ export class UpcomingEventsService {
private readonly sources: CatalogEnrichmentService,
private readonly forecasts: ForecastService,
private readonly metadata: InstrumentMetadataService,
private readonly sync: TbankSyncRepository,
) {}
// Read endpoints stay local; callers without a saved portfolio receive unknown.
@@ -54,6 +59,7 @@ export class UpcomingEventsService {
const window = dateWindow(query);
const connection = await this.access.local(userId, accountId);
const portfolio = await this.portfolios.read(userId, accountId);
const hourly = await this.sync.hourly(userId);
const events: UpcomingEventDto[] = [];
let ready = portfolio.data !== null;
const positions = portfolio.data?.positions.filter(supported) ?? [];
@@ -61,7 +67,7 @@ export class UpcomingEventsService {
userId,
positions,
window,
query.sync === 'observe',
true,
);
for (const position of positions) {
const generic = metadata.get(metadataKey(position))?.data;
@@ -98,7 +104,7 @@ export class UpcomingEventsService {
source.input.uid === uid && source.input.kind === 'detail',
)?.instrument || generic;
const ticker = position.ticker || instrument?.ticker;
if (!calendar || calendar.meta.availability !== 'complete' || !ticker) {
if (!calendar || calendar.meta.completeness !== 'complete' || !ticker) {
ready = false;
continue;
}
@@ -157,6 +163,11 @@ export class UpcomingEventsService {
membershipRevision: connection.membershipRevision,
scopeKey: `events/${JSON.stringify([accountId, window.from, window.to])}`,
portfolio: portfolio.meta,
lastAttempt: hourlyAttempt(
hourly,
'events',
accountId === 'all' ? undefined : accountId,
),
sources,
candidate,
});
@@ -168,8 +179,12 @@ export class UpcomingEventsService {
// Triggers online refreshes of the account's event sources and returns the
// launched work ids for the hourly walk to observe.
async refresh(userId: string, accountId: string): Promise<string[]> {
const window = dateWindow({});
async refresh(
userId: string,
accountId: string,
query: UpcomingEventsQueryDto = {},
): Promise<string[]> {
const window = dateWindow(query);
await this.access.local(userId, accountId);
const portfolio = await this.portfolios.read(userId, accountId);
const positions = portfolio.data?.positions.filter(supported) ?? [];