const express = require('express'); const { v4: uuidv4 } = require('uuid'); const router = express.Router(); const shippingService = require('../services/shippingService.js'); const config = require('../config'); 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, p.weight_grams, p.length_cm, p.width_cm, p.height_cm, ( 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, p.weight_grams, p.length_cm, p.width_cm, p.height_cm`, [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 subtotal = processedItems.reduce((sum, item) => { return sum + (parseFloat(item.price) * item.quantity); }, 0); // Initialize shipping const shipping = { rates: [] }; // Calculate basic flat rate shipping if (config.shipping.enabled) { shipping.rates = await shippingService.getFlatRateShipping(subtotal); } res.json({ id: cartId, userId, items: processedItems, itemCount: processedItems.length, subtotal, shipping, total: subtotal + (shipping.rates.length > 0 ? shipping.rates[0].rate : 0) }); } catch (error) { next(error); } }); /** * Apply coupon to cart * POST /api/cart/apply-coupon */ router.post('/apply-coupon', async (req, res, next) => { try { const { userId, code } = 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; // Check if coupon code exists and is valid const couponResult = await query(` SELECT * FROM coupons WHERE code = $1 AND is_active = true `, [code.toUpperCase()]); if (couponResult.rows.length === 0) { return res.status(404).json({ error: true, message: 'Invalid coupon code or coupon is inactive' }); } const coupon = couponResult.rows[0]; // Check if coupon is expired if (coupon.end_date && new Date(coupon.end_date) < new Date()) { return res.status(400).json({ error: true, message: 'Coupon has expired' }); } // Check if coupon has not started yet if (coupon.start_date && new Date(coupon.start_date) > new Date()) { return res.status(400).json({ error: true, message: 'Coupon is not yet active' }); } // Check redemption limit if (coupon.redemption_limit !== null && coupon.current_redemptions >= coupon.redemption_limit) { return res.status(400).json({ error: true, message: 'Coupon redemption limit has been reached' }); } // Get cart items with product details const cartItems = await query(` SELECT ci.*, p.id as product_id, p.name, p.price, p.category_id, pc.name as category_name, ( SELECT array_agg(t.id) FROM product_tags pt JOIN tags t ON pt.tag_id = t.id WHERE pt.product_id = p.id ) as tag_ids 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 `, [cartId]); if (cartItems.rows.length === 0) { return res.status(400).json({ error: true, message: 'Cart is empty' }); } // Calculate subtotal const subtotal = cartItems.rows.reduce((sum, item) => { return sum + (parseFloat(item.price) * item.quantity); }, 0); // Check minimum purchase requirement if (coupon.min_purchase_amount && subtotal < coupon.min_purchase_amount) { return res.status(400).json({ error: true, message: `Minimum purchase amount of ${coupon.min_purchase_amount.toFixed(2)} not met`, minimumAmount: coupon.min_purchase_amount }); } // Get coupon categories const couponCategories = await query(` SELECT category_id FROM coupon_categories WHERE coupon_id = $1 `, [coupon.id]); // Get coupon tags const couponTags = await query(` SELECT tag_id FROM coupon_tags WHERE coupon_id = $1 `, [coupon.id]); // Get blacklisted products const blacklistResult = await query(` SELECT product_id FROM coupon_blacklist WHERE coupon_id = $1 `, [coupon.id]); const blacklistedProductIds = blacklistResult.rows.map(row => row.product_id); // Calculate discount based on eligible products let discountableAmount = 0; const categoryIds = couponCategories.rows.map(row => row.category_id); const tagIds = couponTags.rows.map(row => row.tag_id); for (const item of cartItems.rows) { // Skip blacklisted products if (blacklistedProductIds.includes(item.product_id)) { continue; } let isEligible = false; // If no categories or tags are specified, all products are eligible if (categoryIds.length === 0 && tagIds.length === 0) { isEligible = true; } else { // Check if product belongs to eligible category if (categoryIds.includes(item.category_id)) { isEligible = true; } // Check if product has eligible tag if (!isEligible && item.tag_ids && item.tag_ids.some(tagId => tagIds.includes(tagId))) { isEligible = true; } } if (isEligible) { discountableAmount += parseFloat(item.price) * item.quantity; } } // Calculate discount amount let discountAmount = 0; if (coupon.discount_type === 'percentage') { discountAmount = discountableAmount * (coupon.discount_value / 100); } else { // fixed_amount discountAmount = Math.min(discountableAmount, coupon.discount_value); } // Apply maximum discount cap if set if (coupon.max_discount_amount && discountAmount > coupon.max_discount_amount) { discountAmount = coupon.max_discount_amount; } // Round to 2 decimal places discountAmount = Math.round(discountAmount * 100) / 100; // Update cart metadata with coupon information await query(` UPDATE carts SET metadata = jsonb_set( COALESCE(metadata, '{}'::jsonb), '{coupon}', $1::jsonb ) WHERE id = $2 `, [ JSON.stringify({ id: coupon.id, code: coupon.code, discount_type: coupon.discount_type, discount_value: coupon.discount_value, discount_amount: discountAmount }), cartId ]); // Get updated cart with all items const updatedCartItems = await query(` SELECT ci.*, p.id AS product_id, p.name, p.description, p.price, p.stock_quantity, p.category_id, pc.name AS category_name, p.weight_grams, p.length_cm, p.width_cm, p.height_cm, ( 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, p.weight_grams, p.length_cm, p.width_cm, p.height_cm `, [cartId]); // Get cart metadata with coupon const cartMetadata = await query(` SELECT metadata FROM carts WHERE id = $1 `, [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 subtotal const cartSubtotal = processedItems.reduce((sum, item) => { return sum + (parseFloat(item.price) * item.quantity); }, 0); // Calculate total with discount const total = cartSubtotal - discountAmount; // Format coupon info for response const couponInfo = cartMetadata.rows[0].metadata.coupon; res.json({ id: cartId, userId, items: processedItems, itemCount: processedItems.length, subtotal: cartSubtotal, couponDiscount: discountAmount, couponCode: couponInfo.code, couponId: couponInfo.id, total: total }); } catch (error) { next(error); } }); /** * Remove coupon from cart * POST /api/cart/remove-coupon */ router.post('/remove-coupon', async (req, res, next) => { try { const { userId } = 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; // Remove coupon from cart metadata await query(` UPDATE carts SET metadata = metadata - 'coupon' WHERE id = $1 `, [cartId]); // Get updated cart with all items const updatedCartItems = await query(` SELECT ci.*, p.id AS product_id, p.name, p.description, p.price, p.stock_quantity, p.category_id, pc.name AS category_name, p.weight_grams, p.length_cm, p.width_cm, p.height_cm, ( 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, p.weight_grams, p.length_cm, p.width_cm, p.height_cm `, [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 subtotal const subtotal = processedItems.reduce((sum, item) => { return sum + (parseFloat(item.price) * item.quantity); }, 0); res.json({ id: cartId, userId, items: processedItems, itemCount: processedItems.length, subtotal, total: subtotal, message: 'Coupon removed successfully' }); } 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, p.weight_grams, p.length_cm, p.width_cm, p.height_cm, ( 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, p.weight_grams, p.length_cm, p.width_cm, p.height_cm`, [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 subtotal const subtotal = processedItems.reduce((sum, item) => { return sum + (parseFloat(item.price) * item.quantity); }, 0); // Initialize shipping const shipping = { rates: [] }; // Calculate basic flat rate shipping if (config.shipping.enabled) { shipping.rates = await shippingService.getFlatRateShipping(subtotal); } res.json({ id: cartId, userId, items: processedItems, itemCount: processedItems.length, subtotal, shipping, total: subtotal + (shipping.rates.length > 0 ? shipping.rates[0].rate : 0) }); } 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, p.weight_grams, p.length_cm, p.width_cm, p.height_cm, ( 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, p.weight_grams, p.length_cm, p.width_cm, p.height_cm`, [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 subtotal const subtotal = processedItems.reduce((sum, item) => { return sum + (parseFloat(item.price) * item.quantity); }, 0); // Initialize shipping const shipping = { rates: [] }; // Calculate basic flat rate shipping if (config.shipping.enabled) { shipping.rates = await shippingService.getFlatRateShipping(subtotal); } res.json({ id: cartId, userId, items: processedItems, itemCount: processedItems.length, subtotal, shipping, total: subtotal + (shipping.rates.length > 0 ? shipping.rates[0].rate : 0) }); } 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, subtotal: 0, shipping: { rates: [] }, total: 0 }); } catch (error) { next(error); } }); // Get shipping rates for current cart router.post('/shipping-rates', 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 get shipping rates for your own cart' }); } // Shipping must be enabled if (!config.shipping.enabled) { return res.status(400).json({ error: true, message: 'Shipping is currently disabled' }); } // 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 with product weights const cartItemsResult = await query( `SELECT ci.quantity, p.id, p.weight_grams, p.price, p.length_cm, p.width_cm, p.height_cm 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 weight and order value const totalWeight = shippingService.calculateTotalWeight(cartItemsResult.rows); const subtotal = cartItemsResult.rows.reduce((sum, item) => { return sum + (parseFloat(item.price) * item.quantity); }, 0); // If no address provided, return only flat rate shipping if (!shippingAddress) { console.log("No Address provided - using flat rate"); const rates = shippingService.getFlatRateShipping(subtotal); return res.json({ success: true, shipment_id: null, rates }); } // Get real shipping rates with a shipment const parsedAddress = typeof shippingAddress === 'string' ? shippingService.parseAddressString(shippingAddress) : shippingAddress; const shippingResponse = await shippingService.getShippingRates( null, // Use default from config parsedAddress, { weight: totalWeight, length: Math.max(...cartItemsResult.rows.map(item => item.length_cm || 0)), width: Math.max(...cartItemsResult.rows.map(item => item.width_cm || 0)), height: Math.max(...cartItemsResult.rows.map(item => item.height_cm || 0)), order_total: subtotal } ); console.log("Shipping rates response:", JSON.stringify(shippingResponse, null, 4)); // Save the shipment ID to the session or temporary storage // This will be used when the user selects a rate if (shippingResponse.shipment_id) { // Store the shipment ID in the user's cart for later use await query( `UPDATE carts SET metadata = jsonb_set( COALESCE(metadata, '{}'::jsonb), '{temp_shipment_id}', $1::jsonb ) WHERE id = $2`, [JSON.stringify(shippingResponse.shipment_id), cartId] ); } res.json({ success: true, shipment_id: shippingResponse.shipment_id, rates: shippingResponse.rates }); } catch (error) { console.error('Error getting shipping rates:', error); next(error); } }); // 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' }); } const order = orderResult.rows[0]; // 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; // Check if cart has a coupon applied const cartMetadata = cartResult.rows[0].metadata; if (cartMetadata && cartMetadata.coupon) { const couponInfo = cartMetadata.coupon; // Increment the coupon's current_redemptions await client.query( 'UPDATE coupons SET current_redemptions = current_redemptions + 1 WHERE id = $1', [couponInfo.id] ); // Create a coupon redemption record await client.query( 'INSERT INTO coupon_redemptions (coupon_id, order_id, user_id, discount_amount) VALUES ($1, $2, $3, $4)', [couponInfo.id, orderId, userId, couponInfo.discount_amount] ); // Update the order with coupon information await client.query( 'UPDATE orders SET coupon_id = $1, discount_amount = $2 WHERE id = $3', [couponInfo.id, couponInfo.discount_amount, orderId] ); } // 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] ); // Clear cart metadata (including coupon) await client.query( 'UPDATE carts SET metadata = $1 WHERE id = $2', ['{}', 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); } }); // 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; };