refactor: centralize provider projections

This commit is contained in:
2026-10-01 20:53:00 +03:00
parent 190eb90ffe
commit 7fe88a12c7
7 changed files with 179 additions and 154 deletions
@@ -5,10 +5,8 @@ import {
} from '@nestjs/common';
import { randomUUID } from 'node:crypto';
import { formatDecimal } from '../../../../common/decimal';
import { StructuredLogger } from '../../../../common/logging/structured-logger.service';
import { MILLISECONDS_PER_DAY } from '../../../../common/time';
import { decimal } from '../../../../integrations/tbank/decimal';
import { isTbankErrorCode, TbankErrorCode } from '../../../../integrations/tbank/tbank-error-code';
import {
TbankAccessService,
@@ -26,6 +24,7 @@ import {
} from '../domain/candle-coverage';
import { InstrumentErrorCode } from '../domain/instrument-error-code';
import { CandleRepository } from '../infrastructure/candle.repository';
import { toSavedCandle } from '../infrastructure/tbank-candle.mapper';
import { CatalogEnrichmentService } from './catalog-enrichment.service';
import { timestamp } from './instrument-market';
@@ -413,17 +412,7 @@ export class CandleService {
window.from,
new Date(Math.min(window.to.valueOf(), now.valueOf())),
);
const rows = candles.flatMap((candle) => {
const time = timestamp(candle.time);
if (!time || !candle.close) throw new Error('Invalid candle response');
return [
{
time,
close: formatDecimal(decimal(candle.close)),
isComplete: candle.isComplete === true,
},
];
});
const rows = candles.map(toSavedCandle);
if (!(await this.repository.stage(attempt, window, rows, now))) {
return false;
}
@@ -4,7 +4,6 @@ import { randomUUID } from 'node:crypto';
import {
decimalFromString,
divide,
formatDecimal,
} from '../../../../common/decimal';
import { StructuredLogger } from '../../../../common/logging/structured-logger.service';
import {
@@ -13,7 +12,6 @@ import {
} from '../../../../common/time';
import { type MoexBondEvent, MoexClient } from '../../../../integrations/moex/moex.api';
import { NcrRatingsClient } from '../../../../integrations/ncr/ncr.api';
import { decimal } from '../../../../integrations/tbank/decimal';
import { isTbankErrorCode, TbankErrorCode } from '../../../../integrations/tbank/tbank-error-code';
import {
TbankAccessService,
@@ -37,6 +35,7 @@ import {
catalogScope,
type CatalogSource,
} from '../infrastructure/catalog-discovery.repository';
import { toSavedCandle } from '../infrastructure/tbank-candle.mapper';
import { catalogSearchLimit } from './catalog-selection';
import { previousClose, timestamp } from './instrument-market';
@@ -353,17 +352,7 @@ export class CatalogEnrichmentService {
gap.from,
gap.to,
);
const rows = response.map((candle) => {
const time = timestamp(candle.time);
if (!time || !candle.close) {
throw new Error('Invalid candle response');
}
return {
time,
close: formatDecimal(decimal(candle.close)),
isComplete: candle.isComplete === true,
};
});
const rows = response.map(toSavedCandle);
if (!(await this.candles.stage(attempt, gap, rows, now))) {
throw new Error('Cancelled candle comparison');
}
@@ -0,0 +1,19 @@
import { toSavedCandle } from './tbank-candle.mapper';
describe('T-Bank candle mapping', () => {
it('keeps signed nano precision and requires explicit completeness', () => {
const input = { time: { seconds: '1767225600' }, close: { units: '0', nano: -1 } };
expect(toSavedCandle(input)).toEqual({
time: '2026-01-01T00:00:00.000Z', close: '-0.000000001', isComplete: false,
});
expect(toSavedCandle({ ...input, isComplete: true }).isComplete).toBe(true);
expect(input).toEqual({ time: { seconds: '1767225600' }, close: { units: '0', nano: -1 } });
});
it.each([
{ close: { units: '1' } },
{ time: { seconds: 'invalid' }, close: { units: '1' } },
{ time: { seconds: '1767225600' } },
])('rejects missing or malformed source fields: %j', (input) => {
expect(() => toSavedCandle(input)).toThrow('Invalid candle response');
});
});
@@ -0,0 +1,18 @@
import type { TbankCandle } from '../../../../integrations/tbank/tbank.api';
import type { SavedCandle } from '../domain/candle-coverage';
import { formatDecimal } from '../../../../common/decimal';
import { decimal } from '../../../../integrations/tbank/decimal';
import { timestamp } from '../application/instrument-market';
function toSavedCandle(candle: TbankCandle): SavedCandle {
const time = timestamp(candle.time);
if (!time || !candle.close) throw new Error('Invalid candle response');
return {
time,
close: formatDecimal(decimal(candle.close)),
isComplete: candle.isComplete === true,
};
}
export { toSavedCandle };