/** * 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('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('CMACGM_CLIENT_ID', ''); this.clientSecret = this.configService.get('CMACGM_CLIENT_SECRET', ''); } async searchRates(input: CarrierRateSearchInput): Promise { 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 { 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 { // 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'); } } }