378 lines
No EOL
12 KiB
JavaScript
378 lines
No EOL
12 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);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 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<Object>} 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; |