/** * ONE (Ocean Network Express) 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 ONERequestMapper { toONERequest(input: CarrierRateSearchInput): any { return { loading_port: input.origin, discharge_port: input.destination, equipment_type: this.mapContainerType(input.containerType), cargo_cutoff_date: input.departureDate, shipment_type: input.mode, dangerous_cargo: input.isHazmat || false, imo_class: input.imoClass, cargo_weight_kg: input.weight, cargo_volume_cbm: input.volume, }; } fromONEResponse(oneResponse: any, originalInput: CarrierRateSearchInput): RateQuote[] { if (!oneResponse.instant_quotes || oneResponse.instant_quotes.length === 0) { return []; } return oneResponse.instant_quotes.map((quote: any) => { const surcharges: Surcharge[] = []; // Parse surcharges if (quote.additional_charges) { for (const [key, value] of Object.entries(quote.additional_charges)) { if (typeof value === 'number' && value > 0) { surcharges.push({ type: key.toUpperCase(), description: this.formatChargeName(key), amount: value, currency: quote.currency || 'USD', }); } } } const baseFreight = quote.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: quote.loading_port_name || originalInput.origin, departure: new Date(quote.departure_date), vesselName: quote.vessel_details?.name, voyageNumber: quote.vessel_details?.voyage, }); // 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.discharge_port_name || originalInput.destination, arrival: new Date(quote.arrival_date), }); const transitDays = quote.transit_days || this.calculateTransitDays(quote.departure_date, quote.arrival_date); return RateQuote.create({ id: uuidv4(), carrierId: 'one', carrierName: 'ONE', carrierCode: 'ONEY', origin: { code: originalInput.origin, name: quote.loading_port_name || originalInput.origin, country: quote.loading_country || 'Unknown', }, destination: { code: originalInput.destination, name: quote.discharge_port_name || originalInput.destination, country: quote.discharge_country || 'Unknown', }, pricing: { baseFreight, surcharges, totalAmount, currency: quote.currency || 'USD', }, containerType: originalInput.containerType, mode: originalInput.mode, etd: new Date(quote.departure_date), eta: new Date(quote.arrival_date), transitDays, route, availability: quote.capacity_status?.available || 0, frequency: quote.service_info?.frequency || 'Weekly', vesselType: 'Container Ship', co2EmissionsKg: quote.environmental_info?.co2_emissions, }); }); } private mapContainerType(type: string): string { const mapping: Record = { '20GP': '20DV', '40GP': '40DV', '40HC': '40HC', '45HC': '45HC', '20RF': '20RF', '40RF': '40RH', }; return mapping[type] || type; } private formatChargeName(key: string): string { return key .split('_') .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) .join(' '); } 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)); } }