initial commit

This commit is contained in:
2026-06-09 14:07:37 +03:30
parent 30ac533800
commit 996a4fcda7
121 changed files with 20557 additions and 3 deletions

View File

@@ -0,0 +1,152 @@
import { Logger } from '@nestjs/common';
import { InquiryType } from '../../common/enums/inquiry-type.enum';
import { ProviderName } from '../../common/enums/provider-name.enum';
import { NormalizedErrorDto } from '../../common/dto/normalized-error.dto';
import {
InquiryProvider,
ProviderExecutionContext,
} from '../../common/interfaces/inquiry-provider.interface';
import { withRetry } from '../../common/helpers/retry.helper';
import { withTimeout } from '../../common/helpers/timeout.helper';
import { RequestLogger } from '../../common/helpers/request-logger.helper';
import { ProviderConfigSlice } from '../interfaces/provider-config.interface';
/**
* Abstract base for all provider adapters.
*
* Responsibilities:
* - Retry + timeout orchestration
* - Standardized error normalization
* - Response formatting hooks
* - Structured logging
*
* Subclasses implement provider-specific HTTP/business logic only.
*/
export abstract class BaseProvider<TRequest = unknown, TResponse = unknown>
implements InquiryProvider<TRequest, TResponse>
{
abstract readonly name: ProviderName;
abstract readonly supportedInquiryTypes: InquiryType[];
protected readonly logger: RequestLogger;
protected readonly nestLogger: Logger;
constructor(protected readonly config: ProviderConfigSlice) {
this.logger = new RequestLogger(this.constructor.name);
this.nestLogger = new Logger(this.constructor.name);
}
isEnabled(): boolean {
return this.config.enabled;
}
async execute(
inquiryType: InquiryType,
payload: TRequest,
context: ProviderExecutionContext,
): Promise<TResponse> {
if (!this.supportedInquiryTypes.includes(inquiryType)) {
throw this.normalizeError({
code: 'UNSUPPORTED_INQUIRY',
message: `Provider ${this.name} does not support ${inquiryType}`,
});
}
const start = Date.now();
this.logger.logStart(
{
requestId: context.requestId,
trackingCode: context.trackingCode,
provider: this.name,
inquiryType,
},
'Provider execution started',
);
try {
const result = await this.executeWithResilience(
() => this.callProvider(inquiryType, payload, context),
context,
);
this.logger.logSuccess(
{
requestId: context.requestId,
trackingCode: context.trackingCode,
provider: this.name,
inquiryType,
durationMs: Date.now() - start,
},
'Provider execution succeeded',
);
return result;
} catch (error) {
this.logger.logFailure(
{
requestId: context.requestId,
trackingCode: context.trackingCode,
provider: this.name,
inquiryType,
durationMs: Date.now() - start,
},
'Provider execution failed',
error,
);
throw error;
}
}
protected async executeWithResilience<T>(
fn: () => Promise<T>,
context: ProviderExecutionContext,
): Promise<T> {
const operation = () =>
withTimeout(fn(), this.config.timeout, `${this.name} request`);
return withRetry(operation, {
maxAttempts: this.config.maxRetries,
delayMs: 300,
shouldRetry: (error) => this.isRetryable(error),
});
}
protected isRetryable(error: unknown): boolean {
if (error && typeof error === 'object' && 'code' in error) {
const code = (error as { code?: string }).code;
return code === 'ETIMEDOUT' || code === 'ECONNABORTED' || code === 'ECONNRESET';
}
return false;
}
protected normalizeError(partial: Partial<NormalizedErrorDto>): NormalizedErrorDto {
return {
code: partial.code ?? 'PROVIDER_ERROR',
message: partial.message ?? 'Provider request failed',
providerMessage: partial.providerMessage,
providerCode: partial.providerCode,
};
}
protected formatProviderError(
providerMessage?: string,
providerCode?: string,
fallbackMessage = 'Provider returned an error',
): Error {
const normalized = this.normalizeError({
code: 'PROVIDER_ERROR',
message: fallbackMessage,
providerMessage,
providerCode,
});
const err = new Error(normalized.message);
(err as Error & { normalizedError: NormalizedErrorDto }).normalizedError = normalized;
return err;
}
protected abstract callProvider(
inquiryType: InquiryType,
payload: TRequest,
context: ProviderExecutionContext,
): Promise<TResponse>;
}