shippinh rates
This commit is contained in:
parent
ccf11a4b64
commit
57c5b6f864
6 changed files with 289 additions and 99 deletions
|
|
@ -560,7 +560,7 @@ module.exports = (pool, query, authMiddleware) => {
|
|||
|
||||
// Get cart items
|
||||
const cartItemsResult = await query(
|
||||
`SELECT ci.*, p.price, p.name, p.description, p.weight_grams,
|
||||
`SELECT ci.*, p.price, p.name, p.description, p.weight_grams, p.length_cm, p.width_cm, p.height_cm,
|
||||
(
|
||||
SELECT json_build_object(
|
||||
'path', pi.image_path,
|
||||
|
|
@ -588,15 +588,55 @@ module.exports = (pool, query, authMiddleware) => {
|
|||
return sum + (parseFloat(item.price) * item.quantity);
|
||||
}, 0);
|
||||
|
||||
// Determine shipping cost
|
||||
// Determine shipping cost and create shipment if needed
|
||||
let shippingCost = 0;
|
||||
let shipmentData = null;
|
||||
|
||||
if (config.shipping.enabled) {
|
||||
// If a specific shipping method was selected
|
||||
if (shippingMethod && shippingMethod.id) {
|
||||
shippingCost = parseFloat(shippingMethod.rate) || 0;
|
||||
// Parse shipping address to object if it's a string
|
||||
const parsedAddress = typeof shippingAddress === 'string'
|
||||
? shippingService.parseAddressString(shippingAddress)
|
||||
: shippingAddress;
|
||||
|
||||
// Calculate total weight
|
||||
const totalWeight = shippingService.calculateTotalWeight(cartItemsResult.rows);
|
||||
|
||||
// Create an actual shipment on EasyPost if a real rate was selected (not flat/free)
|
||||
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
|
||||
);
|
||||
|
||||
// Use the actual rate from the created shipment
|
||||
shippingCost = shipmentData.selected_rate.rate;
|
||||
|
||||
console.log('Shipment created successfully:', shipmentData.shipment_id);
|
||||
} catch (error) {
|
||||
console.error('Error creating shipment:', error);
|
||||
// Fallback to the rate provided
|
||||
shippingCost = parseFloat(shippingMethod.rate) || 0;
|
||||
}
|
||||
} else {
|
||||
// Use the rate provided for flat rate or free shipping
|
||||
shippingCost = parseFloat(shippingMethod.rate) || 0;
|
||||
}
|
||||
} else {
|
||||
// Default to flat rate
|
||||
// Default to flat rate if no method selected
|
||||
const shippingRates = await shippingService.getFlatRateShipping(subtotal);
|
||||
shippingCost = shippingRates[0].rate;
|
||||
}
|
||||
|
|
@ -628,12 +668,13 @@ module.exports = (pool, query, authMiddleware) => {
|
|||
|
||||
// If a shipping method was selected, save it with the order
|
||||
if (shippingMethod && shippingMethod.id) {
|
||||
const shippingInfo = {
|
||||
const shippingInfo = shipmentData || {
|
||||
method_id: shippingMethod.id,
|
||||
carrier: shippingMethod.carrier,
|
||||
service: shippingMethod.service,
|
||||
rate: shippingMethod.rate,
|
||||
estimated_days: shippingMethod.delivery_days
|
||||
estimated_days: shippingMethod.delivery_days,
|
||||
tracking_code: null
|
||||
};
|
||||
|
||||
await client.query(
|
||||
|
|
@ -652,7 +693,8 @@ module.exports = (pool, query, authMiddleware) => {
|
|||
cartItems: cartItemsResult.rows,
|
||||
subtotal,
|
||||
shippingCost,
|
||||
total
|
||||
total,
|
||||
shipmentData
|
||||
});
|
||||
|
||||
// Note: We don't clear the cart here now - we'll do that after successful payment
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ module.exports = (pool, query, authMiddleware) => {
|
|||
// Create checkout session
|
||||
router.post('/create-checkout-session', async (req, res, next) => {
|
||||
try {
|
||||
const { cartItems, shippingAddress, userId, orderId } = req.body;
|
||||
const { cartItems, shippingAddress, userId, orderId, shippingDetails } = req.body;
|
||||
|
||||
if (!cartItems || cartItems.length === 0) {
|
||||
return res.status(400).json({
|
||||
|
|
@ -123,6 +123,12 @@ module.exports = (pool, query, authMiddleware) => {
|
|||
shipping_address: JSON.stringify(shippingAddress),
|
||||
};
|
||||
|
||||
// Add tracking information to metadata if available
|
||||
if (shippingDetails && shippingDetails.tracking_code) {
|
||||
metadata.tracking_code = shippingDetails.tracking_code;
|
||||
metadata.shipping_carrier = shippingDetails.shipping_method || 'Standard Shipping';
|
||||
}
|
||||
|
||||
// Create Stripe checkout session
|
||||
const session = await stripeClient.checkout.sessions.create({
|
||||
payment_method_types: ['card'],
|
||||
|
|
@ -131,22 +137,21 @@ module.exports = (pool, query, authMiddleware) => {
|
|||
success_url: `${config.site.protocol}://${config.site.domain}/checkout/success?session_id={CHECKOUT_SESSION_ID}`,
|
||||
cancel_url: `${config.site.protocol}://${config.site.domain}/checkout/cancel`,
|
||||
metadata: metadata,
|
||||
shipping_address_collection: {
|
||||
allowed_countries: ['US', 'CA'],
|
||||
},
|
||||
shipping_options: [
|
||||
{
|
||||
shipping_rate_data: {
|
||||
type: 'fixed_amount',
|
||||
fixed_amount: {
|
||||
amount: 0,
|
||||
amount: shippingDetails && shippingDetails.shipping_cost ?
|
||||
Math.round(parseFloat(shippingDetails.shipping_cost) * 100) : 0,
|
||||
currency: 'usd',
|
||||
},
|
||||
display_name: 'Free shipping',
|
||||
display_name: shippingDetails && shippingDetails.shipping_method ?
|
||||
shippingDetails.shipping_method : 'Standard Shipping',
|
||||
delivery_estimate: {
|
||||
minimum: {
|
||||
unit: 'business_day',
|
||||
value: 5,
|
||||
value: 2,
|
||||
},
|
||||
maximum: {
|
||||
unit: 'business_day',
|
||||
|
|
|
|||
|
|
@ -64,6 +64,134 @@ const shippingService = {
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
|
@ -142,19 +270,19 @@ const shippingService = {
|
|||
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,4 +1,42 @@
|
|||
Rocks/
|
||||
ROCKS/
|
||||
├── backend/
|
||||
│ ├── src/
|
||||
│ │ ├── services/
|
||||
│ │ │ ├── shippingService.js
|
||||
│ │ ├── routes/
|
||||
│ │ │ ├── shipping.js
|
||||
│ │ │ ├── settingsAdmin.js
|
||||
│ │ │ ├── cart.js
|
||||
│ │ │ ├── userOrders.js
|
||||
│ │ │ ├── orderAdmin.js
|
||||
│ │ │ ├── stripePayment.js
|
||||
│ │ │ ├── auth.js
|
||||
│ │ │ ├── userAdmin.js
|
||||
│ │ │ ├── products.js
|
||||
│ │ │ ├── categoryAdmin.js
|
||||
│ │ │ ├── productAdminImages.js
|
||||
│ │ │ ├── images.js
|
||||
│ │ │ └── productAdmin.js
|
||||
│ │ ├── models/
|
||||
│ │ │ └── SystemSettings.js
|
||||
│ │ ├── middleware/
|
||||
│ │ │ ├── upload.js
|
||||
│ │ │ ├── auth.js
|
||||
│ │ │ └── adminAuth.js
|
||||
│ │ └── db/
|
||||
│ │ └── index.js
|
||||
│ ├── index.js
|
||||
│ ├── config.js
|
||||
│ ├── node_modules/
|
||||
│ ├── public/
|
||||
│ │ └── uploads/
|
||||
│ │ └── products/
|
||||
│ ├── package.json
|
||||
│ ├── package-lock.json
|
||||
│ ├── .env
|
||||
│ ├── Dockerfile
|
||||
│ ├── README.md
|
||||
│ └── .gitignore
|
||||
├── frontend/
|
||||
│ ├── node_modules/
|
||||
│ ├── src/
|
||||
|
|
@ -11,8 +49,8 @@ Rocks/
|
|||
│ │ │ │ ├── DashboardPage.jsx
|
||||
│ │ │ │ ├── CategoriesPage.jsx
|
||||
│ │ │ │ └── ProductsPage.jsx
|
||||
│ │ │ ├── PaymentSuccessPage.jsx
|
||||
│ │ │ ├── CheckoutPage.jsx
|
||||
│ │ │ ├── PaymentSuccessPage.jsx
|
||||
│ │ │ ├── UserOrdersPage.jsx
|
||||
│ │ │ ├── PaymentCancelPage.jsx
|
||||
│ │ │ ├── ProductDetailPage.jsx
|
||||
|
|
@ -23,6 +61,15 @@ Rocks/
|
|||
│ │ │ ├── RegisterPage.jsx
|
||||
│ │ │ ├── NotFoundPage.jsx
|
||||
│ │ │ └── LoginPage.jsx
|
||||
│ │ ├── services/
|
||||
│ │ │ ├── adminService.js
|
||||
│ │ │ ├── authService.js
|
||||
│ │ │ ├── settingsAdminService.js
|
||||
│ │ │ ├── cartService.js
|
||||
│ │ │ ├── categoryAdminService.js
|
||||
│ │ │ ├── imageService.js
|
||||
│ │ │ ├── productService.js
|
||||
│ │ │ └── api.js
|
||||
│ │ ├── components/
|
||||
│ │ │ ├── OrderStatusDialog.jsx
|
||||
│ │ │ ├── StripePaymentForm.jsx
|
||||
|
|
@ -40,15 +87,6 @@ Rocks/
|
|||
│ │ │ ├── reduxHooks.js
|
||||
│ │ │ ├── settingsAdminHooks.js
|
||||
│ │ │ └── categoryAdminHooks.js
|
||||
│ │ ├── services/
|
||||
│ │ │ ├── adminService.js
|
||||
│ │ │ ├── authService.js
|
||||
│ │ │ ├── settingsAdminService.js
|
||||
│ │ │ ├── cartService.js
|
||||
│ │ │ ├── categoryAdminService.js
|
||||
│ │ │ ├── imageService.js
|
||||
│ │ │ ├── productService.js
|
||||
│ │ │ └── api.js
|
||||
│ │ ├── utils/
|
||||
│ │ │ └── imageUtils.js
|
||||
│ │ ├── layouts/
|
||||
|
|
@ -71,65 +109,31 @@ Rocks/
|
|||
│ │ ├── App.jsx
|
||||
│ │ ├── config.js
|
||||
│ │ └── main.jsx
|
||||
│ └── public/
|
||||
│ ├── favicon.svg
|
||||
│ ├── package-lock.json
|
||||
│ ├── package.json
|
||||
│ ├── vite.config.js
|
||||
│ ├── Dockerfile
|
||||
│ ├── nginx.conf
|
||||
│ ├── index.html
|
||||
│ ├── README.md
|
||||
│ ├── .env
|
||||
│ └── setup-frontend.sh
|
||||
├── backend/
|
||||
│ ├── src/
|
||||
│ │ ├── routes/
|
||||
│ │ │ ├── userOrders.js
|
||||
│ │ │ ├── orderAdmin.js
|
||||
│ │ │ ├── stripePayment.js
|
||||
│ │ │ ├── cart.js
|
||||
│ │ │ ├── auth.js
|
||||
│ │ │ ├── userAdmin.js
|
||||
│ │ │ ├── settingsAdmin.js
|
||||
│ │ │ ├── products.js
|
||||
│ │ │ ├── categoryAdmin.js
|
||||
│ │ │ ├── productAdminImages.js
|
||||
│ │ │ ├── images.js
|
||||
│ │ │ └── productAdmin.js
|
||||
│ │ ├── models/
|
||||
│ │ │ └── SystemSettings.js
|
||||
│ │ ├── middleware/
|
||||
│ │ │ ├── upload.js
|
||||
│ │ │ ├── auth.js
|
||||
│ │ │ └── adminAuth.js
|
||||
│ │ ├── db/
|
||||
│ │ │ └── index.js
|
||||
│ │ ├── index.js
|
||||
│ │ └── config.js
|
||||
│ ├── public/
|
||||
│ │ └── uploads/
|
||||
│ │ └── products/
|
||||
│ ├── node_modules/
|
||||
│ ├── .env
|
||||
│ │ ├── favicon.svg
|
||||
│ │ └── index.html
|
||||
│ ├── package-lock.json
|
||||
│ ├── package.json
|
||||
│ ├── vite.config.js
|
||||
│ ├── Dockerfile
|
||||
│ ├── nginx.conf
|
||||
│ ├── README.md
|
||||
│ └── .gitignore
|
||||
│ ├── .env
|
||||
│ └── setup-frontend.sh
|
||||
├── db/
|
||||
│ ├── init/
|
||||
│ │ ├── 01-schema.sql
|
||||
│ │ ├── 02-seed.sql
|
||||
│ │ ├── 03-api-key.sql
|
||||
│ │ ├── 04-product-images.sql
|
||||
│ │ ├── 05-admin-role.sql
|
||||
│ │ ├── 06-product-categories.sql
|
||||
│ │ ├── 07-user-keys.sql
|
||||
│ │ ├── 08-create-email.sql
|
||||
│ │ ├── 09-system-settings.sql
|
||||
│ │ ├── 10-payment.sql
|
||||
│ │ └── 11-notifications.sql
|
||||
│ └── .gitignore
|
||||
│ └── init/
|
||||
│ ├── 12-shipping-orders.sql
|
||||
│ ├── 09-system-settings.sql
|
||||
│ ├── 11-notifications.sql
|
||||
│ ├── 10-payment.sql
|
||||
│ ├── 08-create-email.sql
|
||||
│ ├── 07-user-keys.sql
|
||||
│ ├── 06-product-categories.sql
|
||||
│ ├── 05-admin-role.sql
|
||||
│ ├── 02-seed.sql
|
||||
│ ├── 04-product-images.sql
|
||||
│ ├── 03-api-key.sql
|
||||
│ └── 01-schema.sql
|
||||
├── test/
|
||||
├── fileStructure.txt
|
||||
├── docker-compose.yml
|
||||
|
|
|
|||
|
|
@ -52,13 +52,14 @@ export const StripeProvider = ({ children }) => {
|
|||
};
|
||||
|
||||
// Create a checkout session
|
||||
const createCheckoutSession = async (cartItems, orderId, shippingAddress, userId) => {
|
||||
const createCheckoutSession = async (cartItems, orderId, shippingAddress, userId, shippingDetails = null) => {
|
||||
try {
|
||||
const response = await apiClient.post('/payment/create-checkout-session', {
|
||||
cartItems,
|
||||
orderId,
|
||||
shippingAddress,
|
||||
userId
|
||||
userId,
|
||||
shippingDetails
|
||||
});
|
||||
|
||||
return response.data;
|
||||
|
|
|
|||
|
|
@ -220,6 +220,19 @@ const CheckoutPage = () => {
|
|||
// Store the order ID for later use
|
||||
setOrderId(orderResponse.orderId);
|
||||
|
||||
// Use the shipping data from the response if available
|
||||
const shippingDetails = orderResponse.shipmentData ? {
|
||||
shipping_cost: orderResponse.shippingCost,
|
||||
shipping_method: `${orderResponse.shipmentData.carrier} - ${orderResponse.shipmentData.service}`,
|
||||
tracking_code: orderResponse.shipmentData.tracking_code,
|
||||
label_url: orderResponse.shipmentData.label_url
|
||||
} : {
|
||||
shipping_cost: orderResponse.shippingCost,
|
||||
shipping_method: selectedShippingMethod ?
|
||||
`${selectedShippingMethod.carrier} - ${selectedShippingMethod.service}` :
|
||||
'Standard Shipping'
|
||||
};
|
||||
|
||||
// Proceed to payment step
|
||||
setActiveStep(3);
|
||||
|
||||
|
|
@ -229,10 +242,7 @@ const CheckoutPage = () => {
|
|||
orderResponse.orderId,
|
||||
shippingAddress,
|
||||
user,
|
||||
{
|
||||
shipping_cost: orderResponse.shippingCost || shippingCost,
|
||||
shipping_method: selectedShippingMethod ? selectedShippingMethod.carrier + ' - ' + selectedShippingMethod.service : 'Standard Shipping'
|
||||
}
|
||||
shippingDetails
|
||||
);
|
||||
|
||||
// Redirect to Stripe Checkout
|
||||
|
|
|
|||
Loading…
Reference in a new issue