cart quantity protections

This commit is contained in:
2ManyProjects 2025-04-25 16:58:05 -05:00
parent 57ce946666
commit f6554d2ad0
3 changed files with 170 additions and 77 deletions

View file

@ -98,9 +98,9 @@ module.exports = (pool, query, authMiddleware) => {
message: 'You can only modify your own cart'
});
}
// Check if product exists
// Check if product exists and verify stock
const productResult = await query(
'SELECT * FROM products WHERE id = $1',
'SELECT id, name, stock_quantity FROM products WHERE id = $1',
[productId]
);
@ -111,6 +111,8 @@ module.exports = (pool, query, authMiddleware) => {
});
}
const product = productResult.rows[0];
// Get or create cart
let cartResult = await query(
'SELECT * FROM carts WHERE user_id = $1',
@ -132,10 +134,34 @@ module.exports = (pool, query, authMiddleware) => {
[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
const newQuantity = existingItemResult.rows[0].quantity + quantity;
await query(
'UPDATE cart_items SET quantity = $1 WHERE id = $2',
[newQuantity, existingItemResult.rows[0].id]
@ -151,25 +177,25 @@ module.exports = (pool, query, authMiddleware) => {
// 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.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`,
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]
);
@ -214,6 +240,7 @@ module.exports = (pool, query, authMiddleware) => {
message: 'You can only modify your own cart'
});
}
// Get cart
const cartResult = await query(
'SELECT * FROM carts WHERE user_id = $1',
@ -236,7 +263,31 @@ module.exports = (pool, query, authMiddleware) => {
[cartId, productId]
);
} else {
// Update quantity
// 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]
@ -246,25 +297,25 @@ module.exports = (pool, query, authMiddleware) => {
// 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.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`,
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]
);

View file

@ -80,41 +80,84 @@ module.exports = (pool, qry) => {
}
});
// Get single product by ID
// Get single product by ID or IDS if multiple are passed in as a comma seperated string
router.get('/:id', async (req, res, next) => {
try {
const { id } = req.params;
const query = `
SELECT p.*, pc.name as category_name,
ARRAY_AGG(DISTINCT t.name) FILTER (WHERE t.name IS NOT NULL) AS tags,
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
) FILTER (WHERE pi.id IS NOT NULL) AS images
FROM products p
JOIN product_categories pc ON p.category_id = pc.id
LEFT JOIN product_tags pt ON p.id = pt.product_id
LEFT JOIN tags t ON pt.tag_id = t.id
LEFT JOIN product_images pi ON p.id = pi.product_id
WHERE p.id = $1
GROUP BY p.id, pc.name
`;
const result = await qry(query, [id]);
if (result.rows.length === 0) {
return res.status(404).json({
error: true,
message: 'Product not found'
});
// Check if comma is present in the ID parameter
if (id.includes(',')) {
// Handle multiple product IDs
const productIds = id.split(',').map(item => item.trim());
const placeholders = productIds.map((_, index) => `$${index + 1}`).join(',');
const query = `
SELECT p.*, pc.name as category_name,
ARRAY_AGG(DISTINCT t.name) FILTER (WHERE t.name IS NOT NULL) AS tags,
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
) FILTER (WHERE pi.id IS NOT NULL) AS images
FROM products p
JOIN product_categories pc ON p.category_id = pc.id
LEFT JOIN product_tags pt ON p.id = pt.product_id
LEFT JOIN tags t ON pt.tag_id = t.id
LEFT JOIN product_images pi ON p.id = pi.product_id
WHERE p.id IN (${placeholders})
GROUP BY p.id, pc.name
`;
const result = await qry(query, productIds);
if (result.rows.length === 0) {
return res.status(404).json({
error: true,
message: 'No products found'
});
}
// Return array of products
res.json(result.rows);
} else {
// Handle single product ID (original code)
const query = `
SELECT p.*, pc.name as category_name,
ARRAY_AGG(DISTINCT t.name) FILTER (WHERE t.name IS NOT NULL) AS tags,
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
) FILTER (WHERE pi.id IS NOT NULL) AS images
FROM products p
JOIN product_categories pc ON p.category_id = pc.id
LEFT JOIN product_tags pt ON p.id = pt.product_id
LEFT JOIN tags t ON pt.tag_id = t.id
LEFT JOIN product_images pi ON p.id = pi.product_id
WHERE p.id = $1
GROUP BY p.id, pc.name
`;
const result = await qry(query, [id]);
if (result.rows.length === 0) {
return res.status(404).json({
error: true,
message: 'Product not found'
});
}
// Return single product
res.json([result.rows[0]]);
}
res.json(result.rows[0]);
} catch (error) {
next(error);
}

View file

@ -22,7 +22,7 @@ import ShoppingCartIcon from '@mui/icons-material/ShoppingCart';
import NavigateNextIcon from '@mui/icons-material/NavigateNext';
import { Link as RouterLink, useNavigate } from 'react-router-dom';
import { useAuth } from '@hooks/reduxHooks';
import { useGetCart, useUpdateCartItem, useClearCart } from '@hooks/apiHooks';
import { useGetCart, useUpdateCartItem, useClearCart, useProduct } from '@hooks/apiHooks';
import imageUtils from '@utils/imageUtils';
const CartPage = () => {
@ -31,7 +31,7 @@ const CartPage = () => {
// Get cart data
const { data: cart, isLoading, error } = useGetCart(user);
const { data: products } = useProduct(cart?.items.map(item => item.product_id).join(","));
// Cart mutations
const updateCartItem = useUpdateCartItem();
const clearCart = useClearCart();
@ -218,11 +218,10 @@ const CartPage = () => {
size="small"
sx={{ width: 40, mx: 1 }}
/>
<IconButton
size="small"
onClick={() => handleUpdateQuantity(item.product_id, item.quantity + 1)}
disabled={updateCartItem.isLoading}
disabled={(products?.length > 0 && item.quantity >= products.find(prod => item.product_id === prod.id)?.stock_quantity)|| updateCartItem.isLoading}
>
<AddIcon fontSize="small" />
</IconButton>