import { Injectable } from '@nestjs/common'; import { MoexSecuritiesClient } from '../moex-client/moex-securities.client'; import { CacheService } from '../cache/cache.service'; import { SecurityType } from './dto/search-query.dto'; export interface SearchResultItem { secid: string; isin: string; shortName: string; type: 'share' | 'bond'; listLevel: number; currency: string | null; price: number | null; } @Injectable() export class SecuritiesService { constructor( private readonly moexSecurities: MoexSecuritiesClient, private readonly cache: CacheService, ) {} async search(query: string, type: SecurityType, limit: number): Promise { const { data } = await this.cache.getOrFetch( 'search', [query.toLowerCase()], async () => { const results = await this.moexSecurities.searchSecurities(query); return results .map((s): SearchResultItem | null => { const type = s.group === 'stock_shares' || s.type === 'common_share' || s.type === 'preferred_share' ? ('share' as const) : s.group === 'stock_bonds' ? ('bond' as const) : null; if (!type) return null; return { secid: s.secid, isin: s.isin, shortName: s.shortName, type, listLevel: s.listLevel, currency: s.faceUnit === 'SUR' ? 'RUB' : s.faceUnit || null, price: null, }; }) .filter((r): r is SearchResultItem => r !== null); }, 'searchTtl', ); let filtered = data; if (type === SecurityType.SHARE) { filtered = data.filter((r) => r.type === 'share'); } else if (type === SecurityType.BOND) { filtered = data.filter((r) => r.type === 'bond'); } return filtered.slice(0, limit); } async getShareBrief(secid: string): Promise { try { const desc = await this.moexSecurities.getSecurityDescription(secid); if (!desc) return null; return { secid: desc.secid, isin: desc.isin, shortName: desc.shortName, type: 'share', listLevel: desc.listLevel, currency: desc.faceUnit === 'SUR' ? 'RUB' : desc.faceUnit, price: null, }; } catch { return null; } } }