xpeditis2.0/apps/backend/src/infrastructure/carriers/base-carrier.connector.ts
David 4b00ee2601
Some checks failed
CI/CD Pipeline - Xpeditis PreProd / Frontend - Docker Build & Push (push) Blocked by required conditions
CI/CD Pipeline - Xpeditis PreProd / Deploy to PreProd Server (push) Blocked by required conditions
CI/CD Pipeline - Xpeditis PreProd / Run Smoke Tests (push) Blocked by required conditions
CI/CD Pipeline - Xpeditis PreProd / Backend - Build & Test (push) Failing after 5m53s
CI/CD Pipeline - Xpeditis PreProd / Backend - Docker Build & Push (push) Has been skipped
CI/CD Pipeline - Xpeditis PreProd / Frontend - Build & Test (push) Has been cancelled
fix: replace relative domain imports with TypeScript path aliases
- Replace all ../../domain/ imports with @domain/ across 67 files
- Configure NestJS to use tsconfig.build.json with rootDir
- Add tsc-alias to resolve path aliases after build
- This fixes 'Cannot find module' TypeScript compilation errors

Fixed files:
- 30 files in application layer
- 37 files in infrastructure layer
2025-11-16 19:20:58 +01:00

170 lines
5.4 KiB
TypeScript

/**
* Base Carrier Connector
*
* Abstract base class for carrier API integrations
* Provides common functionality: HTTP client, retry logic, circuit breaker, logging
*/
import { Logger } from '@nestjs/common';
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
import CircuitBreaker from 'opossum';
import {
CarrierConnectorPort,
CarrierRateSearchInput,
CarrierAvailabilityInput,
} from '@domain/ports/out/carrier-connector.port';
import { RateQuote } from '@domain/entities/rate-quote.entity';
import { CarrierTimeoutException } from '@domain/exceptions/carrier-timeout.exception';
import { CarrierUnavailableException } from '@domain/exceptions/carrier-unavailable.exception';
export interface CarrierConfig {
name: string;
code: string;
baseUrl: string;
timeout: number; // milliseconds
maxRetries: number;
circuitBreakerThreshold: number; // failure threshold before opening circuit
circuitBreakerTimeout: number; // milliseconds to wait before half-open
}
export abstract class BaseCarrierConnector implements CarrierConnectorPort {
protected readonly logger: Logger;
protected readonly httpClient: AxiosInstance;
protected readonly circuitBreaker: CircuitBreaker;
constructor(protected readonly config: CarrierConfig) {
this.logger = new Logger(`${config.name}Connector`);
// Create HTTP client
this.httpClient = axios.create({
baseURL: config.baseUrl,
timeout: config.timeout,
headers: {
'Content-Type': 'application/json',
'User-Agent': 'Xpeditis/1.0',
},
});
// Request interceptor
this.httpClient.interceptors.request.use(
request => {
this.logger.debug(
`Request: ${request.method?.toUpperCase()} ${request.url}`,
request.data ? JSON.stringify(request.data).substring(0, 200) : ''
);
return request;
},
error => {
this.logger.error(`Request error: ${error?.message || 'Unknown error'}`);
return Promise.reject(error);
}
);
// Response interceptor
this.httpClient.interceptors.response.use(
response => {
this.logger.debug(`Response: ${response.status} ${response.statusText}`);
return response;
},
error => {
if (error?.code === 'ECONNABORTED') {
this.logger.warn(`Request timeout after ${config.timeout}ms`);
throw new CarrierTimeoutException(config.name, config.timeout);
}
this.logger.error(`Response error: ${error?.message || 'Unknown error'}`);
return Promise.reject(error);
}
);
// Circuit breaker
this.circuitBreaker = new CircuitBreaker(this.makeRequest.bind(this), {
timeout: config.timeout,
errorThresholdPercentage: config.circuitBreakerThreshold,
resetTimeout: config.circuitBreakerTimeout,
name: `${config.name}-circuit-breaker`,
});
this.circuitBreaker.on('open', () =>
this.logger.warn('Circuit breaker opened - carrier unavailable')
);
this.circuitBreaker.on('halfOpen', () =>
this.logger.log('Circuit breaker half-open - testing carrier availability')
);
this.circuitBreaker.on('close', () =>
this.logger.log('Circuit breaker closed - carrier available')
);
}
getCarrierName(): string {
return this.config.name;
}
getCarrierCode(): string {
return this.config.code;
}
protected async makeRequest<T>(
config: AxiosRequestConfig,
retries = this.config.maxRetries
): Promise<AxiosResponse<T>> {
try {
return await this.httpClient.request<T>(config);
} catch (error: any) {
if (retries > 0 && this.isRetryableError(error)) {
const delay = this.calculateRetryDelay(this.config.maxRetries - retries);
this.logger.warn(`Request failed, retrying in ${delay}ms (${retries} retries left)`);
await this.sleep(delay);
return this.makeRequest<T>(config, retries - 1);
}
throw error;
}
}
protected isRetryableError(error: any): boolean {
if (error.code === 'ECONNABORTED') return false;
if (error.code === 'ENOTFOUND') return false;
if (error.response) {
const status = error.response.status;
return status >= 500 && status < 600;
}
return true;
}
protected calculateRetryDelay(attempt: number): number {
const baseDelay = 1000;
const maxDelay = 5000;
const delay = Math.min(baseDelay * Math.pow(2, attempt), maxDelay);
return delay + Math.random() * 1000; // jitter
}
protected sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
protected async requestWithCircuitBreaker<T>(
config: AxiosRequestConfig
): Promise<AxiosResponse<T>> {
try {
return (await this.circuitBreaker.fire(config)) as AxiosResponse<T>;
} catch (error: any) {
if (error?.message === 'Breaker is open') {
throw new CarrierUnavailableException(this.config.name, 'Circuit breaker is open');
}
throw error;
}
}
async healthCheck(): Promise<boolean> {
try {
await this.requestWithCircuitBreaker({ method: 'GET', url: '/health', timeout: 5000 });
return true;
} catch (error: any) {
this.logger.warn(`Health check failed: ${error?.message || 'Unknown error'}`);
return false;
}
}
abstract searchRates(input: CarrierRateSearchInput): Promise<RateQuote[]>;
abstract checkAvailability(input: CarrierAvailabilityInput): Promise<number>;
}