shippinh rates

This commit is contained in:
2ManyProjects 2025-04-28 00:00:17 -05:00
parent ccf11a4b64
commit 57c5b6f864
6 changed files with 289 additions and 99 deletions

View file

@ -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

View file

@ -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',

View file

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

View file

@ -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

View file

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

View file

@ -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