const express = require('express'); const { v4: uuidv4 } = require('uuid'); const router = express.Router(); module.exports = (pool, query, authMiddleware) => { router.use(authMiddleware); // Get user's cart router.get('/:userId', async (req, res, next) => { try { const { userId } = req.params; if (req.user.id !== userId) { return res.status(403).json({ error: true, message: 'You can only access your own cart' }); } // Get cart let cartResult = await query( 'SELECT * FROM carts WHERE user_id = $1', [userId] ); // If no cart exists, create one if (cartResult.rows.length === 0) { cartResult = await query( 'INSERT INTO carts (id, user_id) VALUES ($1, $2) RETURNING *', [uuidv4(), userId] ); } const cartId = cartResult.rows[0].id; // Get cart items with product details const cartItemsResult = await query( `SELECT ci.id, ci.quantity, ci.added_at, p.id AS product_id, p.name, p.description, p.price, p.category_id, pc.name AS category_name, ( SELECT json_agg( json_build_object( 'id', pi.id, 'path', pi.image_path, 'isPrimary', pi.is_primary, 'displayOrder', pi.display_order ) ORDER BY pi.display_order ) FROM product_images pi WHERE pi.product_id = p.id ) AS images FROM cart_items ci JOIN products p ON ci.product_id = p.id JOIN product_categories pc ON p.category_id = pc.id WHERE ci.cart_id = $1 GROUP BY ci.id, ci.quantity, ci.added_at, p.id, p.name, p.description, p.price, p.category_id, pc.name`, [cartId] ); // Process images to add primary_image field const processedItems = cartItemsResult.rows.map(item => { // Add primary_image field derived from images array let primaryImage = null; if (item.images && item.images.length > 0) { primaryImage = item.images.find(img => img.isPrimary === true) || item.images[0]; } return { ...item, primary_image: primaryImage }; }); // Calculate total const total = processedItems.reduce((sum, item) => { return sum + (parseFloat(item.price) * item.quantity); }, 0); res.json({ id: cartId, userId, items: processedItems, itemCount: processedItems.length, total }); } catch (error) { next(error); } }); // Add item to cart router.post('/add', async (req, res, next) => { try { const { userId, productId, quantity = 1 } = req.body; if (req.user.id !== userId) { return res.status(403).json({ error: true, message: 'You can only modify your own cart' }); } // Check if product exists and verify stock const productResult = await query( 'SELECT id, name, stock_quantity FROM products WHERE id = $1', [productId] ); if (productResult.rows.length === 0) { return res.status(404).json({ error: true, message: 'Product not found' }); } const product = productResult.rows[0]; // Get or create cart let cartResult = await query( 'SELECT * FROM carts WHERE user_id = $1', [userId] ); if (cartResult.rows.length === 0) { cartResult = await query( 'INSERT INTO carts (id, user_id) VALUES ($1, $2) RETURNING *', [uuidv4(), userId] ); } const cartId = cartResult.rows[0].id; // Check if item already in cart const existingItemResult = await query( 'SELECT * FROM cart_items WHERE cart_id = $1 AND product_id = $2', [cartId, productId] ); let newQuantity = quantity; if (existingItemResult.rows.length > 0) { // If item already exists, calculate new total quantity newQuantity = existingItemResult.rows[0].quantity + quantity; } if (quantity < 1) { return res.status(400).json({ error: true, message: `Insufficient quantity requested. Only ${product.stock_quantity} units of "${product.name}" are available.`, availableQuantity: product.stock_quantity, currentCartQuantity: existingItemResult.rows.length > 0 ? existingItemResult.rows[0].quantity : 0 }); } // Verify the total requested quantity is available in stock if (newQuantity > product.stock_quantity) { return res.status(400).json({ error: true, message: `Insufficient stock. Only ${product.stock_quantity} units of "${product.name}" are available.`, availableQuantity: product.stock_quantity, currentCartQuantity: existingItemResult.rows.length > 0 ? existingItemResult.rows[0].quantity : 0 }); } if (existingItemResult.rows.length > 0) { // Update quantity await query( 'UPDATE cart_items SET quantity = $1 WHERE id = $2', [newQuantity, existingItemResult.rows[0].id] ); } else { // Add new item await query( 'INSERT INTO cart_items (id, cart_id, product_id, quantity) VALUES ($1, $2, $3, $4)', [uuidv4(), cartId, productId, quantity] ); } // Get updated cart const updatedCartItems = await query( `SELECT ci.id, ci.quantity, ci.added_at, p.id AS product_id, p.name, p.description, p.price, p.stock_quantity, p.category_id, pc.name AS category_name, ( SELECT json_agg( json_build_object( 'id', pi.id, 'path', pi.image_path, 'isPrimary', pi.is_primary, 'displayOrder', pi.display_order ) ORDER BY pi.display_order ) FROM product_images pi WHERE pi.product_id = p.id ) AS images FROM cart_items ci JOIN products p ON ci.product_id = p.id JOIN product_categories pc ON p.category_id = pc.id WHERE ci.cart_id = $1 GROUP BY ci.id, ci.quantity, ci.added_at, p.id, p.name, p.description, p.price, p.stock_quantity, p.category_id, pc.name`, [cartId] ); // Process images to add primary_image field const processedItems = updatedCartItems.rows.map(item => { // Add primary_image field derived from images array let primaryImage = null; if (item.images && item.images.length > 0) { primaryImage = item.images.find(img => img.isPrimary === true) || item.images[0]; } return { ...item, primary_image: primaryImage }; }); // Calculate total const total = processedItems.reduce((sum, item) => { return sum + (parseFloat(item.price) * item.quantity); }, 0); res.json({ id: cartId, userId, items: processedItems, itemCount: processedItems.length, total }); } catch (error) { next(error); } }); // Update cart item quantity router.put('/update', async (req, res, next) => { try { const { userId, productId, quantity } = req.body; if (req.user.id !== userId) { return res.status(403).json({ error: true, message: 'You can only modify 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; if (quantity <= 0) { // Remove item await query( 'DELETE FROM cart_items WHERE cart_id = $1 AND product_id = $2', [cartId, productId] ); } else { // Check product availability before updating const productResult = await query( 'SELECT id, name, stock_quantity FROM products WHERE id = $1', [productId] ); if (productResult.rows.length === 0) { return res.status(404).json({ error: true, message: 'Product not found' }); } const product = productResult.rows[0]; // Verify the requested quantity is available in stock if (quantity > product.stock_quantity) { return res.status(400).json({ error: true, message: `Insufficient stock. Only ${product.stock_quantity} units of "${product.name}" are available.`, availableQuantity: product.stock_quantity }); } // Update quantity if stock is sufficient await query( 'UPDATE cart_items SET quantity = $1 WHERE cart_id = $2 AND product_id = $3', [quantity, cartId, productId] ); } // Get updated cart const updatedCartItems = await query( `SELECT ci.id, ci.quantity, ci.added_at, p.id AS product_id, p.name, p.description, p.price, p.stock_quantity, p.category_id, pc.name AS category_name, ( SELECT json_agg( json_build_object( 'id', pi.id, 'path', pi.image_path, 'isPrimary', pi.is_primary, 'displayOrder', pi.display_order ) ORDER BY pi.display_order ) FROM product_images pi WHERE pi.product_id = p.id ) AS images FROM cart_items ci JOIN products p ON ci.product_id = p.id JOIN product_categories pc ON p.category_id = pc.id WHERE ci.cart_id = $1 GROUP BY ci.id, ci.quantity, ci.added_at, p.id, p.name, p.description, p.price, p.stock_quantity, p.category_id, pc.name`, [cartId] ); // Process images to add primary_image field const processedItems = updatedCartItems.rows.map(item => { // Add primary_image field derived from images array let primaryImage = null; if (item.images && item.images.length > 0) { primaryImage = item.images.find(img => img.isPrimary === true) || item.images[0]; } return { ...item, primary_image: primaryImage }; }); // Calculate total const total = processedItems.reduce((sum, item) => { return sum + (parseFloat(item.price) * item.quantity); }, 0); res.json({ id: cartId, userId, items: processedItems, itemCount: processedItems.length, total }); } catch (error) { next(error); } }); // Clear cart router.delete('/clear/:userId', async (req, res, next) => { try { const { userId } = req.params; if (req.user.id !== userId) { return res.status(403).json({ error: true, message: 'You can only modify 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; // Delete all items await query( 'DELETE FROM cart_items WHERE cart_id = $1', [cartId] ); res.json({ id: cartId, userId, items: [], itemCount: 0, total: 0 }); } catch (error) { next(error); } }); router.post('/checkout', async (req, res, next) => { try { const { userId, shippingAddress } = 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, ( 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 total const total = cartItemsResult.rows.reduce((sum, item) => { return sum + (parseFloat(item.price) * item.quantity); }, 0); // 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) VALUES ($1, $2, $3, $4, $5, $6)', [orderId, userId, 'pending', total, shippingAddress, false] ); // 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] ); } 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, total }); // 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); } }); // New endpoint: Complete checkout after successful payment router.post('/complete-checkout', async (req, res, next) => { try { const { userId, orderId, sessionId } = req.body; 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] ); } // 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; };