159 lines
4.7 KiB
TypeScript
159 lines
4.7 KiB
TypeScript
/**
|
|
* MSC Request/Response Mapper
|
|
*
|
|
* Maps between internal domain format and MSC API format
|
|
*/
|
|
|
|
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 MSCRequestMapper {
|
|
/**
|
|
* Map internal format to MSC API request format
|
|
*/
|
|
toMSCRequest(input: CarrierRateSearchInput): any {
|
|
return {
|
|
pol: input.origin, // Port of Loading
|
|
pod: input.destination, // Port of Discharge
|
|
container_type: this.mapContainerType(input.containerType),
|
|
cargo_ready_date: input.departureDate,
|
|
service_mode: input.mode === 'FCL' ? 'FCL' : 'LCL',
|
|
commodity_code: 'FAK', // Freight All Kinds (default)
|
|
is_dangerous: input.isHazmat || false,
|
|
imo_class: input.imoClass,
|
|
weight_kg: input.weight,
|
|
volume_cbm: input.volume,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Map MSC response to domain RateQuote entities
|
|
*/
|
|
fromMSCResponse(mscResponse: any, originalInput: CarrierRateSearchInput): RateQuote[] {
|
|
if (!mscResponse.quotes || mscResponse.quotes.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
return mscResponse.quotes.map((quote: any) => {
|
|
// Calculate surcharges
|
|
const surcharges: Surcharge[] = [
|
|
{
|
|
type: 'BAF',
|
|
description: 'Bunker Adjustment Factor',
|
|
amount: quote.surcharges?.baf || 0,
|
|
currency: quote.currency || 'USD',
|
|
},
|
|
{
|
|
type: 'CAF',
|
|
description: 'Currency Adjustment Factor',
|
|
amount: quote.surcharges?.caf || 0,
|
|
currency: quote.currency || 'USD',
|
|
},
|
|
{
|
|
type: 'PSS',
|
|
description: 'Peak Season Surcharge',
|
|
amount: quote.surcharges?.pss || 0,
|
|
currency: quote.currency || 'USD',
|
|
},
|
|
].filter(s => s.amount > 0);
|
|
|
|
const totalSurcharges = surcharges.reduce((sum, s) => sum + s.amount, 0);
|
|
const baseFreight = quote.ocean_freight || 0;
|
|
const totalAmount = baseFreight + totalSurcharges;
|
|
|
|
// Build route segments
|
|
const route: RouteSegment[] = [];
|
|
|
|
// Origin port
|
|
route.push({
|
|
portCode: originalInput.origin,
|
|
portName: quote.pol_name || originalInput.origin,
|
|
departure: new Date(quote.etd),
|
|
vesselName: quote.vessel_name,
|
|
voyageNumber: quote.voyage_number,
|
|
});
|
|
|
|
// Transshipment ports
|
|
if (quote.via_ports && Array.isArray(quote.via_ports)) {
|
|
quote.via_ports.forEach((port: any) => {
|
|
route.push({
|
|
portCode: port.code || port,
|
|
portName: port.name || port.code || port,
|
|
});
|
|
});
|
|
}
|
|
|
|
// Destination port
|
|
route.push({
|
|
portCode: originalInput.destination,
|
|
portName: quote.pod_name || originalInput.destination,
|
|
arrival: new Date(quote.eta),
|
|
});
|
|
|
|
const transitDays = quote.transit_days || this.calculateTransitDays(quote.etd, quote.eta);
|
|
|
|
// Create rate quote
|
|
return RateQuote.create({
|
|
id: uuidv4(),
|
|
carrierId: 'msc',
|
|
carrierName: 'MSC',
|
|
carrierCode: 'MSCU',
|
|
origin: {
|
|
code: originalInput.origin,
|
|
name: quote.pol_name || originalInput.origin,
|
|
country: quote.pol_country || 'Unknown',
|
|
},
|
|
destination: {
|
|
code: originalInput.destination,
|
|
name: quote.pod_name || originalInput.destination,
|
|
country: quote.pod_country || 'Unknown',
|
|
},
|
|
pricing: {
|
|
baseFreight,
|
|
surcharges,
|
|
totalAmount,
|
|
currency: quote.currency || 'USD',
|
|
},
|
|
containerType: originalInput.containerType,
|
|
mode: (originalInput.mode as 'FCL' | 'LCL') || 'FCL',
|
|
etd: new Date(quote.etd),
|
|
eta: new Date(quote.eta),
|
|
transitDays,
|
|
route,
|
|
availability: quote.available_slots || 0,
|
|
frequency: quote.frequency || 'Weekly',
|
|
vesselType: 'Container Ship',
|
|
co2EmissionsKg: quote.co2_kg,
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Map internal container type to MSC format
|
|
*/
|
|
private mapContainerType(type: string): string {
|
|
const mapping: Record<string, string> = {
|
|
'20GP': '20DC',
|
|
'40GP': '40DC',
|
|
'40HC': '40HC',
|
|
'45HC': '45HC',
|
|
'20RF': '20RF',
|
|
'40RF': '40RF',
|
|
};
|
|
return mapping[type] || type;
|
|
}
|
|
|
|
/**
|
|
* Calculate transit days from ETD and ETA
|
|
*/
|
|
private calculateTransitDays(etd: string, eta: string): number {
|
|
const etdDate = new Date(etd);
|
|
const etaDate = new Date(eta);
|
|
const diff = etaDate.getTime() - etdDate.getTime();
|
|
return Math.ceil(diff / (1000 * 60 * 60 * 24));
|
|
}
|
|
}
|