E-Commerce
Regras
- Estado do carrinho: store Zustand com persistência em
localStorage — sobrevive a recarregamentos de página e fechamento de aba
- Variantes de produto: modelar como SKUs separados com produto pai compartilhado — cada variante tem seu próprio preço, estoque, imagem
- Checkout: Stripe Checkout Sessions (lado do servidor) — nunca enviar preços do cliente
- Estoque: verificar estoque no servidor antes de criar sessão de checkout — proteção contra condição de corrida com transações de banco de dados
- Ciclo de vida do pedido:
pending → paid → shipped → delivered — atualizar via eventos de webhook, não por ações do cliente
- Listagem de produtos: filtragem/ordenação/paginação no servidor — parâmetros de URL para visualizações filtradas compartilháveis
- Preços: armazenar em centavos (inteiros) — exibir com
Intl.NumberFormat e código da moeda
- Operações do carrinho: adicionar, remover, atualizar quantidade, limpar — validar estoque em toda ação do carrinho
Padrões
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 preços do cliente para criar checkout — sempre resolver preços no lado do servidor a partir do seu banco de dados
- Armazenar carrinho apenas no estado React — usar Zustand persistido ou localStorage para que o carrinho sobreviva a recarregamentos
- Deduzir estoque ao "adicionar ao carrinho" — reservar estoque apenas no checkout, liberar na expiração da sessão
- Atualizar status do pedido a partir do cliente — usar webhooks do Stripe para
checkout.session.completed
- Ponto flutuante para dinheiro — armazenar como inteiros (centavos), formatar apenas para exibição