xpeditis2.0/apps/backend/src/infrastructure/carriers/msc/msc.mapper.ts
2025-10-21 21:18:01 +02:00

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));
}
}