From 0d7839cf79669768bba55f420ed43304545ac8d9 Mon Sep 17 00:00:00 2001 From: codex-bot Date: Thu, 15 Oct 2026 05:41:21 +0300 Subject: [PATCH 1/2] fix: separate T-Bank RPC network time from queue wait (WealthLedger/backend#81) (#94) --- src/common/logging/structured-logger.service.ts | 1 + src/integrations/tbank/tbank.client.spec.ts | 2 ++ src/integrations/tbank/tbank.client.ts | 8 ++++++-- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/common/logging/structured-logger.service.ts b/src/common/logging/structured-logger.service.ts index 095e896..a05baaa 100644 --- a/src/common/logging/structured-logger.service.ts +++ b/src/common/logging/structured-logger.service.ts @@ -18,6 +18,7 @@ const allowedFields = new Set([ 'grpcCode', 'headersReceived', 'method', + 'networkMs', 'operation', 'queueWaitMs', 'requestId', diff --git a/src/integrations/tbank/tbank.client.spec.ts b/src/integrations/tbank/tbank.client.spec.ts index a35da59..4f73c6b 100644 --- a/src/integrations/tbank/tbank.client.spec.ts +++ b/src/integrations/tbank/tbank.client.spec.ts @@ -165,6 +165,8 @@ describe('TbankClient operations', () => { operation: 'getAccounts', grpcCode: status.DEADLINE_EXCEEDED, queueWaitMs: 0, channelState: 'CONNECTING', headersReceived: false, })); + const [, , fields] = logger.event.mock.calls.at(-1) as [string, string, { networkMs: unknown }]; + expect(typeof fields.networkMs).toBe('number'); expect(JSON.stringify(logger.event.mock.calls)).not.toContain('secret'); } finally { jest.useRealTimers(); diff --git a/src/integrations/tbank/tbank.client.ts b/src/integrations/tbank/tbank.client.ts index c6100b9..e3be72a 100644 --- a/src/integrations/tbank/tbank.client.ts +++ b/src/integrations/tbank/tbank.client.ts @@ -64,6 +64,7 @@ type TbankClientService = Client & Record>; type RpcDiagnostics = { channelState?: string; headersReceived?: boolean; + networkMs?: number; queueWaitMs?: number; trackingId?: string; }; @@ -524,10 +525,13 @@ export class TbankClient { throw new ServiceUnavailableException({ code: TbankErrorCode.Unavailable }); } const response = await this.queue.run(() => { - diagnostics.queueWaitMs = Math.round(performance.now() - startedAt); + const sentAt = performance.now(); + diagnostics.queueWaitMs = Math.round(sentAt - startedAt); return this.request( token, client, method, request, diagnostics, - ); + ).finally(() => { + diagnostics.networkMs = Math.round(performance.now() - sentAt); + }); }); this.logger.event('debug', 'tbank.rpc.completed', { operation: method, From 2ad3517d87bffa8af9285d72fd6f78b96b3eed9f Mon Sep 17 00:00:00 2001 From: codex-bot Date: Thu, 15 Oct 2026 06:00:39 +0300 Subject: [PATCH 2/2] fix: restore screener OpenAPI constraints (WealthLedger/backend#70) (#95) --- .../instruments/stocks/presentation/stock.dto.ts | 2 +- src/openapi.ts | 13 ++++++++++++- test/openapi-contract.e2e-spec.ts | 7 +++++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/modules/investments/instruments/stocks/presentation/stock.dto.ts b/src/modules/investments/instruments/stocks/presentation/stock.dto.ts index c4d47f4..14b0e60 100644 --- a/src/modules/investments/instruments/stocks/presentation/stock.dto.ts +++ b/src/modules/investments/instruments/stocks/presentation/stock.dto.ts @@ -93,7 +93,7 @@ export class StockScreenerItemDto { @ApiProperty({ nullable: true, type: String }) declare currency: null | string; @ApiProperty({ nullable: true, type: String }) declare realExchange: null | string; @ApiProperty({ nullable: true, type: String }) declare countryOfRisk: null | string; - @ApiProperty({ nullable: true, type: 'integer' }) declare lot: null | number; + @ApiProperty({ nullable: true, minimum: MIN_TICKER_LENGTH, type: 'integer' }) declare lot: null | number; } export class StockScreenerPageDto { diff --git a/src/openapi.ts b/src/openapi.ts index 46e13fb..3ea3fda 100644 --- a/src/openapi.ts +++ b/src/openapi.ts @@ -3,9 +3,20 @@ import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; export const API_PREFIX = 'api/v1'; +// Nest Swagger cannot express additionalProperties on a class, so closed request +// bodies are restored here after generation; the runtime allowlist stays in the controller. +const CLOSED_SCHEMAS = ['StockScreenerCriteriaDto']; + export function createOpenApiDocument(app: INestApplication) { - return SwaggerModule.createDocument( + const document = SwaggerModule.createDocument( app, new DocumentBuilder().setTitle('WealthLedger API').setVersion('v1').build(), ); + const schemas = document.components?.schemas ?? {}; + for (const [name, schema] of Object.entries(schemas)) { + if (CLOSED_SCHEMAS.includes(name) && 'properties' in schema) { + schema.additionalProperties = false; + } + } + return document; } diff --git a/test/openapi-contract.e2e-spec.ts b/test/openapi-contract.e2e-spec.ts index d95b945..f9eef12 100644 --- a/test/openapi-contract.e2e-spec.ts +++ b/test/openapi-contract.e2e-spec.ts @@ -122,6 +122,13 @@ describe('OpenAPI contract (integration)', () => { }); it('generates the runtime contract without server startup or deployment configuration', async () => { + const schemas = createOpenApiDocument(app).components?.schemas; + expect(schemas?.StockScreenerCriteriaDto).toMatchObject({ + additionalProperties: false, + }); + expect(schemas?.StockScreenerItemDto).toMatchObject({ + properties: { lot: { type: 'integer', minimum: 1, nullable: true } }, + }); expect( createOpenApiDocument(app).components?.schemas?.ProblemDetailsDto, ).toMatchObject({