feat: reconcile hourly investment resource states
This commit is contained in:
@@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
|
||||
|
||||
import type { InvestmentResourceMetaDto } from '../../investment-resource.dto';
|
||||
|
||||
import { add, decimalFromString, formatDecimal } from '../../../../common/decimal';
|
||||
import { investmentResourceMeta } from '../../investment-resource.mapper';
|
||||
import {
|
||||
ForecastRepository,
|
||||
@@ -16,6 +17,7 @@ export class ForecastService {
|
||||
|
||||
async result<T extends object>(input: {
|
||||
candidate?: T;
|
||||
lastAttempt?: InvestmentResourceMetaDto['lastAttempt'];
|
||||
membershipRevision: number;
|
||||
portfolio: InvestmentResourceMetaDto;
|
||||
revision: string;
|
||||
@@ -72,22 +74,51 @@ export class ForecastService {
|
||||
.map((dependency) => dependency.sync)
|
||||
.find((sync) => sync.state === 'failed');
|
||||
return {
|
||||
data: stored ? (stored.payload as T) : null,
|
||||
data: stored ? filterEvents(stored.payload as T, input.scopeKey) : null,
|
||||
meta: investmentResourceMeta({
|
||||
resourceKey: input.scopeKey,
|
||||
dataRevision: String(stored?.dataRevision ?? MISSING_DATA_REVISION),
|
||||
availability: stored
|
||||
? 'complete'
|
||||
: dependencies.some(
|
||||
(dependency) => dependency.availability !== 'missing',
|
||||
(dependency) => dependency.completeness === 'complete',
|
||||
)
|
||||
? 'partial'
|
||||
: 'missing',
|
||||
fetchedAt: first ? new Date(first) : null,
|
||||
freshForMs: Number.POSITIVE_INFINITY,
|
||||
work: work?.id ? { ...work, id: work.id } : null,
|
||||
lastAttempt: input.lastAttempt,
|
||||
dependencies,
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function filterEvents<T extends object>(payload: T, scopeKey: string): T {
|
||||
if (!scopeKey.startsWith('events/')) return payload;
|
||||
try {
|
||||
const [, from, to] = JSON.parse(scopeKey.slice('events/'.length)) as [string, string, string];
|
||||
const value = payload as T & {
|
||||
events?: { date: string; projectedAmount?: { currency: string; value: string }; type: string }[];
|
||||
incomeTotalsByCurrency?: { currency: string; value: string }[];
|
||||
};
|
||||
if (!value.events || !value.incomeTotalsByCurrency) return payload;
|
||||
const events = value.events.filter((event) => event.date >= from && event.date <= to);
|
||||
const totals = new Map<string, ReturnType<typeof decimalFromString>>();
|
||||
for (const event of events) {
|
||||
if (!['coupon', 'dividend'].includes(event.type) || !event.projectedAmount) continue;
|
||||
const current = totals.get(event.projectedAmount.currency) ?? { value: 0n, scale: 0 };
|
||||
totals.set(event.projectedAmount.currency, add(current, decimalFromString(event.projectedAmount.value)));
|
||||
}
|
||||
return {
|
||||
...value,
|
||||
events,
|
||||
incomeTotalsByCurrency: [...totals]
|
||||
.sort(([left], [right]) => left.localeCompare(right))
|
||||
.map(([currency, amount]) => ({ currency, value: formatDecimal(amount) })),
|
||||
};
|
||||
} catch {
|
||||
return payload;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user