xpeditis2.0/apps/backend/src/infrastructure/carriers/cma-cgm/cma-cgm.connector.ts
2025-10-27 20:54:01 +01:00

133 lines
4.1 KiB
TypeScript

/**
* CMA CGM Connector
*
* Implements CarrierConnectorPort for CMA CGM WebAccess API integration
*/
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import {
CarrierConnectorPort,
CarrierRateSearchInput,
CarrierAvailabilityInput,
} from '../../../domain/ports/out/carrier-connector.port';
import { RateQuote } from '../../../domain/entities/rate-quote.entity';
import { BaseCarrierConnector, CarrierConfig } from '../base-carrier.connector';
import { CMACGMRequestMapper } from './cma-cgm.mapper';
@Injectable()
export class CMACGMConnectorAdapter extends BaseCarrierConnector implements CarrierConnectorPort {
private readonly apiUrl: string;
private readonly clientId: string;
private readonly clientSecret: string;
constructor(
private readonly configService: ConfigService,
private readonly requestMapper: CMACGMRequestMapper
) {
const config: CarrierConfig = {
name: 'CMA CGM',
code: 'CMDU',
baseUrl: configService.get<string>('CMACGM_API_URL', 'https://api.cma-cgm.com/v1'),
timeout: 5000,
maxRetries: 3,
circuitBreakerThreshold: 50,
circuitBreakerTimeout: 30000,
};
super(config);
this.apiUrl = config.baseUrl;
this.clientId = this.configService.get<string>('CMACGM_CLIENT_ID', '');
this.clientSecret = this.configService.get<string>('CMACGM_CLIENT_SECRET', '');
}
async searchRates(input: CarrierRateSearchInput): Promise<RateQuote[]> {
this.logger.log(`Searching CMA CGM rates: ${input.origin} -> ${input.destination}`);
try {
// Get OAuth token first
const accessToken = await this.getAccessToken();
// Map to CMA CGM format
const cgmRequest = this.requestMapper.toCMACGMRequest(input);
// Make API call
const response = await this.makeRequest({
url: `${this.apiUrl}/quotations/search`,
method: 'POST',
data: cgmRequest,
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
});
// Map response to domain
const rateQuotes = this.requestMapper.fromCMACGMResponse(response.data, input);
this.logger.log(`Found ${rateQuotes.length} CMA CGM rates`);
return rateQuotes;
} catch (error: any) {
this.logger.error(`CMA CGM API error: ${error?.message || 'Unknown error'}`);
if (error?.response?.status === 401) {
this.logger.error('CMA CGM authentication failed');
throw new Error('CMACGM_AUTH_FAILED');
}
return [];
}
}
async checkAvailability(input: CarrierAvailabilityInput): Promise<number> {
try {
const accessToken = await this.getAccessToken();
const response = await this.makeRequest({
url: `${this.apiUrl}/capacity/check`,
method: 'POST',
data: {
departure_port: input.origin,
arrival_port: input.destination,
departure_date: input.departureDate,
equipment_type: input.containerType,
quantity: input.quantity,
},
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
return (response.data as any).capacity_available || 0;
} catch (error: any) {
this.logger.error(`CMA CGM availability check error: ${error?.message || 'Unknown error'}`);
return 0;
}
}
/**
* Get OAuth access token
*/
private async getAccessToken(): Promise<string> {
// In production, implement token caching
try {
const response = await this.makeRequest({
url: `${this.apiUrl}/oauth/token`,
method: 'POST',
data: {
grant_type: 'client_credentials',
client_id: this.clientId,
client_secret: this.clientSecret,
},
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
});
return (response.data as any).access_token;
} catch (error: any) {
this.logger.error(`Failed to get CMA CGM access token: ${error?.message || 'Unknown error'}`);
throw new Error('CMACGM_TOKEN_ERROR');
}
}
}