feat: enrich ledger events with logo metadata
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import { Injectable, ServiceUnavailableException } from '@nestjs/common';
|
||||
import { RequestContextService } from '../common/logging/request-context.service';
|
||||
import { StructuredLogger } from '../common/logging/structured-logger.service';
|
||||
import {
|
||||
add,
|
||||
decimal,
|
||||
@@ -11,11 +10,11 @@ import {
|
||||
} from './decimal';
|
||||
import { MoneyDto, PortfolioDto, PortfolioPositionDto } from './portfolio.dto';
|
||||
import { TbankAccessService } from './tbank-access.service';
|
||||
import { InstrumentMetadataService } from './instrument-metadata.service';
|
||||
import { TbankClient, TbankMoney } from './tbank.client';
|
||||
|
||||
const freshForMs = 30_000;
|
||||
const fallbackForMs = 300_000;
|
||||
const metadataForMs = 86_400_000;
|
||||
const types: Record<string, string> = {
|
||||
share: 'STOCK',
|
||||
bond: 'BOND',
|
||||
@@ -28,26 +27,16 @@ const types: Record<string, string> = {
|
||||
};
|
||||
|
||||
type CachedSnapshot = { snapshot: PortfolioDto; storedAt: number };
|
||||
type CachedMetadata = {
|
||||
value: {
|
||||
ticker?: string;
|
||||
name?: string;
|
||||
isin?: string;
|
||||
brand?: { logoName?: string | null };
|
||||
} | null;
|
||||
storedAt: number;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class PortfolioService {
|
||||
private readonly snapshots = new Map<string, CachedSnapshot>();
|
||||
private readonly metadata = new Map<string, CachedMetadata>();
|
||||
|
||||
constructor(
|
||||
private readonly access: TbankAccessService,
|
||||
private readonly tbank: TbankClient,
|
||||
private readonly requestContext: RequestContextService,
|
||||
private readonly logger: StructuredLogger,
|
||||
private readonly metadata: InstrumentMetadataService,
|
||||
) {}
|
||||
|
||||
async get(userId: string, accountId: string): Promise<PortfolioDto> {
|
||||
@@ -168,7 +157,7 @@ export class PortfolioService {
|
||||
),
|
||||
}
|
||||
: null;
|
||||
const metadata = await this.instrument(token, instrumentUid, figi);
|
||||
const metadata = await this.metadata.resolve(token, instrumentUid, figi);
|
||||
return {
|
||||
figi,
|
||||
displayName:
|
||||
@@ -202,53 +191,6 @@ export class PortfolioService {
|
||||
? { currency, value: formatDecimal(decimal(value)) }
|
||||
: null;
|
||||
}
|
||||
|
||||
private async instrument(
|
||||
token: string,
|
||||
instrumentUid: string | null,
|
||||
figi: string | null,
|
||||
) {
|
||||
const lookup = async (
|
||||
key: string,
|
||||
fetch: () => Promise<{
|
||||
ticker?: string;
|
||||
name?: string;
|
||||
isin?: string;
|
||||
brand?: { logoName?: string | null };
|
||||
} | null>,
|
||||
) => {
|
||||
const cached = this.metadata.get(key);
|
||||
if (cached && Date.now() - cached.storedAt <= metadataForMs)
|
||||
return cached.value;
|
||||
try {
|
||||
const value = await fetch();
|
||||
this.metadata.set(key, { value, storedAt: Date.now() });
|
||||
this.logger.event('debug', 'portfolio.instrument-metadata.resolved', {
|
||||
found: Boolean(value?.name?.trim() || value?.ticker?.trim()),
|
||||
});
|
||||
return value;
|
||||
} catch (error) {
|
||||
this.logger.event('warn', 'portfolio.instrument-metadata.failed', {
|
||||
code:
|
||||
error instanceof Error && 'response' in error
|
||||
? ((error as { response?: { code?: string } }).response?.code ??
|
||||
'UNKNOWN')
|
||||
: 'UNKNOWN',
|
||||
});
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
if (instrumentUid) {
|
||||
const byUid = await lookup(`uid:${instrumentUid}`, () =>
|
||||
this.tbank.getInstrumentByUid(token, instrumentUid),
|
||||
);
|
||||
if (byUid) return byUid;
|
||||
}
|
||||
if (!figi) return null;
|
||||
return lookup(`figi:${figi}`, () =>
|
||||
this.tbank.getInstrumentByFigi(token, figi),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function decimalFromString(value: string) {
|
||||
|
||||
Reference in New Issue
Block a user