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); } }, /** * 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) 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().includes(carrier.toUpperCase()) ) ); 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;