157 lines
5.1 KiB
TypeScript
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));
|
|
}
|
|
}
|