/** * 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, 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 = { '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)); } }