full shipping flow
This commit is contained in:
parent
57c5b6f864
commit
d9953baa19
4 changed files with 152 additions and 132 deletions
|
|
@ -475,7 +475,8 @@ module.exports = (pool, query, authMiddleware) => {
|
|||
|
||||
// Get cart items with product weights
|
||||
const cartItemsResult = await query(
|
||||
`SELECT ci.quantity, p.id, p.weight_grams, p.price
|
||||
`SELECT ci.quantity, p.id, p.weight_grams, p.price,
|
||||
p.length_cm, p.width_cm, p.height_cm
|
||||
FROM cart_items ci
|
||||
JOIN products p ON ci.product_id = p.id
|
||||
WHERE ci.cart_id = $1`,
|
||||
|
|
@ -497,34 +498,55 @@ module.exports = (pool, query, authMiddleware) => {
|
|||
|
||||
// If no address provided, return only flat rate shipping
|
||||
if (!shippingAddress) {
|
||||
console.log("No Address provide flat rate");
|
||||
const rates = await shippingService.getFlatRateShipping(subtotal);
|
||||
console.log("No Address provided - using flat rate");
|
||||
const rates = shippingService.getFlatRateShipping(subtotal);
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
shipment_id: null,
|
||||
rates
|
||||
});
|
||||
}
|
||||
|
||||
// Get real shipping rates
|
||||
// Get real shipping rates with a shipment
|
||||
const parsedAddress = typeof shippingAddress === 'string'
|
||||
? shippingService.parseAddressString(shippingAddress)
|
||||
: shippingAddress;
|
||||
console.log("parsedAddress provided ", parsedAddress);
|
||||
|
||||
const rates = await shippingService.getShippingRates(
|
||||
console.log("Fetching rates for parsed address:", parsedAddress);
|
||||
|
||||
const shippingResponse = await shippingService.getShippingRates(
|
||||
null, // Use default from config
|
||||
parsedAddress,
|
||||
{
|
||||
weight: totalWeight,
|
||||
length: Math.max(...cartItemsResult.rows.map(item => item.length_cm || 0)),
|
||||
width: Math.max(...cartItemsResult.rows.map(item => item.width_cm || 0)),
|
||||
height: Math.max(...cartItemsResult.rows.map(item => item.height_cm || 0)),
|
||||
order_total: subtotal
|
||||
}
|
||||
);
|
||||
console.log("rates provided ", JSON.stringify(rates, null ,4));
|
||||
|
||||
console.log("Shipping rates response:", JSON.stringify(shippingResponse, null, 4));
|
||||
|
||||
// Save the shipment ID to the session or temporary storage
|
||||
// This will be used when the user selects a rate
|
||||
if (shippingResponse.shipment_id) {
|
||||
// Store the shipment ID in the user's cart for later use
|
||||
await query(
|
||||
`UPDATE carts SET metadata = jsonb_set(
|
||||
COALESCE(metadata, '{}'::jsonb),
|
||||
'{temp_shipment_id}',
|
||||
$1::jsonb
|
||||
) WHERE id = $2`,
|
||||
[JSON.stringify(shippingResponse.shipment_id), cartId]
|
||||
);
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
rates
|
||||
shipment_id: shippingResponse.shipment_id,
|
||||
rates: shippingResponse.rates
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error getting shipping rates:', error);
|
||||
|
|
@ -595,49 +617,54 @@ module.exports = (pool, query, authMiddleware) => {
|
|||
if (config.shipping.enabled) {
|
||||
// If a specific shipping method was selected
|
||||
if (shippingMethod && shippingMethod.id) {
|
||||
// Parse shipping address to object if it's a string
|
||||
const parsedAddress = typeof shippingAddress === 'string'
|
||||
? shippingService.parseAddressString(shippingAddress)
|
||||
: shippingAddress;
|
||||
shippingCost = parseFloat(shippingMethod.rate) || 0;
|
||||
|
||||
// Calculate total weight
|
||||
const totalWeight = shippingService.calculateTotalWeight(cartItemsResult.rows);
|
||||
|
||||
// Create an actual shipment on EasyPost if a real rate was selected (not flat/free)
|
||||
// Check if this is a non-flat rate and we need to purchase a real shipment
|
||||
if (config.shipping.easypostEnabled &&
|
||||
!shippingMethod.id.includes('flat-rate') &&
|
||||
!shippingMethod.id.includes('free-shipping')) {
|
||||
try {
|
||||
// Create parcel details
|
||||
const parcelDetails = {
|
||||
weight: totalWeight,
|
||||
order_total: subtotal
|
||||
};
|
||||
|
||||
// Create shipment with the selected rate
|
||||
shipmentData = await shippingService.createShipment(
|
||||
null, // Use default origin
|
||||
parsedAddress,
|
||||
parcelDetails,
|
||||
shippingMethod.id
|
||||
try {
|
||||
// Parse shipping address to object if it's a string
|
||||
const parsedAddress = typeof shippingAddress === 'string'
|
||||
? shippingService.parseAddressString(shippingAddress)
|
||||
: shippingAddress;
|
||||
|
||||
// Retrieve temporary shipment ID from cart metadata
|
||||
const cartMetadataResult = await query(
|
||||
'SELECT metadata FROM carts WHERE id = $1',
|
||||
[cartId]
|
||||
);
|
||||
|
||||
// Use the actual rate from the created shipment
|
||||
shippingCost = shipmentData.selected_rate.rate;
|
||||
let shipmentId = null;
|
||||
if (cartMetadataResult.rows.length > 0 &&
|
||||
cartMetadataResult.rows[0].metadata &&
|
||||
cartMetadataResult.rows[0].metadata.temp_shipment_id) {
|
||||
shipmentId = cartMetadataResult.rows[0].metadata.temp_shipment_id;
|
||||
}
|
||||
|
||||
console.log('Shipment created successfully:', shipmentData.shipment_id);
|
||||
if (shipmentId) {
|
||||
// Purchase the shipment with the selected rate
|
||||
shipmentData = await shippingService.purchaseShipment(
|
||||
shipmentId,
|
||||
shippingMethod.id
|
||||
);
|
||||
|
||||
console.log('Shipment purchased successfully:', shipmentData.shipment_id);
|
||||
|
||||
// Use the actual rate from the purchased shipment
|
||||
shippingCost = shipmentData.selected_rate.rate;
|
||||
} else {
|
||||
console.log('No shipment ID found in cart metadata, using standard rate');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating shipment:', error);
|
||||
// Fallback to the rate provided
|
||||
shippingCost = parseFloat(shippingMethod.rate) || 0;
|
||||
console.error('Error purchasing shipment:', error);
|
||||
// Continue with the rate provided
|
||||
}
|
||||
} else {
|
||||
// Use the rate provided for flat rate or free shipping
|
||||
shippingCost = parseFloat(shippingMethod.rate) || 0;
|
||||
}
|
||||
} else {
|
||||
// Default to flat rate if no method selected
|
||||
const shippingRates = await shippingService.getFlatRateShipping(subtotal);
|
||||
const shippingRates = shippingService.getFlatRateShipping(subtotal);
|
||||
shippingCost = shippingRates[0].rate;
|
||||
}
|
||||
}
|
||||
|
|
@ -666,7 +693,7 @@ module.exports = (pool, query, authMiddleware) => {
|
|||
);
|
||||
}
|
||||
|
||||
// If a shipping method was selected, save it with the order
|
||||
// If we have shipping details, save them with the order
|
||||
if (shippingMethod && shippingMethod.id) {
|
||||
const shippingInfo = shipmentData || {
|
||||
method_id: shippingMethod.id,
|
||||
|
|
@ -683,6 +710,12 @@ module.exports = (pool, query, authMiddleware) => {
|
|||
);
|
||||
}
|
||||
|
||||
// Clear the temporary shipment ID from cart metadata
|
||||
await client.query(
|
||||
`UPDATE carts SET metadata = metadata - 'temp_shipment_id' WHERE id = $1`,
|
||||
[cartId]
|
||||
);
|
||||
|
||||
await client.query('COMMIT');
|
||||
|
||||
// Send back cart items for Stripe checkout
|
||||
|
|
|
|||
|
|
@ -6,17 +6,20 @@ const config = require('../config');
|
|||
*/
|
||||
const shippingService = {
|
||||
/**
|
||||
* Create a shipment in EasyPost and get available rates
|
||||
* Create a shipment in EasyPost and get available rates with proper IDs
|
||||
* @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
|
||||
* @returns {Promise<Object>} Object containing shipment details and available 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);
|
||||
return {
|
||||
shipment_id: null,
|
||||
rates: this.getFlatRateShipping(parcelDetails.order_total)
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
@ -26,16 +29,10 @@ const shippingService = {
|
|||
|
||||
// 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
|
||||
|
||||
// Create shipment first to get proper rate IDs (using v2 API)
|
||||
const response = await axios.post(
|
||||
'https://api.easypost.com/beta/rates',
|
||||
'https://api.easypost.com/v2/shipments',
|
||||
{
|
||||
shipment: {
|
||||
from_address: fromAddress,
|
||||
|
|
@ -53,74 +50,48 @@ const shippingService = {
|
|||
}
|
||||
}
|
||||
);
|
||||
// console.log("EasyPost shipment response", response)
|
||||
|
||||
// Process and filter rates
|
||||
return this.processShippingRates(response.data.rates, parcelDetails.order_total);
|
||||
const shipment = response.data;
|
||||
console.log("Shipment created successfully:", shipment.id);
|
||||
|
||||
// Process and filter rates from the shipment object
|
||||
const formattedRates = this.processShippingRates(shipment.rates, parcelDetails.order_total);
|
||||
|
||||
// Return both the shipment ID and the rates
|
||||
return {
|
||||
shipment_id: shipment.id,
|
||||
rates: formattedRates
|
||||
};
|
||||
} 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);
|
||||
return {
|
||||
shipment_id: null,
|
||||
rates: 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
|
||||
* Purchase a selected rate for an existing shipment
|
||||
* @param {string} shipmentId - The EasyPost shipment ID
|
||||
* @param {string} rateId - The ID of the selected rate
|
||||
* @returns {Promise<Object>} Purchased 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);
|
||||
async purchaseShipment(shipmentId, rateId) {
|
||||
// If EasyPost is not enabled or we don't have a shipment ID, return a simulated shipment
|
||||
if (!config.shipping.easypostEnabled || !config.shipping.easypostApiKey || !shipmentId) {
|
||||
console.log("Cannot purchase shipment - EasyPost not configured or no shipment ID");
|
||||
return this.createSimulatedShipment(rateId);
|
||||
}
|
||||
|
||||
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
|
||||
// Buy the selected rate
|
||||
const buyResponse = await axios.post(
|
||||
`https://api.easypost.com/v2/shipments/${shipment.id}/buy`,
|
||||
`https://api.easypost.com/v2/shipments/${shipmentId}/buy`,
|
||||
{
|
||||
rate: {
|
||||
id: selectedRateId
|
||||
id: rateId
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -156,19 +127,19 @@ const shippingService = {
|
|||
status: purchasedShipment.status
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('EasyPost API error during shipment creation:', error.response?.data || error.message);
|
||||
console.error('EasyPost API error during purchasing shipment:', error.response?.data || error.message);
|
||||
|
||||
// Return a fallback simulated shipment
|
||||
return this.createSimulatedShipment(selectedRateId);
|
||||
return this.createSimulatedShipment(rateId);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a simulated shipment when EasyPost is not available
|
||||
* @param {string} selectedRateId - The ID of the selected rate
|
||||
* @param {string} rateId - The ID of the selected rate
|
||||
* @returns {Object} Simulated shipment details
|
||||
*/
|
||||
createSimulatedShipment(selectedRateId) {
|
||||
createSimulatedShipment(rateId) {
|
||||
// Generate a random tracking number
|
||||
const trackingNumber = `SIMSHIP${Math.floor(Math.random() * 1000000000)}`;
|
||||
|
||||
|
|
@ -178,15 +149,15 @@ const shippingService = {
|
|||
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,
|
||||
id: rateId,
|
||||
carrier: rateId.includes('flat') ? 'Standard' : 'Simulated',
|
||||
service: rateId.includes('flat') ? 'Flat Rate Shipping' : 'Standard Service',
|
||||
rate: rateId.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',
|
||||
carrier: rateId.includes('flat') ? 'Standard' : 'Simulated',
|
||||
service: rateId.includes('flat') ? 'Flat Rate Shipping' : 'Standard Service',
|
||||
created_at: new Date().toISOString(),
|
||||
status: 'simulated'
|
||||
};
|
||||
|
|
@ -242,7 +213,6 @@ const shippingService = {
|
|||
* @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);
|
||||
}
|
||||
|
|
@ -258,31 +228,31 @@ const shippingService = {
|
|||
return this.getFlatRateShipping(orderTotal);
|
||||
}
|
||||
|
||||
// Format rates to standardized format
|
||||
// Format rates to standardized format - now including the rate ID
|
||||
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_days: rate.est_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
|
||||
// });
|
||||
// }
|
||||
// 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;
|
||||
},
|
||||
|
|
|
|||
1
db/init/13-cart-metadata.sql
Normal file
1
db/init/13-cart-metadata.sql
Normal file
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE carts ADD COLUMN IF NOT EXISTS metadata JSONB DEFAULT '{}'::jsonb;
|
||||
|
|
@ -26,7 +26,6 @@ import { useNavigate, Link as RouterLink } from 'react-router-dom';
|
|||
import { useAuth, useCart } from '../hooks/reduxHooks';
|
||||
import { useCheckout } from '../hooks/apiHooks';
|
||||
import { useStripe, StripeElementsProvider } from '../context/StripeContext';
|
||||
import StripePaymentForm from '../components/StripePaymentForm';
|
||||
import apiClient from '../services/api';
|
||||
|
||||
// Checkout steps
|
||||
|
|
@ -51,6 +50,7 @@ const CheckoutPage = () => {
|
|||
const [shippingRates, setShippingRates] = useState([]);
|
||||
const [selectedShippingMethod, setSelectedShippingMethod] = useState(null);
|
||||
const [shippingCost, setShippingCost] = useState(0);
|
||||
const [shipmentId, setShipmentId] = useState(null);
|
||||
|
||||
// State for form data
|
||||
const [formData, setFormData] = useState({
|
||||
|
|
@ -72,6 +72,11 @@ const CheckoutPage = () => {
|
|||
...formData,
|
||||
[name]: name === 'saveAddress' ? checked : value,
|
||||
});
|
||||
|
||||
// Clear validation error when field is edited
|
||||
if (error) {
|
||||
setError(null);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle next step
|
||||
|
|
@ -87,8 +92,8 @@ const CheckoutPage = () => {
|
|||
return;
|
||||
}
|
||||
|
||||
// If valid, fetch shipping rates
|
||||
fetchShippingRates();
|
||||
return
|
||||
}
|
||||
|
||||
// If on shipping method step, validate selection
|
||||
|
|
@ -162,6 +167,12 @@ const CheckoutPage = () => {
|
|||
|
||||
if (response.data.rates && response.data.rates.length > 0) {
|
||||
setShippingRates(response.data.rates);
|
||||
|
||||
// Store shipment_id if provided (needed for actual shipment creation)
|
||||
if (response.data.shipment_id) {
|
||||
setShipmentId(response.data.shipment_id);
|
||||
}
|
||||
|
||||
// Default to lowest cost option
|
||||
const lowestCostOption = response.data.rates.reduce(
|
||||
(lowest, current) => current.rate < lowest.rate ? current : lowest,
|
||||
|
|
@ -172,9 +183,13 @@ const CheckoutPage = () => {
|
|||
} else {
|
||||
setShippingRates([]);
|
||||
setSelectedShippingMethod(null);
|
||||
setShipmentId(null);
|
||||
setShippingCost(0);
|
||||
setError('No shipping options available for this address');
|
||||
}
|
||||
|
||||
// Move to next step
|
||||
setActiveStep((prevStep) => prevStep + 1);
|
||||
} catch (error) {
|
||||
console.error('Error fetching shipping rates:', error);
|
||||
setError('Failed to retrieve shipping options. Please try again.');
|
||||
|
|
@ -257,6 +272,7 @@ const CheckoutPage = () => {
|
|||
}
|
||||
};
|
||||
|
||||
|
||||
// Redirect to Stripe checkout when the URL is available
|
||||
useEffect(() => {
|
||||
if (checkoutUrl) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue