feat: unify income payment cards and drop period comparisons (WealthLedger/main#33) (#83)
Backend verify / commits (push) Skipped
Backend verify / security (push) Successful in 13s
Backend verify / verify (push) Failing after 7m1s

feat: unify income payment cards and drop period comparisons (WealthLedger/main#33)
This commit was merged in pull request #83.
This commit is contained in:
2026-10-13 18:16:46 +03:00
parent 90486d4237
commit 31b0e15579
14 changed files with 230 additions and 274 deletions
+44 -5
View File
@@ -2,6 +2,7 @@ import {
ChannelCredentials,
Client,
ClientUnaryCall,
connectivityState,
loadPackageDefinition,
Metadata,
ServiceError,
@@ -60,6 +61,12 @@ type Unary<Request, Response> = (
callback: (error: null | ServiceError, response: Response) => void,
) => ClientUnaryCall;
type TbankClientService = Client & Record<string, Unary<never, never>>;
type RpcDiagnostics = {
channelState?: string;
headersReceived?: boolean;
queueWaitMs?: number;
trackingId?: string;
};
export type TbankDecimal = { nano?: number | string; units?: number | string };
export type TbankMoney = TbankDecimal & { currency?: string };
export type TbankAccount = {
@@ -511,15 +518,20 @@ export class TbankClient {
for (let attempts = FIRST_ATTEMPT; attempts <= RPC_MAX_ATTEMPTS; attempts += ATTEMPT_INCREMENT) {
const startedAt = performance.now();
const queueFull = this.queue.size >= MAX_QUEUE_SIZE;
const diagnostics: RpcDiagnostics = {};
try {
if (queueFull) {
throw new ServiceUnavailableException({ code: TbankErrorCode.Unavailable });
}
const response = await this.queue.run(() =>
this.request<Response>(token, client, method, request),
);
const response = await this.queue.run(() => {
diagnostics.queueWaitMs = Math.round(performance.now() - startedAt);
return this.request<Response>(
token, client, method, request, diagnostics,
);
});
this.logger.event('debug', 'tbank.rpc.completed', {
operation: method,
...diagnostics,
attempts,
durationMs: Math.round(performance.now() - startedAt),
});
@@ -537,6 +549,7 @@ export class TbankClient {
if (retryable && attempts < RPC_MAX_ATTEMPTS) {
this.logger.event('warn', 'tbank.rpc.retry', {
operation: method,
...diagnostics,
...failureContext,
attempts,
durationMs: Math.round(performance.now() - startedAt),
@@ -546,6 +559,7 @@ export class TbankClient {
}
this.logger.event('error', 'tbank.rpc.failed', {
operation: method,
...diagnostics,
...failureContext,
attempts: queueFull ? NO_RPC_ATTEMPTS : attempts,
durationMs: Math.round(performance.now() - startedAt),
@@ -573,17 +587,42 @@ export class TbankClient {
client: TbankClientService,
method: string,
request: object,
diagnostics?: RpcDiagnostics,
): Promise<Response> {
return new Promise((resolve, reject) => {
const metadata = new Metadata();
metadata.set('Authorization', `Bearer ${token}`);
// A channel state is a snapshot, not proof that this particular call reached the server.
const channel = diagnostics && client.getChannel();
if (diagnostics) diagnostics.headersReceived = false;
// eslint-disable-next-line security/detect-object-injection -- Service and method names are internal RPC selections, not request input.
client[method](
const call = client[method](
request as never,
metadata,
{ deadline: new Date(Date.now() + requestDeadlineMs) },
(error, response) => (error ? reject(error) : resolve(response)),
(error, response) => {
if (diagnostics && channel) {
diagnostics.channelState = connectivityState[
channel.getConnectivityState(false)
];
}
if (error) reject(error);
else resolve(response);
},
);
if (diagnostics) {
call.on('metadata', (headers: Metadata) => {
diagnostics.headersReceived = true;
const trackingId = headers.get('x-tracking-id')[0];
// Only a UUID may enter logs; never forward arbitrary upstream metadata.
if (
typeof trackingId === 'string' &&
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(trackingId)
) {
diagnostics.trackingId = trackingId;
}
});
}
});
}
}