import { Logger } from '@nestjs/common'; import { AxiosError } from 'axios'; 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 implements InquiryProvider { 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 { 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( fn: () => Promise, context: ProviderExecutionContext, ): Promise { 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 { return { code: partial.code ?? 'PROVIDER_ERROR', message: partial.message ?? 'Provider request failed', providerMessage: partial.providerMessage, providerCode: partial.providerCode, providerTrackingCode: partial.providerTrackingCode, details: partial.details, conflict: partial.conflict, }; } protected formatProviderError( providerMessage?: string, providerCode?: string, fallbackMessage = 'Provider request failed', extras?: Pick, ): Error { const message = providerMessage?.trim() || fallbackMessage; const code = providerCode?.trim() || 'PROVIDER_ERROR'; const normalized = this.normalizeError({ code, message, providerMessage: message, providerCode: code, ...extras, }); const err = new Error(message); (err as Error & { normalizedError: NormalizedErrorDto }).normalizedError = normalized; return err; } protected formatAxiosError(error: AxiosError): Error { const data = error.response?.data; if (typeof data === 'string') { const faultMatch = data.match( /<(?:\w+:)?faultstring[^>]*>([\s\S]*?)<\/(?:\w+:)?faultstring>/i, ); if (faultMatch?.[1]) { return this.formatProviderError( faultMatch[1].trim().replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'), String(error.response?.status ?? 'NETWORK_ERROR'), ); } } return this.formatProviderError( error.message, String(error.response?.status ?? 'NETWORK_ERROR'), ); } protected abstract callProvider( inquiryType: InquiryType, payload: TRequest, context: ProviderExecutionContext, ): Promise; }