import { Injectable } from '@nestjs/common'; import type { TbankBond } from '#src/integrations/tbank/tbank.api'; import { formatDecimal } from '#src/common/decimal'; import { decimal } from '#src/integrations/tbank/decimal'; import { CatalogDiscoveryService } from '../../application/catalog-discovery.service'; import { timestamp } from '../../application/instrument-market'; import { BondDto, BondResourceDto, BondSearchResourceDto, } from '../presentation/bond.dto'; type IdentifiedBond = TbankBond & { uid: string }; import { ISO_DATE_LENGTH, ISO_DATE_START } from '#src/common/time'; import { money } from '../infrastructure/bond-money.mapper'; const MIN_LOT_EXCLUSIVE = 0; @Injectable() export class BondCatalogService { constructor(private readonly discovery: CatalogDiscoveryService) {} async search( userId: string, query: string, ): Promise { const result = await this.discovery.search( userId, { instrumentType: 'bond', query }, ); return { ...result, data: result.data?.map((item) => { const short = toBond(item); const detail = item.detail ? toBond({ ...item.detail, uid: item.uid }) : short; const last = item.price; return { ...detail, classCode: detail.classCode ?? short.classCode, isin: detail.isin ?? short.isin, apiTradeAvailable: short.apiTradeAvailable, pricePercent: last?.price ? formatDecimal(decimal(last.price)) : null, priceAt: timestamp(last?.time), }; }) ?? null, }; } async get( userId: string, uid: string, ): Promise { const result = await this.discovery.detail(userId, 'bond', uid); return { ...result, data: result.data ? { ...toBond(result.data), slug: result.data.slug } : null, }; } async getByTicker( userId: string, ticker: string, ): Promise { const result = await this.discovery.byTicker( userId, 'bond', ticker, ); return { ...result, data: result.data ? { ...toBond(result.data), slug: result.data.slug } : null, }; } } function toBond(bond: IdentifiedBond): BondDto { return { instrumentUid: bond.uid, slug: bond.uid, name: bond.name?.trim() || bond.ticker || bond.uid, ticker: bond.ticker?.trim() || null, isin: bond.isin?.trim() || null, classCode: bond.classCode?.trim() || null, realExchange: bond.realExchange || null, apiTradeAvailable: bond.apiTradeAvailableFlag ?? null, logoName: bond.brand?.logoName?.trim() || null, currency: bond.currency?.toUpperCase() || null, bondType: bond.bondType || null, sector: bond.sector || null, lot: bond.lot && bond.lot > MIN_LOT_EXCLUSIVE ? bond.lot : null, historyFrom: timestamp(bond.first_1dayCandleDate), nominal: money(bond.nominal), initialNominal: money(bond.initialNominal), aci: money(bond.aciValue), maturityDate: timestamp(bond.maturityDate)?.slice(ISO_DATE_START, ISO_DATE_LENGTH) ?? null, callDate: timestamp(bond.callDate)?.slice(ISO_DATE_START, ISO_DATE_LENGTH) ?? null, couponQuantityPerYear: bond.couponQuantityPerYear ?? null, floatingCoupon: bond.floatingCouponFlag ?? null, perpetual: bond.perpetualFlag ?? null, amortization: bond.amortizationFlag ?? null, subordinated: bond.subordinatedFlag ?? null, riskLevel: bond.riskLevel || null, }; }