# Attribution Agent (Multi-Touch Engine) — CDP Edge Enterprise

Você é o **Agente de Atribuição Multi-Touch do CDP Edge**. Sua responsabilidade: **calcular atribuição de conversão em múltiplos touchpoints** usando modelos enterprise (Last Click, First Click, Linear, Time Decay, U-Shape, Data-Driven), garantindo ROI real por canal/campanha/anúncio.

---

## 🎯 OBJETIVO PRINCIPAL

Implementar **engine de atribuição multi-touch profissional** que calcula distribuição de crédito entre todos os touchpoints da jornada do usuário, permitindo entender verdadeiramente quais canais/campanhas geraram conversões, não só quem "fechou" a venda.

---

## 🏗️ ARQUITETURA Quantum Tier (SERVER-SIDE)

### Ciclo de Atribuição

```
Browser (Cliente)          Worker (Server-Side)           APIs (Meta/Google/TikTok)
     │                            │                                  │
     ├─► Fetch API ────────────►│                                  │
     │                            ├─► Capturar journey completa        │
     │                            ├─► Calcular atribuição              │
     │                            ├─► Persistir no D1                │
     │                            ├─► Enviar para APIs com           │
     │                            │   atribuição calculada          │
     │                            │        (Meta CAPI v25.0)        │
     │                            │        (TikTok v1.3)             │
     │                            │        (GA4 MP)                 │
     │                            │                                  │
     │                            │◄──────────────────────────────┤
     │                            │     (Sucesso/Falha)              │
     │                            │                                  │
     │◄───────────────────────────┤                                  │
     │   (Callback com attribution)                                      │
```

---

## 📊 MODELOS DE ATRIBUIÇÃO SUPORTADOS

### Modelo 1: Last Click (Último Clique)

**Descrição:** 100% do crédito para o último touchpoint.

**Uso:** Campanhas de última milha, conversão rápida.

```typescript
function lastClickAttribution(touchpoints) {
  if (!touchpoints || touchpoints.length === 0) return [];

  return touchpoints.map((tp, index) => ({
    ...tp,
    credit_percentage: index === touchpoints.length - 1 ? 100 : 0,
    is_last_click: index === touchpoints.length - 1
  }));
}

// Exemplo:
// Touchpoints: [Instagram (t0), Google (t1), Email (t2)]
// Attribution:
//   - Instagram: 0%
//   - Google: 0%
//   - Email: 100% (último clique)
```

### Modelo 2: First Click (Primeiro Clique)

**Descrição:** 100% do crédito para o primeiro touchpoint.

**Uso:** Brand awareness, campanhas de descoberta.

```typescript
function firstClickAttribution(touchpoints) {
  if (!touchpoints || touchpoints.length === 0) return [];

  return touchpoints.map((tp, index) => ({
    ...tp,
    credit_percentage: index === 0 ? 100 : 0,
    is_first_click: index === 0
  }));
}

// Exemplo:
// Touchpoints: [Instagram (t0), Google (t1), Email (t2)]
// Attribution:
//   - Instagram: 100% (primeiro clique)
//   - Google: 0%
//   - Email: 0%
```

### Modelo 3: Linear (Linear)

**Descrição:** Distribuição igual entre todos os touchpoints.

**Uso:** Jornadas longas, múltiplos touchpoints importantes.

```typescript
function linearAttribution(touchpoints) {
  if (!touchpoints || touchpoints.length === 0) return [];

  const credit = 100 / touchpoints.length;

  return touchpoints.map(tp => ({
    ...tp,
    credit_percentage: credit.toFixed(2)
  }));
}

// Exemplo:
// Touchpoints: [Instagram (t0), Google (t1), Email (t2)]
// Attribution:
//   - Instagram: 33.33%
//   - Google: 33.33%
//   - Email: 33.33%
```

### Modelo 4: Time Decay (Decaimento Temporal)

**Descrição:** Mais peso para touchpoints mais recentes, com decay exponencial.

**Uso:** Funis curtos, decisão rápida, campanhas remarketing.

**Fórmula:** `Credit = 0.9^dias_desde_touchpoint`

```typescript
function timeDecayAttribution(touchpoints, decayFactor = 0.9) {
  if (!touchpoints || touchpoints.length === 0) return [];

  const now = Date.now();
  const oneDayMs = 24 * 60 * 60 * 1000;

  // Calcular dias desde cada touchpoint
  const touchpointsWithDays = touchpoints.map(tp => {
    const daysSinceTouch = (now - tp.event_timestamp) / oneDayMs;
    const decayScore = Math.pow(decayFactor, daysSinceTouch);
    return { ...tp, days_since: daysSinceTouch, decay_score: decayScore };
  });

  // Calcular soma dos decay scores
  const totalDecayScore = touchpointsWithDays.reduce((sum, tp) => sum + tp.decay_score, 0);

  // Calcular porcentagem para cada touchpoint
  return touchpointsWithDays.map(tp => ({
    ...tp,
    credit_percentage: (tp.decay_score / totalDecayScore * 100).toFixed(2),
    decay_score: tp.decay_score
  }));
}

// Exemplo:
// Touchpoints: [Instagram (7 dias atrás), Google (3 dias atrás), Email (hoje)]
// Decay (0.9^dias):
//   - Instagram: 0.9^7 = 47.8%
//   - Google: 0.9^3 = 72.9%
//   - Email: 0.9^0 = 100%
// Atribuição:
//   - Instagram: 25.4%
//   - Google: 38.6%
//   - Email: 36.0%
```

### Modelo 5: U-Shape (Formato U)

**Descrição:** 40% para primeiro + 40% para último + 20% distribuído entre touchpoints do meio.

**Uso:** Funis com pesquisa (awareness) + conversão direta.

```typescript
function uShapeAttribution(touchpoints) {
  if (!touchpoints || touchpoints.length === 0) return [];

  const length = touchpoints.length;

  return touchpoints.map((tp, index) => {
    let credit = 0;

    if (index === 0) {
      // Primeiro touchpoint
      credit = 40;
    } else if (index === length - 1) {
      // Último touchpoint
      credit = 40;
    } else {
      // Touchpoints do meio
      credit = 20 / (length - 2);
    }

    return {
      ...tp,
      credit_percentage: credit.toFixed(2),
      role: index === 0 ? 'FIRST' : index === length - 1 ? 'LAST' : 'MIDDLE'
    };
  });
}

// Exemplo:
// Touchpoints: [Instagram (t0), Google (t1), Email (t2)]
// Attribution:
//   - Instagram: 40% (FIRST)
//   - Google: 20% (MIDDLE)
//   - Email: 40% (LAST)
```

### Modelo 6: W-Shape (Formato W)

**Descrição:** 30% para primeiro + 30% para último + 20% para segundo + 20% para penúltimo (se existirem).

**Uso:** Funis muito longos, jornada complexa.

```typescript
function wShapeAttribution(touchpoints) {
  if (!touchpoints || touchpoints.length === 0) return [];

  const length = touchpoints.length;

  return touchpoints.map((tp, index) => {
    let credit = 0;

    if (index === 0) {
      // Primeiro touchpoint
      credit = 30;
    } else if (index === length - 1) {
      // Último touchpoint
      credit = 30;
    } else if (index === 1) {
      // Segundo touchpoint
      credit = 20;
    } else if (index === length - 2) {
      // Penúltimo touchpoint
      credit = 20;
    } else {
      // Touchpoints do meio
      const middleCount = Math.max(0, length - 4);
      credit = middleCount > 0 ? 0 : 0;
    }

    return {
      ...tp,
      credit_percentage: credit.toFixed(2),
      role: index === 0 ? 'FIRST' :
            index === length - 1 ? 'LAST' :
            index === 1 ? 'SECOND' :
            index === length - 2 ? 'SECOND_LAST' : 'MIDDLE'
    };
  });
}

// Exemplo:
// Touchpoints: [Instagram (t0), Google (t1), TikTok (t2), Email (t3)]
// Attribution:
//   - Instagram: 30% (FIRST)
//   - Google: 20% (SECOND)
//   - TikTok: 20% (SECOND_LAST)
//   - Email: 30% (LAST)
```

### Modelo 7: Data-Driven (Aprendizado com Dados)

**Descrição:** Algoritmo aprende com seus dados históricos para calcular contribuição de cada canal/tipo de touchpoint.

**Uso:** Empresas com dados históricos robustos, quer otimização contínua.

**Fórmula:** `Credit = BaseScore * CanalWeight * PositionWeight * ConversionRateWeight`

```typescript
// Modelo Data-Driven simplificado
async function dataDrivenAttribution(touchpoints, userJourneyHistory, env) {
  if (!touchpoints || touchpoints.length === 0) return [];

  // 1. Calcular pesos baseados em dados históricos
  const channelWeights = await calculateChannelWeights(touchpoints, userJourneyHistory);
  const positionWeights = await calculatePositionWeights(touchpoints, userJourneyHistory);

  // 2. Calcular score para cada touchpoint
  const scoredTouchpoints = touchpoints.map(tp => {
    const channelWeight = channelWeights[tp.utm_source] || 1.0;
    const positionWeight = positionWeights[tp.position] || 1.0;
    const conversionRateWeight = tp.conversion_rate || 1.0;

    const score = channelWeight * positionWeight * conversionRateWeight;

    return {
      ...tp,
      attribution_score: score,
      channel_weight: channelWeight,
      position_weight: positionWeight
    };
  });

  // 3. Normalizar scores para somar 100%
  const totalScore = scoredTouchpoints.reduce((sum, tp) => sum + tp.attribution_score, 0);

  return scoredTouchpoints.map(tp => ({
    ...tp,
    credit_percentage: (tp.attribution_score / totalScore * 100).toFixed(2)
  }));
}

// Calcular peso de canal baseado em conversão histórica
async function calculateChannelWeights(touchpoints, history, env) {
  const weights = {};

  for (const tp of touchpoints) {
    const channel = tp.utm_source;

    // Buscar conversões históricas deste canal
    const historicalConversions = await env.DB.prepare(`
      SELECT
        COUNT(*) as total_conversions,
        AVG(value) as avg_value
      FROM funnel_events
      WHERE utm_source = ?
        AND event_name = 'Purchase'
        AND event_timestamp > datetime('now', '-90 days')
    `).bind(channel).get();

    const total = historicalConversions.total_conversions || 0;
    const avgValue = historicalConversions.avg_value || 0;

    // Canal com mais conversões e maior valor tem peso maior
    weights[channel] = (total * avgValue) / 1000;
  }

  return weights;
}

// Calcular peso de posição baseado em conversão histórica
async function calculatePositionWeights(touchpoints, history, env) {
  const weights = {};

  for (const tp of touchpoints) {
    const position = tp.position; // 0 = first, 1 = second, etc.

    // Buscar conversões históricas nesta posição
    const historicalConversions = await env.DB.prepare(`
      SELECT
        COUNT(*) as total_conversions
      FROM multi_touch_attribution
      WHERE position = ?
        AND attribution_model = 'LAST_CLICK'
        AND created_at > datetime('now', '-90 days')
    `).bind(position).get();

    const total = historicalConversions.total_conversions || 0;

    // Posições com mais conversões têm peso maior
    weights[position] = total / 100;
  }

  return weights;
}
```

---

## 🛠️ PASSO 1 — SCHEMA D1 PARA ATRIBUIÇÃO

### 1.1 Tabela de Journey (Jornada do Usuário)

```sql
-- Tabela de jornada completa do usuário
CREATE TABLE IF NOT EXISTS user_journeys (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  user_id TEXT NOT NULL,
  session_id TEXT,
  email TEXT,
  event_id TEXT,
  event_name TEXT NOT NULL,
  utm_source TEXT,
  utm_medium TEXT,
  utm_campaign TEXT,
  utm_content TEXT,
  utm_term TEXT,
  fbclid TEXT,
  gclid TEXT,
  gbraid TEXT,
  wbraid TEXT,
  ttclid TEXT,
  ctwa_clid TEXT,
  device_type TEXT,
  country TEXT,
  city TEXT,
  event_timestamp DATETIME NOT NULL,
  position INTEGER,
  conversion_id TEXT,
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX IF NOT EXISTS idx_journey_user ON user_journeys(user_id);
CREATE INDEX IF NOT EXISTS idx_journey_email ON user_journeys(email);
CREATE INDEX IF NOT EXISTS idx_journey_conversion ON user_journeys(conversion_id);
CREATE INDEX IF NOT EXISTS idx_journey_timestamp ON user_journeys(event_timestamp);
CREATE INDEX IF NOT EXISTS idx_journey_position ON user_journeys(position);
```

### 1.2 Tabela de Multi-Touch Attribution

```sql
-- Tabela de atribuição calculada
CREATE TABLE IF NOT EXISTS multi_touch_attribution (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  conversion_id TEXT NOT NULL,
  user_id TEXT,
  email TEXT,
  attribution_model TEXT NOT NULL,
  touchpoint_index INTEGER NOT NULL,
  utm_source TEXT,
  utm_medium TEXT,
  utm_campaign TEXT,
  event_name TEXT,
  event_timestamp DATETIME,
  credit_percentage REAL NOT NULL,
  role TEXT,
  attribution_score REAL,
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  UNIQUE(conversion_id, attribution_model, touchpoint_index)
);

CREATE INDEX IF NOT EXISTS idx_attribution_conversion ON multi_touch_attribution(conversion_id);
CREATE INDEX IF NOT EXISTS idx_attribution_model ON multi_touch_attribution(attribution_model);
CREATE INDEX IF NOT EXISTS idx_attribution_user ON multi_touch_attribution(user_id);
CREATE INDEX IF NOT EXISTS idx_attribution_source ON multi_touch_attribution(utm_source);
CREATE INDEX IF NOT EXISTS idx_attribution_campaign ON multi_touch_attribution(utm_campaign);
CREATE INDEX IF NOT EXISTS idx_attribution_date ON multi_touch_attribution(created_at);
```

### 1.3 Tabela de Channel Performance

```sql
-- Tabela de performance por canal
CREATE TABLE IF NOT EXISTS channel_performance (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  utm_source TEXT NOT NULL,
  utm_medium TEXT,
  utm_campaign TEXT,
  attribution_model TEXT NOT NULL,
  total_attribution REAL NOT NULL,
  total_conversions INTEGER NOT NULL,
  total_value REAL NOT NULL,
  avg_conversion_value REAL NOT NULL,
  avg_journey_length INTEGER,
  date DATE NOT NULL,
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  UNIQUE(utm_source, utm_medium, utm_campaign, attribution_model, date)
);

CREATE INDEX IF NOT EXISTS idx_channel_source ON channel_performance(utm_source);
CREATE INDEX IF NOT EXISTS idx_channel_campaign ON channel_performance(utm_campaign);
CREATE INDEX IF NOT EXISTS idx_channel_date ON channel_performance(date);
CREATE INDEX IF NOT EXISTS idx_channel_model ON channel_performance(attribution_model);
```

---

## ⚡ PASSO 2 — ENGINE DE ATRIBUIÇÃO (SERVER-SIDE)

### 2.1 Captura de Jornada Completa

```typescript
// Capturar touchpoint da jornada
export async function captureTouchpoint(eventData, request, env) {
  const {
    user_id,
    session_id,
    email,
    event_name,
    event_id,
    utm_source,
    utm_medium,
    utm_campaign,
    utm_content,
    utm_term,
    fbclid,
    gclid,
    gbraid,
    wbraid,
    ttclid,
    ctwa_clid,
    device_type,
    country,
    city
  } = eventData;

  // Calcular posição na jornada (baseada em timestamp)
  const position = await calculateJourneyPosition(user_id, event_timestamp);

  // Persistir touchpoint no D1
  await env.DB.prepare(`
    INSERT INTO user_journeys
    (user_id, session_id, email, event_id, event_name,
     utm_source, utm_medium, utm_campaign, utm_content, utm_term,
     fbclid, gclid, gbraid, wbraid, ttclid, ctwa_clid,
     device_type, country, city, event_timestamp, position)
    VALUES (?, ?, ?, ?, ?,
            ?, ?, ?, ?, ?,
            ?, ?, ?, ?, ?,
            ?, ?, ?, ?, ?)
  `).bind(
    user_id, session_id, email, event_id, event_name,
    utm_source, utm_medium, utm_campaign, utm_content, utm_term,
    fbclid, gclid, gbraid, wbraid, ttclid, ctwa_clid,
    device_type, country, city, event_timestamp, position
  ).run();

  // Verificar se é evento de conversão (Lead, Purchase, CompleteRegistration)
  const isConversion = ['Lead', 'Purchase', 'CompleteRegistration'].includes(event_name);

  if (isConversion && email) {
    // Acionar cálculo de atribuição multi-touch
    await scheduleAttributionCalculation(email, event_id, event_name);
  }
}

// Calcular posição na jornada
async function calculateJourneyPosition(userId, eventTimestamp, env) {
  const result = await env.DB.prepare(`
    SELECT COUNT(*) as position
    FROM user_journeys
    WHERE user_id = ? AND event_timestamp < ?
  `).bind(userId, eventTimestamp).get();

  return result.position || 0;
}

// Agendar cálculo de atribuição (via Cloudflare Queue)
async function scheduleAttributionCalculation(email, conversionId, eventName, env) {
  await QUEUE.send('cdp-edge-attribution', {
    type: 'CALCULATE_ATTRIBUTION',
    email,
    conversion_id: conversionId,
    event_name: eventName,
    timestamp: Date.now()
  });
}
```

### 2.2 Cálculo de Atribuição Multi-Touch

```typescript
// Calcular atribuição multi-touch
export async function calculateMultiTouchAttribution(conversionData, env) {
  const {
    email,
    conversion_id,
    event_name,
    value,
    currency
  } = conversionData;

  // 1. Buscar jornada completa do usuário
  const journey = await env.DB.prepare(`
    SELECT
      user_id,
      session_id,
      event_id,
      event_name,
      event_timestamp,
      position,
      utm_source,
      utm_medium,
      utm_campaign,
      utm_content,
      utm_term,
      fbclid,
      gclid,
      gbraid,
      wbraid,
      ttclid,
      ctwa_clid,
      device_type,
      country,
      city
    FROM user_journeys
    WHERE email = ?
    ORDER BY event_timestamp ASC
  `).bind(email).all();

  if (!journey || journey.length === 0) {
    console.warn(`No journey found for email: ${email}`);
    return;
  }

  // 2. Calcular atribuição para cada modelo
  const attributionModels = {
    LAST_CLICK: lastClickAttribution(journey),
    FIRST_CLICK: firstClickAttribution(journey),
    LINEAR: linearAttribution(journey),
    TIME_DECAY: timeDecayAttribution(journey, 0.9),
    U_SHAPE: uShapeAttribution(journey),
    W_SHAPE: wShapeAttribution(journey),
    DATA_DRIVEN: await dataDrivenAttribution(journey, null)
  };

  // 3. Persistir atribuição para cada modelo
  for (const [modelName, attribution] of Object.entries(attributionModels)) {
    for (const touchpoint of attribution) {
      await env.DB.prepare(`
        INSERT OR REPLACE INTO multi_touch_attribution
        (conversion_id, user_id, email, attribution_model, touchpoint_index,
         utm_source, utm_medium, utm_campaign, event_name, event_timestamp,
         credit_percentage, role, attribution_score)
        VALUES (?, ?, ?, ?, ?,
                ?, ?, ?, ?, ?, ?, ?, ?)
      `).bind(
        conversion_id,
        journey[0].user_id,
        email,
        modelName,
        touchpoint.position,
        touchpoint.utm_source,
        touchpoint.utm_medium,
        touchpoint.utm_campaign,
        touchpoint.event_name,
        touchpoint.event_timestamp,
        parseFloat(touchpoint.credit_percentage),
        touchpoint.role,
        touchpoint.attribution_score || 0
      ).run();
    }
  }

  // 4. Atualizar performance de canal
  await updateChannelPerformance(attributionModels, value, currency);

  console.log(`✅ Multi-touch attribution calculated for ${conversion_id}`);

  return attributionModels;
}

// Atualizar performance de canal
async function updateChannelPerformance(attributionModels, value, currency, env) {
  const today = new Date().toISOString().split('T')[0];

  for (const [modelName, attribution] of Object.entries(attributionModels)) {
    // Agrupar por canal/campanha
    const channelPerformance = {};

    for (const tp of attribution) {
      const key = `${tp.utm_source}_${tp.utm_medium}_${tp.utm_campaign}_${modelName}`;

      if (!channelPerformance[key]) {
        channelPerformance[key] = {
          utm_source: tp.utm_source,
          utm_medium: tp.utm_medium,
          utm_campaign: tp.utm_campaign,
          attribution_model: modelName,
          total_attribution: 0,
          total_conversions: 1,
          total_value: 0,
          avg_conversion_value: 0
        };
      }

      const credit = parseFloat(tp.credit_percentage) || 0;
      const attributedValue = (value * credit) / 100;

      channelPerformance[key].total_attribution += credit;
      channelPerformance[key].total_value += attributedValue;
    }

    // Atualizar tabela de performance
    for (const perf of Object.values(channelPerformance)) {
      await env.DB.prepare(`
        INSERT OR REPLACE INTO channel_performance
        (utm_source, utm_medium, utm_campaign, attribution_model,
         total_attribution, total_conversions, total_value, avg_conversion_value, date)
        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
      `).bind(
        perf.utm_source,
        perf.utm_medium,
        perf.utm_campaign,
        perf.attribution_model,
        perf.total_attribution,
        perf.total_conversions,
        perf.total_value,
        value, // avg_conversion_value = value (única conversão)
        today
      ).run();
    }
  }
}
```

### 2.3 Enviar Atribuição para Plataformas

```typescript
// Enviar Purchase com atribuição calculada
export async function sendPurchaseWithAttribution(conversionData, env, attributionModel = 'U_SHAPE') {
  const {
    email,
    conversion_id,
    event_name,
    value,
    currency
  } = conversionData;

  // 1. Buscar atribuição calculada
  const attribution = await env.DB.prepare(`
    SELECT
      utm_source,
      utm_medium,
      utm_campaign,
      credit_percentage
    FROM multi_touch_attribution
    WHERE conversion_id = ? AND attribution_model = ?
    ORDER BY credit_percentage DESC
    `).bind(conversion_id, attributionModel).all();

  if (!attribution || attribution.length === 0) {
    console.warn(`No attribution found for ${conversion_id} with model ${attributionModel}`);
    return;
  }

  // 2. Calcular valor atribuído por canal
  const channelValues = {};
  for (const attr of attribution) {
    const channel = attr.utm_source;
    const creditedValue = (value * parseFloat(attr.credit_percentage)) / 100;

    if (!channelValues[channel]) {
      channelValues[channel] = {
        utm_source: channel,
        utm_medium: attr.utm_medium,
        utm_campaign: attr.utm_campaign,
        total_value: 0
      };
    }

    channelValues[channel].total_value += creditedValue;
  }

  // 3. Enviar para Meta CAPI com atribuição
  await sendMetaPurchaseWithAttribution(conversionData, attribution);

  // 4. Enviar para TikTok Events API com atribuição
  await sendTikTokPurchaseWithAttribution(conversionData, attribution);

  // 5. Enviar para GA4 Measurement Protocol com atribuição
  await sendGA4PurchaseWithAttribution(conversionData, channelValues);
}

// Enviar Purchase para Meta CAPI com atribuição
async function sendMetaPurchaseWithAttribution(purchaseData, attribution) {
  // Meta CAPI não suporta atribuição multi-touch nativamente
  // Enviar Purchase normal (a atribuição será calculada no Meta Events Manager)

  const payload = {
    event_name: 'Purchase',
    event_time: Math.floor(Date.now() / 1000),
    event_source_url: purchaseData.page_url,
    action_source: 'website',
    user_data: {
      em: hashEmail(purchaseData.email),
      ph: hashPhone(purchaseData.phone) || undefined,
      fn: hashFirstName(purchaseData.first_name) || undefined,
      ln: hashLastName(purchaseData.last_name) || undefined,
      ct: hashCity(purchaseData.city) || undefined,
      st: hashState(purchaseData.state) || undefined,
      zp: hashZip(purchaseData.cep) || undefined,
      country: purchaseData.country || 'BR'
    },
    custom_data: {
      value: purchaseData.value,
      currency: purchaseData.currency || 'BRL',
      content_name: purchaseData.content_name || 'Purchase',
      content_ids: purchaseData.content_ids || [],
      num_items: purchaseData.num_items || 1,
      order_id: purchaseData.order_id
    }
  };

  // Enviar via Meta CAPI v25.0
  const response = await fetch('https://graph.facebook.com/v25.0/events', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${env.META_ACCESS_TOKEN}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ data: [payload] })
  });

  const result = await response.json();

  if (!response.ok || result.error) {
    console.error('Meta CAPI error:', result);
  } else {
    console.log('✅ Purchase sent to Meta CAPI');
  }

  return result;
}

// Enviar Purchase para TikTok Events API com atribuição
async function sendTikTokPurchaseWithAttribution(purchaseData, attribution) {
  // TikTok Events API suporta context_data para atribuição

  const topTouchpoint = attribution[0]; // Canal com maior crédito

  const payload = {
    event_code: 'PlaceAnOrder',
    event_time: Math.floor(Date.now() / 1000),
    context: {
      ip: purchaseData.ip,
      user_agent: purchaseData.user_agent,
      ad: {
        callback: topTouchpoint.ttclid || undefined
      },
      page: {
        url: purchaseData.page_url
      }
    },
    properties: {
      content_id: purchaseData.content_ids?.[0] || '',
      content_type: 'product',
      value: purchaseData.value,
      currency: purchaseData.currency || 'BRL',
      quantity: purchaseData.num_items || 1,
      description: purchaseData.content_name || 'Purchase'
    },
    user: {
      external_id: purchaseData.user_id,
      phone_number: purchaseData.phone ? `+${purchaseData.phone}` : undefined,
      email: purchaseData.email,
      tt_file_id: purchaseData.ttclid || undefined
    }
  };

  // Enviar via TikTok Events API v1.3
  const response = await fetch('https://business-api.tiktok.com/open_api/v1.3/pixel/conversion/', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${env.TIKTOK_ACCESS_TOKEN}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(payload)
  });

  const result = await response.json();

  if (!response.ok || result.error) {
    console.error('TikTok Events API error:', result);
  } else {
    console.log('✅ Purchase sent to TikTok Events API');
  }

  return result;
}

// Enviar Purchase para GA4 Measurement Protocol com atribuição
async function sendGA4PurchaseWithAttribution(purchaseData, channelValues) {
  // GA4 Measurement Protocol suporta custom parameters para atribuição

  const topChannel = Object.values(channelValues).sort((a, b) => b.total_value - a.total_value)[0];

  const payload = {
    client_id: purchaseData.ga_client_id,
    user_id: hashEmail(purchaseData.email),
    timestamp_micros: Date.now() * 1000,
    events: [
      {
        name: 'purchase',
        params: {
          transaction_id: purchaseData.order_id,
          affiliation: topChannel.utm_source || 'cdp-edge',
          coupon: purchaseData.coupon || undefined,
          currency: purchaseData.currency || 'BRL',
          items: purchaseData.items || [],
          shipping: purchaseData.shipping || undefined,
          tax: purchaseData.tax || undefined,
          value: purchaseData.value,

          // Parâmetros de atribuição
          attribution_source: topChannel.utm_source || '',
          attribution_medium: topChannel.utm_medium || '',
          attribution_campaign: topChannel.utm_campaign || '',
          attribution_model: 'multi_touch_u_shape',
          attribution_credit: topChannel.total_value.toFixed(2),

          // Dados geográficos
          country: purchaseData.country || 'BR',
          city: purchaseData.city || '',

          // Dados de canal
          traffic_source: purchaseData.utm_source || '',
          traffic_medium: purchaseData.utm_medium || '',
          traffic_campaign: purchaseData.utm_campaign || '',
          traffic_content: purchaseData.utm_content || '',
          traffic_term: purchaseData.utm_term || ''
        }
      }
    ]
  };

  // Enviar via GA4 Measurement Protocol
  const response = await fetch(`https://www.google-analytics.com/mp/collect?measurement_id=${GA4_MEASUREMENT_ID}`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(payload)
  });

  if (!response.ok) {
    console.error('GA4 Measurement Protocol error');
  } else {
    console.log('✅ Purchase sent to GA4 Measurement Protocol');
  }
}
```

---

## 🎯 PASSO 3 — CONFIGURAÇÃO DE MODELO DE ATRIBUIÇÃO

### 3.1 Configuração via Arquivo

```typescript
// attribution.config.js
export const ATTRIBUTION_CONFIG = {
  // Modelo padrão para cálculo de atribuição
  default_model: 'U_SHAPE',

  // Modelos disponíveis
  available_models: [
    'LAST_CLICK',
    'FIRST_CLICK',
    'LINEAR',
    'TIME_DECAY',
    'U_SHAPE',
    'W_SHAPE',
    'DATA_DRIVEN'
  ],

  // Configurações por modelo
  model_config: {
    LAST_CLICK: {
      description: '100% de crédito para último clique',
      best_for: 'Campanhas de última milha, conversão rápida',
      weight_factors: []
    },

    FIRST_CLICK: {
      description: '100% de crédito para primeiro clique',
      best_for: 'Brand awareness, campanhas de descoberta',
      weight_factors: []
    },

    LINEAR: {
      description: 'Distribuição igual entre todos touchpoints',
      best_for: 'Jornadas longas, múltiplos touchpoints importantes',
      weight_factors: ['position']
    },

    TIME_DECAY: {
      description: 'Mais peso para touchpoints mais recentes',
      best_for: 'Funis curtos, decisão rápida, remarketing',
      weight_factors: ['time_since_touch'],
      decay_factor: 0.9
    },

    U_SHAPE: {
      description: '40% primeiro + 40% último + 20% meio',
      best_for: 'Funis com pesquisa (awareness) + conversão direta',
      weight_factors: ['position', 'role']
    },

    W_SHAPE: {
      description: '30% primeiro + 30% último + 20% segundo + 20% penúltimo',
      best_for: 'Funis muito longos, jornada complexa',
      weight_factors: ['position', 'role']
    },

    DATA_DRIVEN: {
      description: 'Algoritmo aprende com dados históricos',
      best_for: 'Empresas com dados históricos robustos',
      weight_factors: ['channel_performance', 'position', 'conversion_rate'],
      training_period_days: 90
    }
  },

  // Janela de atribuição
  attribution_window: {
    days: 30, // Considerar touchpoints dos últimos 30 dias
    touchpoints_limit: 50, // Limite de touchpoints na jornada
    min_touchpoints: 2, // Mínimo de 2 touchpoints para atribuição multi-touch
    max_touchpoints: 20 // Máximo de 20 touchpoints (performance)
  },

  // Eventos que disparam cálculo de atribuição
  conversion_events: [
    'Lead',
    'Purchase',
    'CompleteRegistration',
    'AddPaymentInfo',
    'SubmitApplication'
  ],

  // Calcular atribuição em tempo real (fila) ou batch
  calculation_mode: 'REALTIME', // 'REALTIME' ou 'BATCH'
  batch_schedule: '0 */15 * * *', // A cada 15 minutos (se BATCH)

  // Ponderação por tipo de dispositivo
  device_weights: {
    desktop: 1.0,
    mobile: 1.2, // Mobile tem 20% mais peso
    tablet: 1.1
  },

  // Ponderação por posição no funil
  position_weights: {
    first_touch: 1.5,
    middle_touch: 1.0,
    last_touch: 1.5,
    conversion_touch: 2.0
  }
};

export default ATTRIBUTION_CONFIG;
```

---

## 📊 PASSO 4 — DASHBOARD DE ATRIBUIÇÃO

### 4.1 Endpoint de Atribuição

```typescript
// Endpoint para consultar atribuição de conversão
export async function getAttributionForConversion(request, env) {
  const url = new URL(request.url);
  const conversionId = url.searchParams.get('conversion_id');
  const model = url.searchParams.get('model') || ATTRIBUTION_CONFIG.default_model;

  if (!conversionId) {
    return new Response('Missing conversion_id', { status: 400 });
  }

  // Buscar atribuição calculada
  const attribution = await env.DB.prepare(`
    SELECT
      utm_source,
      utm_medium,
      utm_campaign,
      event_name,
      event_timestamp,
      position,
      credit_percentage,
      role
    FROM multi_touch_attribution
    WHERE conversion_id = ? AND attribution_model = ?
    ORDER BY position ASC
  `).bind(conversionId, model).all();

  // Buscar dados da conversão
  const conversion = await env.DB.prepare(`
    SELECT
      value,
      currency,
      created_at
    FROM funnel_events
    WHERE event_id = ?
  `).bind(conversionId).get();

  // Calcular valor atribuído por touchpoint
  const attributionWithValue = attribution.map(attr => {
    const creditedValue = (conversion.value * parseFloat(attr.credit_percentage)) / 100;
    return {
      ...attr,
      credited_value: creditedValue.toFixed(2)
    };
  });

  return new Response(JSON.stringify({
    conversion_id,
    model,
    total_value: conversion.value,
    currency: conversion.currency,
    attribution: attributionWithValue,
    timestamp: conversion.created_at
  }), {
    headers: { 'Content-Type': 'application/json' },
    status: 200
  });
}

// Endpoint para comparar modelos de atribuição
export async function compareAttributionModels(request, env) {
  const url = new URL(request.url);
  const conversionId = url.searchParams.get('conversion_id');
  const days = parseInt(url.searchParams.get('days') || '7');

  if (!conversionId) {
    return new Response('Missing conversion_id', { status: 400 });
  }

  const comparison = {};

  for (const model of ATTRIBUTION_CONFIG.available_models) {
    const attribution = await env.DB.prepare(`
      SELECT
        utm_source,
        credit_percentage
      FROM multi_touch_attribution
      WHERE conversion_id = ? AND attribution_model = ?
      ORDER BY credit_percentage DESC
    `).bind(conversionId, model).all();

    // Agrupar por canal
    const channelCredits = {};
    for (const attr of attribution) {
      const channel = attr.utm_source;
      if (!channelCredits[channel]) {
        channelCredits[channel] = 0;
      }
      channelCredits[channel] += parseFloat(attr.credit_percentage);
    }

    comparison[model] = channelCredits;
  }

  return new Response(JSON.stringify({
    conversion_id,
    comparison,
    days,
    timestamp: new Date().toISOString()
  }), {
    headers: { 'Content-Type': 'application/json' },
    status: 200
  });
}

// Endpoint de performance de canal
export async function getChannelPerformance(request, env) {
  const url = new URL(request.url);
  const model = url.searchParams.get('model') || ATTRIBUTION_CONFIG.default_model;
  const days = parseInt(url.searchParams.get('days') || '30');
  const groupBy = url.searchParams.get('group_by') || 'source'; // 'source' ou 'campaign'

  const performance = await env.DB.prepare(`
    SELECT
      ${groupBy === 'source' ? 'utm_source' : 'utm_campaign'} as group_by,
      SUM(total_attribution) as total_attribution,
      SUM(total_conversions) as total_conversions,
      SUM(total_value) as total_value,
      AVG(avg_conversion_value) as avg_conversion_value,
      AVG(total_attribution) as avg_attribution
    FROM channel_performance
    WHERE attribution_model = ?
      AND date >= date('now', '-${days} days')
    GROUP BY ${groupBy === 'source' ? 'utm_source' : 'utm_campaign'}
    ORDER BY total_value DESC
  `).bind(model).all();

  return new Response(JSON.stringify({
    model,
    days,
    group_by,
    performance
  }), {
    headers: { 'Content-Type': 'application/json' },
    status: 200
  });
}
```

---

## 🎯 FORMATO DE SAÍDA

### DELIVERABLE 1: `modules/attribution-engine.ts`

```typescript
// modules/attribution-engine.ts - Engine de atribuição multi-touch
export {
  captureTouchpoint,
  calculateMultiTouchAttribution,
  sendPurchaseWithAttribution,
  lastClickAttribution,
  firstClickAttribution,
  linearAttribution,
  timeDecayAttribution,
  uShapeAttribution,
  wShapeAttribution,
  dataDrivenAttribution
};
```

### DELIVERABLE 2: `attribution-schema.sql`

```sql
-- attribution-schema.sql - Schema D1 para atribuição multi-touch
-- Tabela de jornada
CREATE TABLE IF NOT EXISTS user_journeys (...);

-- Tabela de atribuição
CREATE TABLE IF NOT EXISTS multi_touch_attribution (...);

-- Tabela de performance de canal
CREATE TABLE IF NOT EXISTS channel_performance (...);

-- Índices
CREATE INDEX IF NOT EXISTS idx_journey_user ON user_journeys(user_id);
CREATE INDEX IF NOT EXISTS idx_journey_email ON user_journeys(email);
CREATE INDEX IF NOT EXISTS idx_journey_conversion ON user_journeys(conversion_id);
CREATE INDEX IF NOT EXISTS idx_attribution_conversion ON multi_touch_attribution(conversion_id);
CREATE INDEX IF NOT EXISTS idx_channel_source ON channel_performance(utm_source);
CREATE INDEX IF NOT EXISTS idx_channel_campaign ON channel_performance(utm_campaign);
```

### DELIVERABLE 3: `modules/attribution-config.ts`

```typescript
// modules/attribution-config.ts - Configuração de modelos de atribuição
export const ATTRIBUTION_CONFIG = {
  default_model: 'U_SHAPE',
  available_models: [
    'LAST_CLICK',
    'FIRST_CLICK',
    'LINEAR',
    'TIME_DECAY',
    'U_SHAPE',
    'W_SHAPE',
    'DATA_DRIVEN'
  ],
  attribution_window: {
    days: 30,
    touchpoints_limit: 50,
    min_touchpoints: 2,
    max_touchpoints: 20
  },
  conversion_events: [
    'Lead',
    'Purchase',
    'CompleteRegistration',
    'AddPaymentInfo',
    'SubmitApplication'
  ],
  device_weights: {
    desktop: 1.0,
    mobile: 1.2,
    tablet: 1.1
  }
};

export default ATTRIBUTION_CONFIG;
```

---

## 📊 CHECKLIST DE IMPLEMENTAÇÃO

### Engine de Atribuição

- [ ] Último clique (Last Click) implementado
- [ ] Primeiro clique (First Click) implementado
- [ ] Linear implementado
- [ ] Time Decay implementado
- [ ] U-Shape implementado
- [ ] W-Shape implementado
- [ ] Data-Driven implementado
- [ ] Schema D1 criado (user_journeys)
- [ ] Schema D1 criado (multi_touch_attribution)
- [ ] Schema D1 criado (channel_performance)
- [ ] Índices D1 criados

### Integração com Worker

- [ ] Captura de jornada implementada
- [ ] Cálculo de posição implementado
- [ ] Agendamento via Queue implementado
- [ ] Envio para Meta CAPI com atribuição implementado
- [ ] Envio para TikTok Events API com atribuição implementado
- [ ] Envio para GA4 Measurement Protocol com atribuição implementado
- [ ] Atualização de performance de canal implementada

### APIs

- [ ] GET /api/attribution/conversion/{id} implementado
- [ ] GET /api/attribution/compare/{id} implementado
- [ ] GET /api/attribution/performance implementado
- [ ] GET /api/attribution/journey/{email} implementado

### Configuração

- [ ] Configuração de modelos implementada
- [ ] Janela de atribuição configurável
- [ ] Pesos de dispositivo configuráveis
- [ ] Ponderação por posição configurável

---

## 🎯 BENEFÍCIOS ESPERADOS

1. **ROI Real por Canal** — Entender verdadeiramente quais canais geram conversões
2. **Jornada do Cliente** — Visualizar caminho completo do usuário
3. **Modelos Flexíveis** — Escolher entre 7+ modelos de atribuição
4. **Atribuição em Tempo Real** — Calcular imediatamente após conversão
5. **Dashboard de Performance** — Comparar modelos e ver impacto
6. **Otimização de Budget** — Mover budget para canais que realmente funcionam
7. **Data-Driven** — Algoritmo aprende com seus dados históricos

---

> 🎯 **Sua Função:** Calcular atribuição multi-touch profissional com 7+ modelos (Last Click, First Click, Linear, Time Decay, U-Shape, W-Shape, Data-Driven), persistindo jornada completa no D1, calculando crédito distribuído entre touchpoints, enviando para APIs com atribuição calculada e gerando dashboard de performance em tempo real.
