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

157 lines
5.1 KiB
TypeScript

/**
* CMA CGM Request/Response Mapper
*/
import { Injectable } from '@nestjs/common';
import { CarrierRateSearchInput } from '../../../domain/ports/out/carrier-connector.port';
import { RateQuote, RouteSegment, Surcharge } from '../../../domain/entities/rate-quote.entity';
import { v4 as uuidv4 } from 'uuid';
@Injectable()
export class CMACGMRequestMapper {
toCMACGMRequest(input: CarrierRateSearchInput): any {
return {
departure_port_locode: input.origin,
arrival_port_locode: input.destination,
equipment_type_code: this.mapContainerType(input.containerType),
cargo_ready_date: input.departureDate,
shipment_term: input.mode,
cargo_type: input.isHazmat ? 'HAZARDOUS' : 'GENERAL',
imo_class: input.imoClass,
gross_weight_kg: input.weight,
volume_cbm: input.volume,
};
}
fromCMACGMResponse(cgmResponse: any, originalInput: CarrierRateSearchInput): RateQuote[] {
if (!cgmResponse.quotations || cgmResponse.quotations.length === 0) {
return [];
}
return cgmResponse.quotations.map((quotation: any) => {
const surcharges: Surcharge[] = [
{
type: 'BAF',
description: 'Bunker Surcharge',
amount: quotation.charges?.bunker_surcharge || 0,
currency: quotation.charges?.currency || 'USD',
},
{
type: 'CAF',
description: 'Currency Surcharge',
amount: quotation.charges?.currency_surcharge || 0,
currency: quotation.charges?.currency || 'USD',
},
{
type: 'PSS',
description: 'Peak Season',
amount: quotation.charges?.peak_season || 0,
currency: quotation.charges?.currency || 'USD',
},
{
type: 'THC',
description: 'Terminal Handling',
amount: quotation.charges?.thc || 0,
currency: quotation.charges?.currency || 'USD',
},
].filter(s => s.amount > 0);
const baseFreight = quotation.charges?.ocean_freight || 0;
const totalSurcharges = surcharges.reduce((sum, s) => sum + s.amount, 0);
const totalAmount = baseFreight + totalSurcharges;
// Build route segments
const route: RouteSegment[] = [];
// Origin port
route.push({
portCode: originalInput.origin,
portName: quotation.routing?.departure_port_name || originalInput.origin,
departure: new Date(quotation.schedule?.departure_date),
vesselName: quotation.vessel?.name,
voyageNumber: quotation.vessel?.voyage,
});
// Transshipment ports
if (
quotation.routing?.transshipment_ports &&
Array.isArray(quotation.routing.transshipment_ports)
) {
quotation.routing.transshipment_ports.forEach((port: any) => {
route.push({
portCode: port.code || port,
portName: port.name || port.code || port,
});
});
}
// Destination port
route.push({
portCode: originalInput.destination,
portName: quotation.routing?.arrival_port_name || originalInput.destination,
arrival: new Date(quotation.schedule?.arrival_date),
});
const transitDays =
quotation.schedule?.transit_time_days ||
this.calculateTransitDays(
quotation.schedule?.departure_date,
quotation.schedule?.arrival_date
);
return RateQuote.create({
id: uuidv4(),
carrierId: 'cmacgm',
carrierName: 'CMA CGM',
carrierCode: 'CMDU',
origin: {
code: originalInput.origin,
name: quotation.routing?.departure_port_name || originalInput.origin,
country: quotation.routing?.departure_country || 'Unknown',
},
destination: {
code: originalInput.destination,
name: quotation.routing?.arrival_port_name || originalInput.destination,
country: quotation.routing?.arrival_country || 'Unknown',
},
pricing: {
baseFreight,
surcharges,
totalAmount,
currency: quotation.charges?.currency || 'USD',
},
containerType: originalInput.containerType,
mode: (originalInput.mode as 'FCL' | 'LCL') || 'FCL',
etd: new Date(quotation.schedule?.departure_date),
eta: new Date(quotation.schedule?.arrival_date),
transitDays,
route,
availability: quotation.capacity?.slots_available || 0,
frequency: quotation.service?.frequency || 'Weekly',
vesselType: 'Container Ship',
co2EmissionsKg: quotation.environmental?.co2_kg,
});
});
}
private mapContainerType(type: string): string {
const mapping: Record<string, string> = {
'20GP': '22G1',
'40GP': '42G1',
'40HC': '45G1',
'45HC': '45G1',
'20RF': '22R1',
'40RF': '42R1',
};
return mapping[type] || type;
}
private calculateTransitDays(departure?: string, arrival?: string): number {
if (!departure || !arrival) return 0;
const depDate = new Date(departure);
const arrDate = new Date(arrival);
const diff = arrDate.getTime() - depDate.getTime();
return Math.ceil(diff / (1000 * 60 * 60 * 24));
}
}