DOMAIN: E-commerce Completo
Skill gerado a partir do pack templates-claude-code. Arquivo de origem: dominio/02-ecommerce-completo.md. Use como baseline e adapte ao projeto antes de mudancas grandes.
Conteudo do template
CONTEXT
E-commerce parece simples até o primeiro pedido com problema. Inventário que vende negativo,
carrinho que não persiste entre dispositivos, tax calculation incorreta, estado de pedido
inconsistente entre sistemas — são os bugs que chegam às 2h da manhã em produção.
O coração do e-commerce é uma máquina de estados de pedido — modelar errado gera dívida técnica impossível de pagar.
STACK ASSUMPTIONS
- Backend: Node.js / Laravel / Django / Rails
- DB: PostgreSQL (transações, locks otimistas/pessimistas, constraints)
- Cache: Redis (carrinho, sessions, rate limiting)
- Storage: S3/GCS para imagens de produto
- Search: Elasticsearch ou Algolia para catálogo
- Queue: BullMQ / SQS para processamento de pedidos assíncrono
- Tax: TaxJar / Avalara ou cálculo próprio (ICMS BR)
CORE CONCEPTS
- SKU vs Product: Product é entidade de catálogo; SKU é a unidade vendável (cor+tamanho)
- Cart Session: carrinho anônimo (session/Redis) vs carrinho autenticado (DB) — merge no login
- Inventory Hold: reservar estoque no add-to-cart vs no checkout vs no pagamento
- Order State Machine: pending → confirmed → processing → shipped → delivered | canceled | refunded
- Proration/Return: pedido de devolução cria novo fluxo, não desfaz o original
- Tax Calculation: deve ocorrer no checkout final, não no carrinho (endereço pode mudar)
- Checkout Session: snapshot imutável do carrinho no momento do checkout (preços podem mudar)
ARCHITECTURE RULES
Catálogo de Produtos
- Separar
Product (info de catálogo) de ProductVariant (SKU, preço, estoque)
- Imagens: armazenar URLs do CDN, nunca base64 em banco
- Slug único global para SEO (não apenas por categoria)
- Soft delete produtos — pedidos antigos precisam referenciar produto deletado
Carrinho
- Carrinho anônimo:
cart_id em cookie + dados no Redis (TTL 7 dias)
- Carrinho autenticado: salvar no banco ao fazer login (merge com anônimo)
- Ao merge: se mesmo produto em ambos, somar quantidades respeitando estoque
- Nunca confiar no preço do carrinho para cobrança — sempre recalcular na hora do checkout
Gestão de Estoque
- Usar
SELECT ... FOR UPDATE (pessimistic lock) ao decrementar estoque
- Reserva temporária no add-to-cart: decremento em
reserved_quantity, não em quantity
- Liberar reserva se checkout não completado em 15 min (job agendado)
- Confirmar decremento em
quantity apenas com pagamento confirmado
- Nunca permitir quantidade negativa — constraint no banco + validação na aplicação
Máquina de Estados do Pedido
pending → payment_processing → confirmed → processing → shipped → delivered
↓ ↓ ↓
payment_failed canceled return_requested → returned → refunded
- Transições de estado via eventos (domain events ou state machine lib)
- Cada transição dispara: email, inventory update, webhook para ERP
- Nunca saltar estados — validar transição antes de executar
Checkout Flow
- Criar CheckoutSession (snapshot de carrinho: produtos, preços, quantidades)
- Calcular tax com endereço de entrega final
- Reservar estoque
- Processar pagamento
- Em sucesso: confirmar pedido, decrementar estoque real, liberar reserva
- Em falha: liberar reserva, notificar usuário
ROUTING TABLE
| Trigger |
Action |
| GET /products?category=X&sort=price |
Buscar com filtros, paginação cursor-based |
| GET /products/:slug |
Retornar produto com todas as variantes e estoque |
| POST /cart/items |
Adicionar ao carrinho (criar se não existe, verificar estoque) |
| PATCH /cart/items/:sku |
Atualizar quantidade (verificar disponibilidade) |
| DELETE /cart/items/:sku |
Remover item do carrinho |
| POST /cart/merge |
Merge carrinho anônimo com autenticado no login |
| POST /checkout/session |
Criar checkout session (snapshot + tax calc + reserva) |
| POST /checkout/complete |
Processar pagamento → confirmar ou falhar pedido |
| GET /orders/:id |
Detalhe do pedido (com linha do tempo de estados) |
| POST /orders/:id/cancel |
Cancelar se estado permite → liberar estoque → estorno |
| POST /orders/:id/return |
Iniciar fluxo de devolução → gerar label → aguardar recebimento |
| POST /admin/inventory/adjust |
Ajuste manual de estoque com motivo (auditoria) |
| GET /admin/orders?status=pending |
Listar com filtros, server-side pagination |
| POST /webhooks/shipping |
Atualizar tracking status → notificar cliente |
| POST /admin/products/:id/variants |
Criar nova variante (SKU, preço, estoque inicial) |
CRITICAL RULES
- NUNCA decrementar estoque sem transação de banco de dados com lock
- NUNCA confiar no preço enviado pelo frontend — sempre buscar do banco no checkout
- SEMPRE criar CheckoutSession como snapshot imutável antes de cobrar
- SEMPRE usar idempotency key no gateway de pagamento para evitar double charge
- Cancelamento só é possível em estados:
pending, payment_failed, confirmed
- Pedido em
shipped não pode ser cancelado — apenas devolvido
- Tax calculation com endereço final, não endereço salvo no perfil
- Imagens de produto: gerar thumbnails no upload (não on-the-fly)
- Variant sem estoque deve retornar
available: false, não 404
- Carrinho abandonado (>1h sem checkout): disparar email automation
- Multi-currency: armazenar preço base em centavos + moeda base; converter na exibição
- Soft delete em produtos: nunca deletar produto com pedido associado
COMMON PITFALLS
❌ Race condition de estoque
-- ERRADO: dois usuários podem comprar o último item simultaneamente
SELECT quantity FROM product_variants WHERE id = $1;
-- (aqui outro usuário lê quantity = 1 também)
UPDATE product_variants SET quantity = quantity - 1 WHERE id = $1;
-- CORRETO: pessimistic lock
BEGIN;
SELECT quantity FROM product_variants WHERE id = $1 FOR UPDATE;
-- agora apenas esta transaction lê o valor
UPDATE product_variants SET quantity = quantity - 1 WHERE id = $1 AND quantity > 0;
COMMIT;
❌ Preço salvo no carrinho sem re-validação
// ERRADO: usar preço do carrinho/frontend diretamente
const total = cartItems.reduce((sum, item) => sum + item.price * item.qty, 0);
// CORRETO: buscar preço atual do banco
const variants = await db.productVariants.findMany({ where: { id: { in: cartItemIds } } });
const total = cartItems.reduce((sum, item) => {
const variant = variants.find(v => v.id === item.variantId);
return sum + variant.price * item.qty; // preço do banco
}, 0);
❌ Cancelar pedido já enviado
// ERRADO: permite cancelar qualquer pedido
await order.update({ status: 'canceled' });
// CORRETO: validar transição
const cancelableStates = ['pending', 'payment_failed', 'confirmed'];
if (!cancelableStates.includes(order.status)) {
throw new BusinessError('ORDER_NOT_CANCELABLE', `Cannot cancel order in state: ${order.status}`);
}
QUALITY GATES
FORBIDDEN
- Armazenar preço do produto apenas no carrinho sem re-validação
- Decrementar estoque fora de uma transação de banco com lock
- Permitir transições de estado de pedido sem validação (ex: de
delivered para pending)
- Deletar fisicamente produtos com histórico de pedidos
- Calcular tax com endereço do perfil do usuário em vez do endereço de entrega do checkout
- Pular a etapa de CheckoutSession e cobrar diretamente a partir dos dados do carrinho
1---2name: tpl-dominio-ecommerce-completo3description: Template do pack (dominio/02-ecommerce-completo.md). Orienta o agente em regras de negocio e requisitos de produto alinhado a esse contexto.4---56# DOMAIN: E-commerce Completo78Skill gerado a partir do pack `templates-claude-code`. Arquivo de origem: `dominio/02-ecommerce-completo.md`. Use como baseline e adapte ao projeto antes de mudancas grandes.910## Conteudo do template1112## CONTEXT13E-commerce parece simples até o primeiro pedido com problema. Inventário que vende negativo,14carrinho que não persiste entre dispositivos, tax calculation incorreta, estado de pedido15inconsistente entre sistemas — são os bugs que chegam às 2h da manhã em produção.16O coração do e-commerce é uma máquina de estados de pedido — modelar errado gera dívida técnica impossível de pagar.1718## STACK ASSUMPTIONS19- Backend: Node.js / Laravel / Django / Rails20- DB: PostgreSQL (transações, locks otimistas/pessimistas, constraints)21- Cache: Redis (carrinho, sessions, rate limiting)22- Storage: S3/GCS para imagens de produto23- Search: Elasticsearch ou Algolia para catálogo24- Queue: BullMQ / SQS para processamento de pedidos assíncrono25- Tax: TaxJar / Avalara ou cálculo próprio (ICMS BR)2627## CORE CONCEPTS28- **SKU vs Product**: Product é entidade de catálogo; SKU é a unidade vendável (cor+tamanho)29- **Cart Session**: carrinho anônimo (session/Redis) vs carrinho autenticado (DB) — merge no login30- **Inventory Hold**: reservar estoque no add-to-cart vs no checkout vs no pagamento31- **Order State Machine**: pending → confirmed → processing → shipped → delivered | canceled | refunded32- **Proration/Return**: pedido de devolução cria novo fluxo, não desfaz o original33- **Tax Calculation**: deve ocorrer no checkout final, não no carrinho (endereço pode mudar)34- **Checkout Session**: snapshot imutável do carrinho no momento do checkout (preços podem mudar)3536## ARCHITECTURE RULES3738### Catálogo de Produtos39- Separar `Product` (info de catálogo) de `ProductVariant` (SKU, preço, estoque)40- Imagens: armazenar URLs do CDN, nunca base64 em banco41- Slug único global para SEO (não apenas por categoria)42- Soft delete produtos — pedidos antigos precisam referenciar produto deletado4344### Carrinho45- Carrinho anônimo: `cart_id` em cookie + dados no Redis (TTL 7 dias)46- Carrinho autenticado: salvar no banco ao fazer login (merge com anônimo)47- Ao merge: se mesmo produto em ambos, somar quantidades respeitando estoque48- Nunca confiar no preço do carrinho para cobrança — sempre recalcular na hora do checkout4950### Gestão de Estoque51- Usar `SELECT ... FOR UPDATE` (pessimistic lock) ao decrementar estoque52- Reserva temporária no add-to-cart: decremento em `reserved_quantity`, não em `quantity`53- Liberar reserva se checkout não completado em 15 min (job agendado)54- Confirmar decremento em `quantity` apenas com pagamento confirmado55- Nunca permitir quantidade negativa — constraint no banco + validação na aplicação5657### Máquina de Estados do Pedido58```59pending → payment_processing → confirmed → processing → shipped → delivered60 ↓ ↓ ↓61 payment_failed canceled return_requested → returned → refunded62```63- Transições de estado via eventos (domain events ou state machine lib)64- Cada transição dispara: email, inventory update, webhook para ERP65- Nunca saltar estados — validar transição antes de executar6667### Checkout Flow681. Criar CheckoutSession (snapshot de carrinho: produtos, preços, quantidades)692. Calcular tax com endereço de entrega final703. Reservar estoque714. Processar pagamento725. Em sucesso: confirmar pedido, decrementar estoque real, liberar reserva736. Em falha: liberar reserva, notificar usuário7475## ROUTING TABLE76| Trigger | Action |77|---------|--------|78| GET /products?category=X&sort=price | Buscar com filtros, paginação cursor-based |79| GET /products/:slug | Retornar produto com todas as variantes e estoque |80| POST /cart/items | Adicionar ao carrinho (criar se não existe, verificar estoque) |81| PATCH /cart/items/:sku | Atualizar quantidade (verificar disponibilidade) |82| DELETE /cart/items/:sku | Remover item do carrinho |83| POST /cart/merge | Merge carrinho anônimo com autenticado no login |84| POST /checkout/session | Criar checkout session (snapshot + tax calc + reserva) |85| POST /checkout/complete | Processar pagamento → confirmar ou falhar pedido |86| GET /orders/:id | Detalhe do pedido (com linha do tempo de estados) |87| POST /orders/:id/cancel | Cancelar se estado permite → liberar estoque → estorno |88| POST /orders/:id/return | Iniciar fluxo de devolução → gerar label → aguardar recebimento |89| POST /admin/inventory/adjust | Ajuste manual de estoque com motivo (auditoria) |90| GET /admin/orders?status=pending | Listar com filtros, server-side pagination |91| POST /webhooks/shipping | Atualizar tracking status → notificar cliente |92| POST /admin/products/:id/variants | Criar nova variante (SKU, preço, estoque inicial) |9394## CRITICAL RULES951. **NUNCA** decrementar estoque sem transação de banco de dados com lock962. **NUNCA** confiar no preço enviado pelo frontend — sempre buscar do banco no checkout973. **SEMPRE** criar CheckoutSession como snapshot imutável antes de cobrar984. **SEMPRE** usar idempotency key no gateway de pagamento para evitar double charge995. Cancelamento só é possível em estados: `pending`, `payment_failed`, `confirmed`1006. Pedido em `shipped` não pode ser cancelado — apenas devolvido1017. Tax calculation com endereço final, não endereço salvo no perfil1028. Imagens de produto: gerar thumbnails no upload (não on-the-fly)1039. Variant sem estoque deve retornar `available: false`, não 40410410. Carrinho abandonado (>1h sem checkout): disparar email automation10511. Multi-currency: armazenar preço base em centavos + moeda base; converter na exibição10612. Soft delete em produtos: nunca deletar produto com pedido associado107108## COMMON PITFALLS109110### ❌ Race condition de estoque111```sql112-- ERRADO: dois usuários podem comprar o último item simultaneamente113SELECT quantity FROM product_variants WHERE id = $1;114-- (aqui outro usuário lê quantity = 1 também)115UPDATE product_variants SET quantity = quantity - 1 WHERE id = $1;116117-- CORRETO: pessimistic lock118BEGIN;119SELECT quantity FROM product_variants WHERE id = $1 FOR UPDATE;120-- agora apenas esta transaction lê o valor121UPDATE product_variants SET quantity = quantity - 1 WHERE id = $1 AND quantity > 0;122COMMIT;123```124125### ❌ Preço salvo no carrinho sem re-validação126```javascript127// ERRADO: usar preço do carrinho/frontend diretamente128const total = cartItems.reduce((sum, item) => sum + item.price * item.qty, 0);129130// CORRETO: buscar preço atual do banco131const variants = await db.productVariants.findMany({ where: { id: { in: cartItemIds } } });132const total = cartItems.reduce((sum, item) => {133 const variant = variants.find(v => v.id === item.variantId);134 return sum + variant.price * item.qty; // preço do banco135}, 0);136```137138### ❌ Cancelar pedido já enviado139```javascript140// ERRADO: permite cancelar qualquer pedido141await order.update({ status: 'canceled' });142143// CORRETO: validar transição144const cancelableStates = ['pending', 'payment_failed', 'confirmed'];145if (!cancelableStates.includes(order.status)) {146 throw new BusinessError('ORDER_NOT_CANCELABLE', `Cannot cancel order in state: ${order.status}`);147}148```149150## QUALITY GATES151- [ ] Estoque nunca fica negativo (constraint no DB + validação na aplicação)152- [ ] Preço sempre re-validado no checkout a partir do banco153- [ ] Lock pessimista implementado no decremento de estoque154- [ ] Máquina de estados com transições validadas explicitamente155- [ ] Carrinho anônimo faz merge correto ao autenticar156- [ ] CheckoutSession é imutável após criação157- [ ] Tax calculado com endereço final de entrega158- [ ] Idempotency key usado em chamadas ao gateway de pagamento159- [ ] Email disparado em cada mudança de estado relevante do pedido160- [ ] Produtos soft-deleted mas visíveis em pedidos históricos161162## FORBIDDEN163- Armazenar preço do produto apenas no carrinho sem re-validação164- Decrementar estoque fora de uma transação de banco com lock165- Permitir transições de estado de pedido sem validação (ex: de `delivered` para `pending`)166- Deletar fisicamente produtos com histórico de pedidos167- Calcular tax com endereço do perfil do usuário em vez do endereço de entrega do checkout168- Pular a etapa de CheckoutSession e cobrar diretamente a partir dos dados do carrinho