# Modelo: Multi-Step Checkout

> ✅ **STATUS: Quantum Tier (Server-Side Completo)**
>
> Checkout multi-etapa com rastreamento 100% Cloudflare Native (Workers + D1).
> Inclui enriquecimento progressivo de identidade, deduplicação e dispatch server-side para Meta CAPI v25.0, GA4 MP e TikTok API v1.3.

Checkout próprio dividido em múltiplas etapas: dados pessoais → endereço → pagamento → confirmação.

**Quando usar:** checkout integrado na página com múltiplas telas/abas (cartão de crédito, boleto, PIX), sem redirecionamento para plataforma externa.

---

## 🏗️ ARQUITETURA TÉCNICA (Quantum Tier)

O rastreamento segue a lógica de progressão de checkout com enriquecimento de identidade:
1. **Site**: Captura progressivamente dados de identidade ao preencher campos.
2. **Worker**: Recebe cada etapa, enriquece Identity Graph e despacha para APIs.
3. **D1 Database**: Mantém estado do checkout vinculado ao `cdp_uid`.

---

## Estrutura típica

```
Step 1: Dados pessoais (nome, email, CPF)
Step 2: Dados de entrega (endereço) — opcional em infoprodutos
Step 3: Pagamento (cartão/PIX/boleto)
Step 4: Confirmação / Obrigado
```

---

## 📘 EVENTOS PRINCIPAIS

| Evento | Trigger | Meta | GA4 | TikTok | Prioridade |
|--------|---------|------|-----|--------|-----------|
| `InitiateCheckout` | Chegada no step 1 | `InitiateCheckout` | `begin_checkout` | `InitiateCheckout` | 🔴 Crítico |
| `AddPaymentInfo` | Preenchimento dos dados de pagamento (step 3) | `AddPaymentInfo` | `add_payment_info` | `AddPaymentInfo` | 🟡 Importante |
| `Purchase` | Confirmação de pagamento (step 4) | `Purchase` (CAPI) | `purchase` (MP) | `CompletePayment` | 🔴 Crítico |
| `CheckoutAbandonment` | Saída sem completar (visibilitychange) | `CustomEvent` | `checkout_abandon` | — | 🟢 Recomendado |

---

## 🛠️ PASSO 1: CONFIGURAÇÃO DO SITE

### 1.1 SDK de Rastreamento (Header)
```html
<!-- Inserir no <head> -->
<script src="/js/cdpTrack.js" async></script>
<script>
  window.cdpConfig = {
    workerUrl: '/track',  // Same-Domain (furtivo)
    metaId: 'SEU_PIXEL_ID',
    ga4Id: 'G-XXXXXXXX',
    tiktokId: 'C4XXXXXXXXXXXXXXX',
    checkout: {
      value: 197.00,           // ⚠️ substituir pelo valor real
      currency: 'BRL',
      productId: 'PROD_001',     // ⚠️ substituir pelo ID do produto
      productName: 'Produto Premium' // ⚠️ substituir pelo nome do produto
    }
  };
</script>
```

### 1.2 Geração de cdp_uid (First Access)
```javascript
// Inserir no início do <body> (primeiro script)
<script>
(function() {
  // Gera cdp_uid se não existir (1 ano de expiração)
  const existing = document.cookie.match(/cdp_uid=([^;]+)/);
  if (!existing) {
    const uid = 'usr_' + Math.random().toString(36).substring(2, 10) + Date.now();
    const expiry = new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toUTCString();
    document.cookie = `cdp_uid=${encodeURIComponent(uid)}; expires=${expiry}; path=/; SameSite=Lax`;
  }
})();
</script>
```

### 1.3 STEP 1: InitiateCheckout (Chegada na página)
```javascript
// Ao carregar a página de checkout
<script>
window.addEventListener('DOMContentLoaded', async () => {
  const cdp_uid = document.cookie.match(/cdp_uid=([^;]+)/)?.[1] || '';

  await cdpTrack.track('InitiateCheckout', {
    cdp_uid,
    step: 1,
    value: window.cdpConfig.checkout.value,
    currency: window.cdpConfig.checkout.currency,
    content_name: window.cdpConfig.checkout.productName,
    content_id: window.cdpConfig.checkout.productId
  });
});
</script>
```

### 1.4 STEP 2: Captura Progressiva de Identidade
```javascript
// Captura email ao preencher (enrichment para Advanced Matching)
document.querySelector('[name="email"], [type="email"]')?.addEventListener('blur', async function() {
  const email = this.value.trim();

  // Envia identidade atualizada para o Worker
  await cdpTrack.track('IdentityUpdate', {
    email
  });
});

// Captura telefone ao preencher
document.querySelector('[name="phone"], [name="telefone"], [type="tel"]')?.addEventListener('blur', async function() {
  const phone = this.value.trim();

  await cdpTrack.track('IdentityUpdate', {
    phone
  });
});

// Captura nome ao preencher
document.querySelector('[name="name"], [name="nome"]')?.addEventListener('blur', async function() {
  const full = this.value.trim();
  const parts = full.split(/\s+/);

  await cdpTrack.track('IdentityUpdate', {
    first_name: parts[0],
    last_name: parts.slice(1).join(' ')
  });
});

// Captura CPF (opcional)
document.querySelector('[name="cpf"], [name="documento"]')?.addEventListener('blur', async function() {
  const cpf = this.value.replace(/\D/g, '');

  await cdpTrack.track('IdentityUpdate', {
    cpf
  });
});
```

### 1.5 STEP 3: AddPaymentInfo (Chegada na tela de pagamento)
```javascript
// Adaptar trigger conforme estrutura do checkout (clique em aba, next button, etc.)
document.querySelector('#btn-pagamento, [data-step="pagamento"]')?.addEventListener('click', async function() {
  const cdp_uid = document.cookie.match(/cdp_uid=([^;]+)/)?.[1] || '';

  // Capturar método de pagamento selecionado
  const paymentMethod = document.querySelector('[name="payment-method"]:checked')?.value ||
                       document.querySelector('.payment-tab.active')?.dataset?.method ||
                       'unknown';

  await cdpTrack.track('AddPaymentInfo', {
    cdp_uid,
    step: 3,
    payment_method: paymentMethod
  });
});
```

### 1.6 STEP 4: Purchase (Confirmação de pagamento)
```javascript
// Disparar na página de obrigado OU ao receber callback de sucesso da API de pagamento
<script>
async function trackPurchaseConfirmed(transactionId) {
  const cdp_uid = document.cookie.match(/cdp_uid=([^;]+)/)?.[1] || '';

  await cdpTrack.track('Purchase', {
    cdp_uid,
    transaction_id: transactionId,
    step: 4,
    value: window.cdpConfig.checkout.value,
    currency: window.cdpConfig.checkout.currency,
    content_id: window.cdpConfig.checkout.productId,
    content_name: window.cdpConfig.checkout.productName
  });
}

// Exemplo: callback do gateway de pagamento
// paymentGateway.onSuccess((transactionId) => {
//   trackPurchaseConfirmed(transactionId);
// });
</script>
```

### 1.7 Checkout Abandonment (Opcional)
```javascript
// Detecta quando usuário sai sem completar o checkout
<script>
let checkoutState = { step: 0 };

// Atualiza step conforme usuário avança
document.querySelectorAll('[data-step]').forEach(el => {
  el.addEventListener('click', () => {
    checkoutState.step = parseInt(el.dataset.step);
  });
});

document.addEventListener('visibilitychange', async () => {
  if (document.visibilityState === 'hidden' && checkoutState.step > 0 && checkoutState.step < 4) {
    // Usuário saiu sem completar
    const cdp_uid = document.cookie.match(/cdp_uid=([^;]+)/)?.[1] || '';

    // Usa navigator.sendBeacon para garantir envio mesmo se browser encerrar
    navigator.sendBeacon('/track', JSON.stringify({
      event_name: 'CheckoutAbandonment',
      cdp_uid,
      step: checkoutState.step,
      value: window.cdpConfig.checkout.value
    }));
  }
});
</script>
```

---

## ⚡ PASSO 2: SERVIDOR (CLOUDFLARE WORKER)

### 2.1 Handler Principal
```javascript
// worker.js

export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);
    const cors = {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods': 'POST, OPTIONS',
      'Access-Control-Allow-Headers': 'Content-Type',
    };

    if (request.method === 'OPTIONS') {
      return new Response(null, { headers: cors });
    }

    if (url.pathname === '/track') {
      return handleTracking(request, env, ctx);
    }

    return new Response('Not Found', { status: 404 });
  }
};

async function handleTracking(request, env, ctx) {
  const body = await request.json();
  const cf = request.cf || {};
  const ip = cf.colo === 'XX' ? '8.8.8.8' : request.headers.get('CF-Connecting-IP');

  // Captura cookies do request
  const fbp = request.headers.get('Cookie')?.match(/_fbp=([^;]+)/)?.[1];
  const fbc = request.headers.get('Cookie')?.match(/_fbc=([^;]+)/)?.[1];
  const ttp = request.headers.get('Cookie')?.match(/_ttp=([^;]+)/)?.[1];

  const sessionData = {
    cdp_uid: body.cdp_uid,
    fbp,
    fbc,
    ttp,
    ip,
    user_agent: request.headers.get('User-Agent'),
  };

  // Atualiza Identity Graph no D1
  await updateIdentity(env.DB, sessionData, body);

  // Dispatch não-bloqueante para APIs
  ctx.waitUntil(dispatchEvents(body, sessionData, env));

  return new Response(JSON.stringify({ success: true }), {
    headers: { ...cors, 'Content-Type': 'application/json' }
  });
}
```

### 2.2 Atualização de Identity Graph (Enriquecimento Progressivo)
```javascript
async function updateIdentity(DB, sessionData, eventData) {
  const { event_name } = eventData;

  if (event_name === 'IdentityUpdate') {
    // Enrichment progressivo: atualiza campos conforme usuário preenche
    const updates = {};
    if (eventData.email) updates.email_hash = await sha256(eventData.email);
    if (eventData.phone) updates.phone_hash = await sha256('55' + eventData.phone.replace(/\D/g, ''));
    if (eventData.first_name) updates.first_name_hash = await sha256(eventData.first_name);
    if (eventData.last_name) updates.last_name_hash = await sha256(eventData.last_name);
    if (eventData.cpf) updates.cpf_hash = await sha256(eventData.cpf);

    await DB.prepare(`
      INSERT INTO identity_graph (cdp_uid, fbp, fbc, ttp, ip, user_agent,
        email_hash, phone_hash, first_name_hash, last_name_hash, cpf_hash, last_seen)
      VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
      ON CONFLICT(cdp_uid) DO UPDATE SET
        fbp = excluded.fbp,
        fbc = excluded.fbc,
        ttp = excluded.ttp,
        ip = excluded.ip,
        email_hash = COALESCE(excluded.email_hash, updates.email_hash),
        phone_hash = COALESCE(excluded.phone_hash, updates.phone_hash),
        first_name_hash = COALESCE(excluded.first_name_hash, updates.first_name_hash),
        last_name_hash = COALESCE(excluded.last_name_hash, updates.last_name_hash),
        cpf_hash = COALESCE(excluded.cpf_hash, updates.cpf_hash),
        last_seen = datetime('now')
    `).bind(
      sessionData.cdp_uid, sessionData.fbp, sessionData.fbc, sessionData.ttp, sessionData.ip, sessionData.user_agent,
      updates.email_hash || null, updates.phone_hash || null,
      updates.first_name_hash || null, updates.last_name_hash || null, updates.cpf_hash || null
    ).run();
  }
}
```

### 2.3 Dispatch de Eventos
```javascript
async function dispatchEvents(eventBody, sessionData, env) {
  const { event_name } = eventBody;

  if (event_name === 'InitiateCheckout') {
    await Promise.all([
      dispatchMeta('InitiateCheckout', sessionData, env, {
        content_name: eventBody.content_name,
        content_ids: [eventBody.content_id],
        num_items: 1
      }),
      dispatchGA4('begin_checkout', sessionData, env, {
        items: [{
          item_id: eventBody.content_id,
          item_name: eventBody.content_name,
          price: eventBody.value,
          quantity: 1
        }]
      }),
      dispatchTikTok('InitiateCheckout', sessionData, env, {
        content_id: eventBody.content_id
      }),
    ]);
  } else if (event_name === 'AddPaymentInfo') {
    await Promise.all([
      dispatchMeta('AddPaymentInfo', sessionData, env, {
        value: eventBody.value || window.cdpConfig?.checkout?.value,
        currency: eventBody.currency || 'BRL',
        content_ids: [eventBody.content_id || window.cdpConfig?.checkout?.productId]
      }),
      dispatchGA4('add_payment_info', sessionData, env, {
        payment_type: eventBody.payment_method
      }),
      dispatchTikTok('AddPaymentInfo', sessionData, env, {}),
    ]);
  } else if (event_name === 'Purchase') {
    await Promise.all([
      dispatchMeta('Purchase', sessionData, env, {
        content_ids: [eventBody.content_id],
        num_items: 1
      }),
      dispatchGA4('purchase', sessionData, env, {
        transaction_id: eventBody.transaction_id,
        items: [{
          item_id: eventBody.content_id,
          item_name: eventBody.content_name,
          price: eventBody.value,
          quantity: 1
        }]
      }),
      dispatchTikTok('CompletePayment', sessionData, env, {}),
    ]);
  } else if (event_name === 'CheckoutAbandonment') {
    // Apenas log no D1, não dispara para APIs
    await logAbandonment(env.DB, eventBody, sessionData);
  }
}
```

### 2.4 Meta CAPI v25.0
```javascript
async function dispatchMeta(eventName, sessionData, env, customData = {}) {
  if (!env.META_ACCESS_TOKEN || !env.META_PIXEL_ID) return;

  const identity = await getIdentity(env.DB, sessionData.cdp_uid);

  const payload = {
    data: [{
      event_name: eventName,
      event_time: Math.floor(Date.now() / 1000),
      event_id: crypto.randomUUID(),
      event_source_url: sessionData.page_url || '',
      action_source: 'website',
      user_data: {
        em: identity.email_hash ? [identity.email_hash] : undefined,
        ph: identity.phone_hash ? [identity.phone_hash] : undefined,
        fn: identity.first_name_hash ? [identity.first_name_hash] : undefined,
        ln: identity.last_name_hash ? [identity.last_name_hash] : undefined,
        fbp: sessionData.fbp,
        fbc: sessionData.fbc,
        client_ip_address: sessionData.ip,
        client_user_agent: sessionData.user_agent,
        external_id: await sha256(sessionData.cdp_uid),
      },
      custom_data: {
        value: customData.value || 0,
        currency: customData.currency || 'BRL',
        ...customData
      }
    }]
  };

  const res = await fetch(
    `https://graph.facebook.com/v25.0/${env.META_PIXEL_ID}/events?access_token=${env.META_ACCESS_TOKEN}`,
    {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(payload)
    }
  );

  if (!res.ok) {
    console.error('Meta CAPI Error:', await res.text());
  }
}
```

### 2.5 GA4 Measurement Protocol
```javascript
async function dispatchGA4(eventName, sessionData, env, eventData = {}) {
  if (!env.GA4_ID || !env.GA4_API_SECRET) return;

  const payload = {
    client_id: sessionData.cdp_uid,
    user_id: sessionData.cdp_uid,
    events: [{
      name: eventName,
      params: {
        page_location: sessionData.page_url || '',
        ...eventData
      }
    }]
  };

  const res = await fetch(
    `https://www.google-analytics.com/mp/collect?measurement_id=${env.GA4_ID}&api_secret=${env.GA4_API_SECRET}`,
    {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(payload)
    }
  );

  if (!res.ok) {
    console.error('GA4 Error:', await res.text());
  }
}
```

### 2.6 TikTok Events API v1.3
```javascript
async function dispatchTikTok(eventName, sessionData, env, eventData = {}) {
  if (!env.TIKTOK_PIXEL_ID || !env.TIKTOK_ACCESS_TOKEN) return;

  const payload = {
    pixel_code: env.TIKTOK_PIXEL_ID,
    event: eventName,
    event_id: crypto.randomUUID(),
    timestamp: new Date().toISOString(),
    context: {
      ad: {
        callback: sessionData.ttp
      },
      page: {
        url: sessionData.page_url || ''
      },
      user: {
        ip_address: sessionData.ip,
        user_agent: sessionData.user_agent,
        external_id: await sha256(sessionData.cdp_uid)
      }
    },
    properties: {
      value: eventData.value || 0,
      currency: eventData.currency || 'BRL',
      ...eventData
    }
  };

  const res = await fetch(
    'https://business-api.tiktok.com/open_api/v1.3/event/track/',
    {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Access-Token': env.TIKTOK_ACCESS_TOKEN
      },
      body: JSON.stringify(payload)
    }
  );

  if (!res.ok) {
    console.error('TikTok Error:', await res.text());
  }
}
```

### 2.7 Helpers
```javascript
async function sha256(value) {
  if (!value) return null;
  const encoder = new TextEncoder();
  const data = encoder.encode(value);
  const hash = await crypto.subtle.digest('SHA-256', data);
  return Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, '0')).join('');
}

async function getIdentity(DB, cdp_uid) {
  return await DB.prepare(
    'SELECT * FROM identity_graph WHERE cdp_uid = ?'
  ).bind(cdp_uid).first();
}

async function logAbandonment(DB, eventData, sessionData) {
  await DB.prepare(`
    INSERT INTO events_log (event_id, event_name, session_id, heat_score, page_url, created_at)
    VALUES (?, ?, ?, ?, ?, datetime('now'))
  `).bind(
    crypto.randomUUID(), 'CheckoutAbandonment', sessionData.cdp_uid,
    eventData.step || 1, sessionData.page_url || ''
  ).run();
}
```

---

## 📊 PASSO 3: SCHEMA D1

```sql
-- Identity Graph (já definido no Server Agent)
CREATE TABLE IF NOT EXISTS identity_graph (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  cdp_uid TEXT UNIQUE NOT NULL,
  fbp TEXT,
  fbc TEXT,
  ttp TEXT,
  ga_client_id TEXT,
  external_id TEXT,
  email_hash TEXT,
  phone_hash TEXT,
  first_name_hash TEXT,
  last_name_hash TEXT,
  cpf_hash TEXT,
  first_utm TEXT,
  heat_score_avg INTEGER DEFAULT 0,
  visit_count INTEGER DEFAULT 1,
  last_seen TEXT DEFAULT (datetime('now')),
  created_at TEXT DEFAULT (datetime('now'))
);

-- Events Log (para Checkout Abandonment)
CREATE TABLE IF NOT EXISTS events_log (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  event_id TEXT UNIQUE NOT NULL,
  event_name TEXT NOT NULL,
  platform TEXT,
  session_id TEXT,
  heat_score INTEGER DEFAULT 0,
  page_url TEXT,
  utm_source TEXT,
  utm_campaign TEXT,
  utm_medium TEXT,
  status TEXT DEFAULT 'pending',
  error_msg TEXT,
  created_at TEXT DEFAULT (datetime('now'))
);

CREATE INDEX IF NOT EXISTS idx_cdp_uid ON identity_graph(cdp_uid);
CREATE INDEX IF NOT EXISTS idx_events_id ON events_log(event_id);
CREATE INDEX IF NOT EXISTS idx_events_created ON events_log(created_at);
```

---

## ✅ CHECKLIST DE VERIFICAÇÃO

### Browser
- [ ] cdp_uid gerado no primeiro acesso e persistido por 1 ano
- [ ] InitiateCheckout disparado na chegada ao step 1
- [ ] IdentityUpdate disparado ao preencher cada campo (email, telefone, nome, CPF)
- [ ] AddPaymentInfo disparado na tela de pagamento (step 3)
- [ ] Purchase disparado na confirmação (step 4)
- [ ] CheckoutAbandonment usa `navigator.sendBeacon` para garantir envio

### Cloudflare Worker
- [ ] Endpoint `/track` configurado como Route no Cloudflare
- [ ] Identity Graph atualizado progressivamente conforme usuário preenche campos
- [ ] Meta CAPI v25.0 endpoint correto
- [ ] TikTok Events API v1.3 endpoint correto
- [ ] GA4 Measurement Protocol configurado
- [ ] CheckoutAbandonment logado apenas no D1 (não dispara para APIs)

### D1 Database
- [ ] Identity Graph com campos de hash (email, phone, first_name, last_name, cpf)
- [ ] Events Log para Checkout Abandonment
- [ ] Índices configurados para performance

---

## 🔄 FLUXO COMPLETO

```
1. Visitante acessa o checkout (Step 1)
   └── JS: gera cdp_uid → salva cookie (1 ano)
   └── JS: dispara InitiateCheckout → Worker
   └── Worker: salva Identity Graph básica (fbp, fbc, ttp)
   └── Worker: dispatch → Meta, GA4, TikTok (InitiateCheckout)

2. Visitante preenche dados pessoais (Step 2)
   └── JS: blur no email → IdentityUpdate
   └── Worker: atualiza Identity Graph com email_hash (enrichment)
   └── JS: blur no telefone → IdentityUpdate
   └── Worker: atualiza Identity Graph com phone_hash
   └── JS: blur no nome → IdentityUpdate
   └── Worker: atualiza Identity Graph com first/last_name_hash

3. Visitante vai para pagamento (Step 3)
   └── JS: clique em botão de pagamento → AddPaymentInfo
   └── Worker: dispatch → Meta, GA4, TikTok (AddPaymentInfo)

4. Visitante finaliza compra (Step 4)
   └── Gateway: callback de sucesso → trackPurchaseConfirmed()
   └── JS: dispara Purchase → Worker
   └── Worker: recupera Identity Graph completa (todos os hashes)
   └── Worker: dispatch → Meta Purchase CAPI (v25.0)
   └── Worker: dispatch → GA4 Purchase (MP)
   └── Worker: dispatch → TikTok CompletePayment (v1.3)

5. Visitante abandona checkout (opcional)
   └── JS: visibilitychange + sendBeacon → CheckoutAbandonment
   └── Worker: log apenas no D1 (não dispara para APIs)
```

---

## ⚠️ NOTAS CRÍTICAS

- **Enrichment Progressivo**: Identity Graph é atualizado conforme usuário preenche campos, melhorando Advanced Matching
- **Deduplicação**: Cada evento deve ter `event_id` único (usar `crypto.randomUUID()`)
- **Hashing**: SHA256 deve ser feito no Worker usando `crypto.subtle.digest` (WebCrypto API)
- **Privacy**: Nunca logar email/telefone/nome/CPF em texto claro no console do Worker
- **sendBeacon**: Checkout Abandonment deve usar `navigator.sendBeacon` para garantir envio mesmo se browser encerrar
- **Mesmo-Domain Protocol**: Worker DEVE rodar no mesmo domínio do site para evitar CORS e bloqueios

---

## 🚀 DEPLOY DO ZERO (Resumo)

1. **Criar Worker**: `npx wrangler init cdp-edge-ms`
2. **Configurar D1**: `npx wrangler d1 create cdp-edge-ms-db`
3. **Aplicar Schema**: `npx wrangler d1 execute cdp-edge-ms-db --file=schema.sql`
4. **Configurar Secrets**:
   ```bash
   wrangler secret put META_ACCESS_TOKEN
   wrangler secret put META_PIXEL_ID
   wrangler secret put GA4_ID
   wrangler secret put GA4_API_SECRET
   wrangler secret put TIKTOK_PIXEL_ID
   wrangler secret put TIKTOK_ACCESS_TOKEN
   ```
5. **Deploy**: `npx wrangler deploy`
6. **Configurar Route**: Cloudflare Dashboard → Workers & Pages → Routes → `seusite.com/api/*` → Worker

---

*Este modelo é 100% Cloudflare Native e elimina qualquer dependência de pixels diretos no browser.*
