diff --git a/backend/src/routes/cart.js b/backend/src/routes/cart.js index a561c76..0c64378 100644 --- a/backend/src/routes/cart.js +++ b/backend/src/routes/cart.js @@ -922,7 +922,195 @@ module.exports = (pool, query, authMiddleware) => { } }); - // Complete checkout after successful payment + router.post('/checkout', async (req, res, next) => { + try { + const { userId, shippingAddress, shippingMethod } = req.body; + + if (req.user.id !== userId) { + return res.status(403).json({ + error: true, + message: 'You can only checkout your own cart' + }); + } + + // Get cart + const cartResult = await query( + 'SELECT * FROM carts WHERE user_id = $1', + [userId] + ); + + if (cartResult.rows.length === 0) { + return res.status(404).json({ + error: true, + message: 'Cart not found' + }); + } + + const cartId = cartResult.rows[0].id; + + // Get cart items + const cartItemsResult = await query( + `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, + 'isPrimary', pi.is_primary + ) + FROM product_images pi + WHERE pi.product_id = p.id AND pi.is_primary = true + LIMIT 1 + ) AS primary_image + FROM cart_items ci + JOIN products p ON ci.product_id = p.id + WHERE ci.cart_id = $1`, + [cartId] + ); + + if (cartItemsResult.rows.length === 0) { + return res.status(400).json({ + error: true, + message: 'Cart is empty' + }); + } + + // Calculate subtotal + const subtotal = cartItemsResult.rows.reduce((sum, item) => { + return sum + (parseFloat(item.price) * item.quantity); + }, 0); + + // 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; + + // 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 { + // 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] + ); + + 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; + } + + 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 purchasing shipment:', error); + // Continue with the rate provided + } + } + } else { + // Default to flat rate if no method selected + const shippingRates = shippingService.getFlatRateShipping(subtotal); + shippingCost = shippingRates[0].rate; + } + } + + // Calculate total with shipping + const total = subtotal + shippingCost; + + // Begin transaction + const client = await pool.connect(); + + try { + await client.query('BEGIN'); + + // Create order + const orderId = uuidv4(); + await client.query( + 'INSERT INTO orders (id, user_id, status, total_amount, shipping_address, payment_completed, shipping_cost) VALUES ($1, $2, $3, $4, $5, $6, $7)', + [orderId, userId, 'pending', total, shippingAddress, false, shippingCost] + ); + + // Create order items + for (const item of cartItemsResult.rows) { + await client.query( + 'INSERT INTO order_items (id, order_id, product_id, quantity, price_at_purchase) VALUES ($1, $2, $3, $4, $5)', + [uuidv4(), orderId, item.product_id, item.quantity, item.price] + ); + } + + // If we have shipping details, save them with the order + if (shippingMethod && shippingMethod.id) { + const shippingInfo = shipmentData || { + method_id: shippingMethod.id, + carrier: shippingMethod.carrier, + service: shippingMethod.service, + rate: shippingMethod.rate, + estimated_days: shippingMethod.delivery_days, + tracking_code: null + }; + + await client.query( + 'UPDATE orders SET shipping_info = $1 WHERE id = $2', + [JSON.stringify(shippingInfo), orderId] + ); + } + + // 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 + res.status(201).json({ + success: true, + message: 'Order created successfully, ready for payment', + orderId, + cartItems: cartItemsResult.rows, + subtotal, + shippingCost, + total, + shipmentData + }); + + // Note: We don't clear the cart here now - we'll do that after successful payment + } catch (error) { + await client.query('ROLLBACK'); + throw error; + } finally { + client.release(); + } + } catch (error) { + next(error); + } + }); + + // Complete checkout after successful payment router.post('/complete-checkout', async (req, res, next) => { try { const { userId, orderId, sessionId } = req.body; @@ -1094,145 +1282,5 @@ module.exports = (pool, query, authMiddleware) => { } }); - // Complete checkout after successful payment - router.post('/complete-checkout', async (req, res, next) => { - try { - const { userId, orderId, sessionId } = req.body; - console.log("Complete Checkout ", `${userId} ${orderId} ${sessionId}`) - - if (req.user.id !== userId) { - return res.status(403).json({ - error: true, - message: 'You can only complete your own checkout' - }); - } - - // Verify the order exists and belongs to the user - const orderResult = await query( - 'SELECT * FROM orders WHERE id = $1 AND user_id = $2', - [orderId, userId] - ); - - if (orderResult.rows.length === 0) { - return res.status(404).json({ - error: true, - message: 'Order not found' - }); - } - - // Begin transaction - const client = await pool.connect(); - - try { - await client.query('BEGIN'); - - // Update order status and payment info - await client.query( - 'UPDATE orders SET status = $1, payment_completed = true, payment_id = $2 WHERE id = $3', - ['processing', sessionId, orderId] - ); - - // Get cart - const cartResult = await client.query( - 'SELECT * FROM carts WHERE user_id = $1', - [userId] - ); - - if (cartResult.rows.length > 0) { - const cartId = cartResult.rows[0].id; - - // Get cart items to update product stock - const cartItemsResult = await client.query( - 'SELECT * FROM cart_items WHERE cart_id = $1', - [cartId] - ); - - // Update product stock - for (const item of cartItemsResult.rows) { - await client.query( - 'UPDATE products SET stock_quantity = stock_quantity - $1 WHERE id = $2', - [item.quantity, item.product_id] - ); - - // Process stock notifications after updating stock - try { - const productWithNotification = await client.query( - `SELECT id, name, stock_quantity, stock_notification - FROM products - WHERE id = $1`, - [item.product_id] - ); - - if (productWithNotification.rows.length > 0) { - const product = productWithNotification.rows[0]; - let stockNotification; - - // Handle different ways the JSON could be stored - if (product.stock_notification) { - try { - // If it's a string, parse it - // why are we doing this its been an obj for months now - if (typeof product.stock_notification === 'string') { - stockNotification = JSON.parse(product.stock_notification); - } else { - // Otherwise use as is - stockNotification = product.stock_notification; - } - - console.log("Stock notification for product:", product.id, stockNotification); - - // Check if notification is enabled and stock is below threshold - if (stockNotification && - stockNotification.enabled === true && - stockNotification.email && - stockNotification.threshold && - product.stock_quantity <= parseInt(stockNotification.threshold)) { - - // Log the notification with the order ID - await client.query( - `INSERT INTO notification_logs - (order_id, notification_type, sent_at, status) - VALUES ($1, $2, NOW(), $3)`, - [orderId, 'low_stock_alert', 'pending'] - ); - - console.log(`Low stock notification queued for product ${product.id} - ${product.name}`); - } - } catch (parseError) { - console.error("Error parsing stock notification JSON:", parseError); - } - } - } - } catch (notificationError) { - console.error("Error processing stock notification:", notificationError); - // Continue with checkout even if notification processing fails - } - } - - // Clear cart - await client.query( - 'DELETE FROM cart_items WHERE cart_id = $1', - [cartId] - ); - } - - await client.query('COMMIT'); - - res.status(200).json({ - success: true, - message: 'Order completed successfully', - orderId - }); - } catch (error) { - await client.query('ROLLBACK'); - throw error; - } finally { - client.release(); - } - } catch (error) { - next(error); - } - }); - return router; }; \ No newline at end of file