Menu
Guides
VTEX IO Apps

VTEX IO Apps
🛒 Abandoned Cart Sync – VTEX IO
Community extension
Version: 0.0.1
Latest version: 0.0.1

Sistema completo de sincronização, recuperação e limpeza de carrinhos abandonados em ambientes VTEX IO.

Este módulo implementa:

  • Persistência de sessões de carrinho no Master Data v1
  • Recuperação inteligente de carrinhos anteriores em múltiplas sessões
  • Sugestão de recuperação com modal na loja (Store Framework)
  • Sincronização após pedido realizado para evitar itens duplicados
  • Padronização de payload via Base64 (btoa/atob)
  • Validação estruturada com Zod
  • Compatível com Loja (Store) e Checkout (v5/v6)

📌 Sumário


🎯 Objetivo

Este projeto existe para resolver um problema comum em e-commerces com múltiplas sessões:

O cliente começa um carrinho em um dispositivo e continua em outro → carrinhos ficam divergentes.

Este módulo fornece:

  • Persistência confiável das sessões de carrinho
  • Recuperação automática das sessões anteriores
  • Sincronização pós-pedido removendo itens já comprados
  • Modal reativa para sugerir a recuperação do carrinho

🧱 Arquitetura


_10
[Loja / Store Framework] --\
_10
[Checkout v5/v6] -----------> [Middleware VTEX IO] --> [Master Data v1]
_10
|
_10
--> Persistência de carrinhos
_10
--> Limpeza pós-compra
_10
--> Recuperação de itens


🔥 Endpoints da API

Todos os endpoints utilizam payload Base64 via { "data": "<base64>" }.


POST /cart-sync/upsertAbandonedCart

Cria ou atualiza um registro de carrinho abandonado.

Request

Headers


_10
Content-Type: application/json

Body


_10
{
_10
"data": "BASE64(JSON)"
_10
}

JSON interno (antes do btoa):


_12
{
_12
"orderFormId": "string >= 5 chars",
_12
"email": "user@example.com",
_12
"items": [
_12
{
_12
"skuId": "string",
_12
"productId": "string",
_12
"quantity": 1,
_12
"sellerId": "1"
_12
}
_12
]
_12
}

  • orderFormId (string): ID do orderForm da VTEX.
  • email (string, email válido): e-mail do cliente.
  • items (array): lista de itens do carrinho.

Cada item (AbandonedCartItem):

  • skuId (string, obrigatório)
  • productId (string, obrigatório)
  • quantity (int > 0, obrigatório)
  • sellerId (string, opcional, default "1")

Validação

No backend:

  • data é decodificado: parseJSONFromAtob(body.data).
  • Validado com zod via upsertSchema:
    • orderFormId: z.string().min(5)
    • email: z.string().email()
    • items: z.array(itemSchema)

Se falhar:


_10
HTTP 400
_10
{
_10
"error": "Invalid payload",
_10
"details": "<mensagem de erro do Zod>"
_10
}

Comportamento

  1. Busca um documento existente por orderFormId em AC (Master Data v1).
  2. Se não existe:
    • Cria um novo documento:
      • orderFormId
      • email
      • items
      • updatedDate = agora (ISO)
      • completed = false
    • Retorna 201 com o documento criado.
  3. Se já existe:
    • Atualiza:
      • email
      • items
      • updatedDate
    • Retorna 200 com { id, updatedDate }.

POST /cart-sync/getAbandonedCarts

Retorna carrinhos abertos para um email, exceto o carrinho da sessão atual.

Request

Headers


_10
Content-Type: application/json

Body


_10
{
_10
"data": "BASE64(JSON)"
_10
}

JSON interno:


_10
{
_10
"orderFormId": "string >= 5 chars",
_10
"email": "user@example.com"
_10
}

Response


_15
[
_15
{
_15
"id": "a8a61e91-2ebc-4914-801d-1eac530fa2e1",
_15
"orderFormId": "5da8cd1cc9704c98b762ee0767fa7a66",
_15
"items": [
_15
{
_15
"skuId": "40184",
_15
"productId": "7763",
_15
"quantity": 1,
_15
"sellerId": "1",
_15
"scId": "1"
_15
}
_15
]
_15
}
_15
]

Validação

  • Decode via parseJSONFromAtob.
  • Validação com getAbandonedCartsSchema (zod):
    • orderFormId: z.string().min(5)
    • email: z.string().email()

Erro:


_10
HTTP 400
_10
{
_10
"error": "Invalid payload",
_10
"details": "<mensagem do Zod>"
_10
}

Comportamento

  1. Normaliza o e-mail: email.trim().
  2. Define uma janela de consulta (por exemplo days = 7).
  3. Chama md.searchOpenWithinDays(email, orderFormId, days):
    • completed = false
    • updatedDate entre now - days e now
    • orderFormId \<\> <orderFormId atual>
  4. Retorna 200 com array de AbandonedCart:

_11
HTTP 200
_11
[
_11
{
_11
"id": "string",
_11
"orderFormId": "string",
_11
"email": "user@example.com",
_11
"items": [ ... ],
_11
"updatedDate": "2025-11-14T12:34:56.789Z",
_11
"completed": false
_11
}
_11
]


POST /cart-sync/setAbandonedCartCompleted

Usado quando um pedido é finalizado (orderPlaced).
Remove itens dos carrinhos antigos e marca como completed quando esvaziados.

Request

Headers


_10
Content-Type: application/json

Body


_10
{
_10
"data": "BASE64(JSON)"
_10
}

JSON interno:


_11
{
_11
"email": "user@example.com",
_11
"items": [
_11
{
_11
"productId": "string",
_11
"skuId": "string",
_11
"quantity": 2,
_11
"sellerId": "1"
_11
}
_11
]
_11
}

orderFormId pode existir no payload (opcional), mas não é obrigatório na abordagem atual.

Validação

Schema setAbandonedCompletedSchema (Zod):


_10
const setAbandonedCompletedSchema = z.object({
_10
orderFormId: z.string().optional(),
_10
email: z.string().email(),
_10
items: z.array(itemSchema).nonempty('At least one item is required'),
_10
})

Erros de parse/validação → 400:


_10
{
_10
"error": "Invalid payload",
_10
"details": "<mensagem do Zod>"
_10
}

Comportamento

  1. Normaliza o e-mail: email.trim().

  2. Monta mapa purchasedQtyBySku (quantidades compradas por SKU):


    _10
    const purchasedQtyBySku: Record<string, number> = {}
    _10
    _10
    for (const item of purchasedItems) {
    _10
    const skuKey = String(item.skuId)
    _10
    purchasedQtyBySku[skuKey] =
    _10
    (purchasedQtyBySku[skuKey] || 0) + Number(item.quantity || 0)
    _10
    }

  3. Busca todos os carrinhos abertos para aquele e-mail:

    • openDocs = md.searchOpenWithinDays(email, undefined, 30)
  4. Normaliza os items de cada doc (caso venham como string do Master Data):


    _15
    const mdItemsSchema = z.preprocess((value) => {
    _15
    if (typeof value === 'string') {
    _15
    try {
    _15
    return JSON.parse(value)
    _15
    } catch {
    _15
    return []
    _15
    }
    _15
    }
    _15
    return value
    _15
    }, itemsSchema)
    _15
    _15
    openDocs = openDocs.map(openDoc => ({
    _15
    ...openDoc,
    _15
    items: mdItemsSchema.parse(openDoc.items || []),
    _15
    }))

  5. Para cada doc:

    • Itera sobre originalItems.
    • Se sku não foi comprado → item permanece.
    • Se quantity comprada for maior/igual → remove item.
    • Se for menor → reduz quantity no carrinho.
    • Se não sobra nenhum item → completed = true.
    • Se sobra algo → atualiza items com a nova lista.
  6. Resposta:


_10
HTTP 200
_10
{
_10
"message": "Order completion sync executed (by email/items)",
_10
}


🔐 Formato do Payload (Base64)

Todos os endpoints devem receber:


_10
{
_10
"data": "BASE64(JSON_STRING)"
_10
}

Exemplo de envio no front:


_10
const body = {
_10
data: btoa(JSON.stringify(payload)),
_10
}
_10
_10
await fetch('/cart-sync/upsertAbandonedCart', {
_10
method: 'POST',
_10
headers: { 'Content-Type': 'application/json' },
_10
body: JSON.stringify(body),
_10
})

Decodificação no backend:


_10
parseJSONFromAtob(body.data)
_10
// = atob(base64) -> JSON.parse(...)

Motivos:

  • Evitar logs legíveis de dados sensíveis
  • Padronizar entrada
  • Evitar erros quando o payload contém caracteres especiais

📏 Esquemas (Zod)

itemSchema


_10
const itemSchema = z.object({
_10
skuId: z.string().min(1),
_10
productId: z.string().min(1),
_10
quantity: z.number().int().positive(),
_10
sellerId: z.string().optional().default('1'),
_10
})

upsertSchema


_10
const upsertSchema = z.object({
_10
orderFormId: z.string().min(5),
_10
email: z.string().email(),
_10
items: z.array(itemSchema),
_10
})

getAbandonedCartsSchema


_10
const getAbandonedCartsSchema = z.object({
_10
orderFormId: z.string().min(5),
_10
email: z.string().email(),
_10
})

setAbandonedCompletedSchema


_10
const setAbandonedCompletedSchema = z.object({
_10
orderFormId: z.string().optional(),
_10
email: z.string().email(),
_10
items: z.array(itemSchema).nonempty('At least one item is required'),
_10
})


🔄 Fluxo funcional

1. Cliente navega na loja

O componente AbandonedCartSync roda na loja inteira (exceto checkout) e:

  • observa mudanças no orderForm (useOrderForm)
  • envia orderFormId, email, items/cart-sync/upsertAbandonedCart

2. Cliente volta em outra sessão

O componente AbandonedCartRetriever:

  • consulta /cart-sync/getAbandonedCarts
  • compara items antigos vs carrinho atual
  • busca detalhes no catálogo (/api/catalog_system/pub/products/search)
  • mostra uma modal com cards de produtos para recuperação
  • adiciona os itens ao carrinho via addItems do vtex.order-manager

3. Cliente finaliza pedido

Na página de orderPlaced:

  • o componente AbandonedCartSyncCleanUp:
    • coleta email e items do pedido (via vtex.order-placed)
    • envia payload → /cart-sync/setAbandonedCartCompleted

4. Backend limpa carrinhos antigos

  • Remove quantidades compradas dos carrinhos abertos do mesmo email
  • Se um carrinho ficar sem itens → completed = true
  • Se ainda sobraram itens → carrinho permanece aberto, com items ajustados

🧩 Componentes React da loja

AbandonedCartSync

  • Executado na loja (Store Framework), exceto em rotas de checkout:
    • Detecta rota via useRuntime + fallback em window.location.
  • Usa useOrderForm para obter:
    • orderFormId
    • email
    • items
    • salesChannel
  • Envia updates para /cart-sync/upsertAbandonedCart.

AbandonedCartRetriever

  • Executado na loja.
  • Usa useOrderForm:
    • Pega carrinho atual
    • Busca carrinhos antigos via /cart-sync/getAbandonedCarts
    • Exibe modal com:
      • imagem do produto
      • preço de/por
      • quantidade
  • Ao clicar em “Sim, adicionar”:
    • Usa addItems do vtex.order-manager para adicionar ao carrinho atual.
  • Ao clicar em “Não, obrigado”:
    • Marca recusa em sessionStorage para não exibir novamente na mesma sessão.

AbandonedCartSyncCleanUp

  • Executado apenas na página orderPlaced (store.orderplaced).
  • Usa OrderGroupContext de vtex.order-placed:
    • obtém orders do grupo
    • extrai email e items
  • Envia payload para /cart-sync/setAbandonedCartCompleted.

🔒 Considerações de segurança

  • Base64 não é criptografia, mas:
    • reduz risco de log de dados sensíveis em texto puro.
    • evita problemas com caracteres especiais em logs/transporte.
  • email e itens são validados com Zod:
    • garante tipos corretos
    • evita payload malformado
  • Itens provenientes do Master Data:
    • podem vir como string (JSON serializado) ou como array.
    • são sempre normalizados e validados com Zod antes de uso.

📄 Licença

MIT — Sinta-se livre para adaptar, expandir ou reutilizar este módulo em outros projetos VTEX IO.

On this page
Was this helpful?