const axios = require('axios'); const config = require('../config'); /** * Service for handling shipping operations with EasyPost API */ const shippingService = { /** * Create a shipment in EasyPost and get available rates * @param {Object} addressFrom - Shipping origin address * @param {Object} addressTo - Customer shipping address * @param {Object} parcelDetails - Package dimensions and weight * @returns {Promise} Array of available shipping rates */ async getShippingRates(addressFrom, addressTo, parcelDetails) { // If EasyPost is not enabled, return flat rate shipping if (!config.shipping.easypostEnabled || !config.shipping.easypostApiKey) { console.log("EASY POST NOT CONFIGURED ", !config.shipping.easypostEnabled, !config.shipping.easypostApiKey) return this.getFlatRateShipping(parcelDetails.order_total); } try { // Format addresses for EasyPost const fromAddress = this.formatAddress(addressFrom || config.shipping.originAddress); const toAddress = this.formatAddress(addressTo); // Format parcel for EasyPost const parcel = this.formatParcel(parcelDetails); console.log("EasyPost shipment request", JSON.stringify({ shipment: { from_address: fromAddress, to_address: toAddress, parcel: parcel } }, null , 4)) // Create shipment via EasyPost API const response = await axios.post( 'https://api.easypost.com/beta/rates', { shipment: { from_address: fromAddress, to_address: toAddress, parcel: parcel } }, { auth: { username: config.shipping.easypostApiKey, password: '' }, headers: { 'Content-Type': 'application/json' } } ); // console.log("EasyPost shipment response", response) // Process and filter rates return this.processShippingRates(response.data.rates, parcelDetails.order_total); } catch (error) { console.error('EasyPost API error:', error.response?.data || error.message); // Fallback to flat rate if API fails return this.getFlatRateShipping(parcelDetails.order_total); } }, /** * Create an actual shipment in EasyPost based on selected rate * @param {Object} addressFrom - Shipping origin address * @param {Object} addressTo - Customer shipping address * @param {Object} parcelDetails - Package dimensions and weight * @param {string} selectedRateId - The ID of the selected rate * @returns {Promise} Created shipment details */ async createShipment(addressFrom, addressTo, parcelDetails, selectedRateId) { // If EasyPost is not enabled, return a simulated shipment if (!config.shipping.easypostEnabled || !config.shipping.easypostApiKey) { console.log("EasyPost not configured for shipment creation"); return this.createSimulatedShipment(selectedRateId); } try { // Format addresses for EasyPost const fromAddress = this.formatAddress(addressFrom || config.shipping.originAddress); const toAddress = this.formatAddress(addressTo); // Format parcel for EasyPost const parcel = this.formatParcel(parcelDetails); // Step 1: Create a shipment const shipmentResponse = await axios.post( 'https://api.easypost.com/v2/shipments', { shipment: { from_address: fromAddress, to_address: toAddress, parcel: parcel, options: { label_format: 'PDF', label_size: '4x6' } } }, { auth: { username: config.shipping.easypostApiKey, password: '' }, headers: { 'Content-Type': 'application/json' } } ); const shipment = shipmentResponse.data; console.log("Shipment created successfully:", shipment.id); // Step 2: Buy the selected rate const buyResponse = await axios.post( `https://api.easypost.com/v2/shipments/${shipment.id}/buy`, { rate: { id: selectedRateId } }, { auth: { username: config.shipping.easypostApiKey, password: '' }, headers: { 'Content-Type': 'application/json' } } ); const purchasedShipment = buyResponse.data; console.log("Rate purchased successfully:", purchasedShipment.id); // Return the important details return { shipment_id: purchasedShipment.id, tracking_code: purchasedShipment.tracking_code, label_url: purchasedShipment.postage_label.label_url, selected_rate: { id: purchasedShipment.selected_rate.id, carrier: purchasedShipment.selected_rate.carrier, service: purchasedShipment.selected_rate.service, rate: parseFloat(purchasedShipment.selected_rate.rate), delivery_days: purchasedShipment.selected_rate.delivery_days || 'Unknown', delivery_date: purchasedShipment.selected_rate.delivery_date || null }, carrier: purchasedShipment.selected_rate.carrier, service: purchasedShipment.selected_rate.service, created_at: purchasedShipment.created_at, status: purchasedShipment.status }; } catch (error) { console.error('EasyPost API error during shipment creation:', error.response?.data || error.message); // Return a fallback simulated shipment return this.createSimulatedShipment(selectedRateId); } }, /** * Create a simulated shipment when EasyPost is not available * @param {string} selectedRateId - The ID of the selected rate * @returns {Object} Simulated shipment details */ createSimulatedShipment(selectedRateId) { // Generate a random tracking number const trackingNumber = `SIMSHIP${Math.floor(Math.random() * 1000000000)}`; // Return a simulated shipment return { shipment_id: `sim_${Date.now()}`, tracking_code: trackingNumber, label_url: null, selected_rate: { id: selectedRateId, carrier: selectedRateId.includes('flat') ? 'Standard' : 'Simulated', service: selectedRateId.includes('flat') ? 'Flat Rate Shipping' : 'Standard Service', rate: selectedRateId.includes('free') ? 0 : config.shipping.flatRate, delivery_days: '5-7', delivery_date: null }, carrier: selectedRateId.includes('flat') ? 'Standard' : 'Simulated', service: selectedRateId.includes('flat') ? 'Flat Rate Shipping' : 'Standard Service', created_at: new Date().toISOString(), status: 'simulated' }; }, /** * Format address for EasyPost API * @param {Object} address - Address details * @returns {Object} Formatted address object */ formatAddress(address) { return { street1: address.street || address.street1, city: address.city, state: address.state || address.province, zip: address.zip || address.postalCode, country: address.country, name: address.name || undefined, company: address.company || undefined, phone: address.phone || undefined, email: address.email || undefined }; }, /** * Format parcel for EasyPost API * @param {Object} parcelDetails - Package dimensions and weight * @returns {Object} Formatted parcel object */ formatParcel(parcelDetails) { const pkg = config.shipping.defaultPackage; // Convert weight to ounces if coming from grams const weight = parcelDetails.weight || 500; // Default to 500g if not provided const weightOz = pkg.weightUnit === 'g' ? weight * 0.035274 : weight; // Convert dimensions to inches if coming from cm const lengthConversionFactor = pkg.unit === 'cm' ? 0.393701 : 1; return { length: (parcelDetails.length || pkg.length) * lengthConversionFactor, width: (parcelDetails.width || pkg.width) * lengthConversionFactor, height: (parcelDetails.height || pkg.height) * lengthConversionFactor, weight: weightOz, predefined_package: parcelDetails.predefined_package || null }; }, /** * Process and filter shipping rates from EasyPost * @param {Array} rates - EasyPost shipping rates * @param {number} orderTotal - Order total amount * @returns {Array} Processed shipping rates */ processShippingRates(rates, orderTotal) { console.log("processShippingRates", rates, orderTotal, config.shipping.carriersAllowed) if (!rates || !Array.isArray(rates)) { return this.getFlatRateShipping(orderTotal); } // Filter by allowed carriers let filteredRates = rates.filter(rate => config.shipping.carriersAllowed.some(carrier => rate.carrier.toUpperCase().replaceAll(' ', '').includes(carrier.toUpperCase().replaceAll(' ', '')) ) ); if (filteredRates.length === 0) { return this.getFlatRateShipping(orderTotal); } // Format rates to standardized format const formattedRates = filteredRates.map(rate => ({ id: rate.id, carrier: rate.carrier, service: rate.service, rate: parseFloat(rate.rate), currency: rate.currency, delivery_days: rate.delivery_days || 'Unknown', delivery_date: rate.delivery_date || null, delivery_time: rate.est_delivery_time || null })); // // Check if free shipping applies // if (orderTotal >= config.shipping.freeThreshold) { // formattedRates.push({ // id: 'free-shipping', // carrier: 'FREE', // service: 'Standard Shipping', // rate: 0, // currency: 'USD', // delivery_days: '5-7', // delivery_date: null, // delivery_time: null // }); // } return formattedRates; }, /** * Get flat rate shipping as fallback * @param {number} orderTotal - Order total amount * @returns {Array} Flat rate shipping options */ getFlatRateShipping(orderTotal) { const shippingOptions = [{ id: 'flat-rate', carrier: 'Standard', service: 'Flat Rate Shipping', rate: config.shipping.flatRate, currency: 'USD', delivery_days: '5-7', delivery_date: null, delivery_time: null }]; // Add free shipping if order qualifies if (orderTotal >= config.shipping.freeThreshold) { shippingOptions.push({ id: 'free-shipping', carrier: 'FREE', service: 'Standard Shipping', rate: 0, currency: 'USD', delivery_days: '5-7', delivery_date: null, delivery_time: null }); } return shippingOptions; }, /** * Parse shipping address from string format * @param {string} addressString - Shipping address as string * @returns {Object} Parsed address object */ parseAddressString(addressString) { const lines = addressString.trim().split('\n').map(line => line.trim()); // Try to intelligently parse the address components // This is a simplified version - might need enhancement for edge cases const parsedAddress = { name: lines[0] || '', street: lines[1] || '', city: '', state: '', zip: '', country: lines[lines.length - 1] || '' }; // Try to parse city, state, zip from line 2 if (lines[2]) { const cityStateZip = lines[2].split(','); if (cityStateZip.length >= 2) { parsedAddress.city = cityStateZip[0].trim(); // Split state and zip const stateZip = cityStateZip[1].trim().split(' '); if (stateZip.length >= 2) { parsedAddress.state = stateZip[0].trim(); parsedAddress.zip = stateZip.slice(1).join(' ').trim(); } else { parsedAddress.state = stateZip[0].trim(); } } else { parsedAddress.city = lines[2]; } } return parsedAddress; }, /** * Calculate total shipping weight from cart items * @param {Array} items - Cart items * @returns {number} Total weight in grams */ calculateTotalWeight(items) { return items.reduce((total, item) => { const itemWeight = item.weight_grams || 100; return total + (itemWeight * item.quantity); }, 0); } }; module.exports = shippingService;