diff --git a/backend/src/routes/cart.js b/backend/src/routes/cart.js index 977b483..b00818b 100644 --- a/backend/src/routes/cart.js +++ b/backend/src/routes/cart.js @@ -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 diff --git a/backend/src/routes/stripePayment.js b/backend/src/routes/stripePayment.js index 7ccc715..e352c5f 100644 --- a/backend/src/routes/stripePayment.js +++ b/backend/src/routes/stripePayment.js @@ -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({ @@ -122,7 +122,13 @@ module.exports = (pool, query, authMiddleware) => { user_id: userId, 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', diff --git a/backend/src/services/shippingService.js b/backend/src/services/shippingService.js index cd89ab9..5bafc08 100644 --- a/backend/src/services/shippingService.js +++ b/backend/src/services/shippingService.js @@ -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} 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; }, diff --git a/fileStructure.txt b/fileStructure.txt index 4c3de10..3a6458b 100644 --- a/fileStructure.txt +++ b/fileStructure.txt @@ -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 diff --git a/frontend/src/context/StripeContext.jsx b/frontend/src/context/StripeContext.jsx index 5f3f5a9..0ecbd7a 100644 --- a/frontend/src/context/StripeContext.jsx +++ b/frontend/src/context/StripeContext.jsx @@ -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; diff --git a/frontend/src/pages/CheckoutPage.jsx b/frontend/src/pages/CheckoutPage.jsx index 277264a..37503fa 100644 --- a/frontend/src/pages/CheckoutPage.jsx +++ b/frontend/src/pages/CheckoutPage.jsx @@ -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