cart quantity protections
This commit is contained in:
parent
57ce946666
commit
f6554d2ad0
3 changed files with 170 additions and 77 deletions
|
|
@ -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]
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
`;
|
||||
// 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 result = await qry(query, [id]);
|
||||
const placeholders = productIds.map((_, index) => `$${index + 1}`).join(',');
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({
|
||||
error: true,
|
||||
message: 'Product not found'
|
||||
});
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue