E-Commerce-Module/backend/src/routes/cart.js

1238 lines
No EOL
40 KiB
JavaScript

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