Comercio Electrónico
Reglas
- Estado del carrito: store de Zustand con persistencia en
localStorage — sobrevive al refresco de página y al cierre de pestaña
- Variantes de producto: modelar como SKUs separados con un producto padre compartido — cada variante tiene su propio precio, inventario e imagen
- Checkout: Sesiones de Checkout de Stripe (del lado del servidor) — nunca enviar precios desde el cliente
- Inventario: verificar stock del lado del servidor antes de crear la sesión de checkout — protección contra condiciones de carrera con transacciones de base de datos
- Ciclo de vida del pedido:
pending → paid → shipped → delivered — actualizar mediante eventos webhook, no mediante acciones del cliente
- Listado de productos: filtrado/ordenamiento/paginación del lado del servidor — parámetros de búsqueda en la URL para vistas filtradas compartibles
- Precios: almacenar en céntimos (enteros) — mostrar con
Intl.NumberFormat y código de moneda
- Operaciones del carrito: añadir, eliminar, actualizar cantidad, vaciar — validar stock en cada acción del carrito
Patrones
import { create } from "zustand";
import { persist } from "zustand/middleware";
interface CartItem { productId: string; variantId: string; quantity: number; price: number; }
interface CartStore {
items: CartItem[];
addItem: (item: CartItem) => void;
removeItem: (variantId: string) => void;
updateQuantity: (variantId: string, quantity: number) => void;
clear: () => void;
total: () => number;
}
export const useCart = create<CartStore>()(
persist(
(set, get) => ({
items: [],
addItem: (item) => set((state) => {
const existing = state.items.find((i) => i.variantId === item.variantId);
if (existing) {
return { items: state.items.map((i) =>
i.variantId === item.variantId ? { ...i, quantity: i.quantity + item.quantity } : i
)};
}
return { items: [...state.items, item] };
}),
removeItem: (variantId) => set((s) => ({ items: s.items.filter((i) => i.variantId !== variantId) })),
updateQuantity: (variantId, quantity) => set((s) => ({
items: quantity <= 0
? s.items.filter((i) => i.variantId !== variantId)
: s.items.map((i) => (i.variantId === variantId ? { ...i, quantity } : i)),
})),
clear: () => set({ items: [] }),
total: () => get().items.reduce((sum, i) => sum + i.price * i.quantity, 0),
}),
{ name: "cart-storage" }
)
);
Evitar
- Enviar precios desde el cliente para crear el checkout — resolver siempre los precios del lado del servidor desde tu base de datos
- Almacenar el carrito solo en el estado de React — usar Zustand persistido o localStorage para que el carrito sobreviva al refresco
- Deducir inventario al "añadir al carrito" — solo reservar stock en el checkout, liberar al expirar la sesión
- Actualizar el estado del pedido desde el cliente — usar webhooks de Stripe para
checkout.session.completed
- Punto flotante para dinero — almacenar como enteros (céntimos), formatear solo para mostrar