Overview
The Mis Compras platform implements shopping cart functionality primarily on the client-side using browser storage (localStorage or sessionStorage). The cart data is then submitted to the checkout endpoints when the user completes their purchase.
Unlike users, products, and orders, the cart does not have dedicated server-side CRUD endpoints. Cart management is handled client-side, and the cart contents are sent to the server only during checkout.
Cart Data Structure
The cart should be maintained client-side with the following structure:
{
"items": [
{
"id": 1,
"nombre": "iPhone 15 Pro",
"precio": 999.99,
"cantidad": 2,
"subtotal": 1999.98,
"imagen": "1704123456_abc123def456.jpg"
},
{
"id": 3,
"nombre": "AirPods Pro",
"precio": 249.99,
"cantidad": 1,
"subtotal": 249.99,
"imagen": "1704123789_airpods.jpg"
}
],
"total": 2249.97,
"item_count": 3
}
Cart Item Properties
Product ID from the products table
Product name for display purposes
Unit price of the product
Quantity of this product in the cart
Calculated as precio × cantidad
Product image filename for display
Client-Side Cart Operations
Add to Cart
function addToCart(product) {
let cart = JSON.parse(localStorage.getItem('cart')) || { items: [], total: 0 };
// Check if product already exists in cart
const existingItem = cart.items.find(item => item.id === product.id);
if (existingItem) {
// Increment quantity
existingItem.cantidad++;
existingItem.subtotal = existingItem.precio * existingItem.cantidad;
} else {
// Add new item
cart.items.push({
id: product.id_producto,
nombre: product.nombre,
precio: product.precio,
cantidad: 1,
subtotal: product.precio,
imagen: product.imagen
});
}
// Recalculate total
cart.total = cart.items.reduce((sum, item) => sum + item.subtotal, 0);
cart.item_count = cart.items.reduce((sum, item) => sum + item.cantidad, 0);
localStorage.setItem('cart', JSON.stringify(cart));
return cart;
}
Update Quantity
function updateCartQuantity(productId, newQuantity) {
let cart = JSON.parse(localStorage.getItem('cart'));
const item = cart.items.find(item => item.id === productId);
if (item) {
if (newQuantity <= 0) {
// Remove item if quantity is 0 or negative
cart.items = cart.items.filter(item => item.id !== productId);
} else {
item.cantidad = newQuantity;
item.subtotal = item.precio * item.cantidad;
}
// Recalculate total
cart.total = cart.items.reduce((sum, item) => sum + item.subtotal, 0);
cart.item_count = cart.items.reduce((sum, item) => sum + item.cantidad, 0);
localStorage.setItem('cart', JSON.stringify(cart));
}
return cart;
}
Remove from Cart
function removeFromCart(productId) {
let cart = JSON.parse(localStorage.getItem('cart'));
cart.items = cart.items.filter(item => item.id !== productId);
// Recalculate total
cart.total = cart.items.reduce((sum, item) => sum + item.subtotal, 0);
cart.item_count = cart.items.reduce((sum, item) => sum + item.cantidad, 0);
localStorage.setItem('cart', JSON.stringify(cart));
return cart;
}
Clear Cart
function clearCart() {
localStorage.removeItem('cart');
return { items: [], total: 0, item_count: 0 };
}
Get Cart
function getCart() {
return JSON.parse(localStorage.getItem('cart')) || { items: [], total: 0, item_count: 0 };
}
Checkout Integration
When the user is ready to complete their purchase, send the cart data to one of the checkout endpoints:
Using Node.js Endpoint
Checkout with Node.js API
async function checkout(userId, shippingAddress) {
const cart = getCart();
if (cart.items.length === 0) {
alert('Your cart is empty!');
return;
}
const checkoutData = {
usuario_id: userId,
items: cart.items,
total: cart.total,
direccion: shippingAddress
};
try {
const response = await fetch('/api/pedidos/checkout', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(checkoutData)
});
const result = await response.json();
if (result.success) {
// Clear cart after successful checkout
clearCart();
window.location.href = '/order-confirmation?id=' + result.pedido_id;
} else {
alert('Checkout failed: ' + result.message);
}
} catch (error) {
console.error('Checkout error:', error);
alert('An error occurred during checkout');
}
}
Using PHP Endpoint
async function checkoutPHP(userId) {
const cart = getCart();
if (cart.items.length === 0) {
alert('Your cart is empty!');
return;
}
const checkoutData = {
usuario_id: userId,
items: cart.items,
total: cart.total
};
try {
const response = await fetch('/php/checkout.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(checkoutData)
});
const result = await response.json();
if (result.success) {
clearCart();
window.location.href = result.redirect || 'gracias.html';
} else {
alert('Checkout failed: ' + result.message);
}
} catch (error) {
console.error('Checkout error:', error);
alert('An error occurred during checkout');
}
}
Stock Validation
Before submitting to checkout, it’s recommended to validate product availability:
Validate Stock Before Checkout
async function validateCartStock() {
const cart = getCart();
const validationErrors = [];
for (const item of cart.items) {
try {
const response = await fetch(`/php/obtener_producto.php?id=${item.id}`);
const result = await response.json();
if (!result.exito) {
validationErrors.push(`Product "${item.nombre}" is no longer available`);
continue;
}
const product = result.producto;
// Check if stock is sufficient
if (product.stock < item.cantidad) {
validationErrors.push(
`Only ${product.stock} units of "${item.nombre}" available (you have ${item.cantidad} in cart)`
);
}
// Check if price has changed
if (Math.abs(product.precio - item.precio) > 0.01) {
validationErrors.push(
`Price of "${item.nombre}" has changed from $${item.precio} to $${product.precio}`
);
}
} catch (error) {
validationErrors.push(`Error validating "${item.nombre}"`);
}
}
return validationErrors;
}
// Use before checkout
async function safeCheckout(userId, shippingAddress) {
const errors = await validateCartStock();
if (errors.length > 0) {
alert('Cart validation failed:\n' + errors.join('\n'));
return;
}
await checkout(userId, shippingAddress);
}
Always validate stock availability before checkout. The checkout endpoint will reduce stock levels, which could lead to negative stock if not validated beforehand.
Cart Display Example
function renderCart() {
const cart = getCart();
const cartContainer = document.getElementById('cart-items');
const totalElement = document.getElementById('cart-total');
if (cart.items.length === 0) {
cartContainer.innerHTML = '<p>Your cart is empty</p>';
totalElement.textContent = '$0.00';
return;
}
cartContainer.innerHTML = cart.items.map(item => `
<div class="cart-item">
<img src="/imagenes/${item.imagen}" alt="${item.nombre}" width="80">
<div class="item-details">
<h3>${item.nombre}</h3>
<p>Price: $${item.precio.toFixed(2)}</p>
<div class="quantity-controls">
<button onclick="updateCartQuantity(${item.id}, ${item.cantidad - 1})">-</button>
<span>${item.cantidad}</span>
<button onclick="updateCartQuantity(${item.id}, ${item.cantidad + 1})">+</button>
</div>
<p>Subtotal: $${item.subtotal.toFixed(2)}</p>
<button onclick="removeFromCart(${item.id})">Remove</button>
</div>
</div>
`).join('');
totalElement.textContent = `$${cart.total.toFixed(2)}`;
}
// Update display when cart changes
window.addEventListener('storage', renderCart);
Best Practices
Cart Persistence: Use localStorage for persistent carts that survive browser sessions, or sessionStorage for carts that clear when the browser closes.
Price Integrity: Always fetch current prices from the server during checkout to prevent price manipulation.
Stock Validation: Validate stock availability before allowing checkout to prevent overselling.
Security: Never trust cart totals calculated client-side. The server should recalculate totals based on current prices during checkout.
User Experience: Update cart counts in the UI navbar/header when cart contents change using event listeners or custom events.