E-Commerce-Module/backend/src/services/shippingService.js
2025-04-27 22:31:16 -05:00

250 lines
No EOL
7.7 KiB
JavaScript

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