582 lines
No EOL
17 KiB
JavaScript
582 lines
No EOL
17 KiB
JavaScript
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;
|
|
}; |