cdp-edge 1.23.2 → 1.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +82 -21
  2. package/bin/cdp-edge.js +10 -1
  3. package/contracts/agent-versions.json +42 -41
  4. package/contracts/types.ts +81 -0
  5. package/dist/commands/install.js +6 -1
  6. package/dist/commands/server.js +4 -4
  7. package/docs/whatsapp-ctwa.md +3 -2
  8. package/extracted-skill/tracking-events-generator/agents/database-agent.md +5 -4
  9. package/extracted-skill/tracking-events-generator/agents/fraud-detection-agent.md +0 -1
  10. package/extracted-skill/tracking-events-generator/agents/linkedin-agent.md +1 -1
  11. package/extracted-skill/tracking-events-generator/agents/ltv-predictor-agent.md +4 -4
  12. package/extracted-skill/tracking-events-generator/agents/ml-clustering-agent.md +81 -70
  13. package/extracted-skill/tracking-events-generator/agents/page-analyzer.md +6 -2
  14. package/extracted-skill/tracking-events-generator/cdpTrack.js +7 -0
  15. package/extracted-skill/tracking-events-generator/models/lancamento-imobiliario.md +344 -0
  16. package/extracted-skill/tracking-events-generator/route-intent-capture.js +222 -0
  17. package/package.json +9 -5
  18. package/server-edge-tracker/INSTALAR.md +5 -5
  19. package/server-edge-tracker/{index.js → index.ts} +186 -72
  20. package/server-edge-tracker/modules/{db.js → db.ts} +180 -69
  21. package/server-edge-tracker/modules/dispatch/{ga4.js → ga4.ts} +12 -10
  22. package/server-edge-tracker/modules/dispatch/meta.ts +138 -0
  23. package/server-edge-tracker/modules/dispatch/{platforms.js → platforms.ts} +58 -56
  24. package/server-edge-tracker/modules/dispatch/{tiktok.js → tiktok.ts} +22 -20
  25. package/server-edge-tracker/modules/dispatch/{whatsapp.js → whatsapp.ts} +59 -25
  26. package/server-edge-tracker/modules/{intelligence.js → intelligence.ts} +175 -60
  27. package/server-edge-tracker/modules/ml/{bidding.js → bidding.ts} +37 -35
  28. package/server-edge-tracker/modules/ml/{fraud.js → fraud.ts} +49 -56
  29. package/server-edge-tracker/modules/ml/{logistic.js → logistic.ts} +44 -19
  30. package/server-edge-tracker/modules/ml/{ltv.js → ltv.ts} +179 -83
  31. package/server-edge-tracker/modules/ml/{matchquality.js → matchquality.ts} +70 -26
  32. package/server-edge-tracker/modules/ml/segmentation.ts +407 -0
  33. package/server-edge-tracker/modules/utils.ts +186 -0
  34. package/server-edge-tracker/schema-ltv-feedback.sql +11 -0
  35. package/server-edge-tracker/types.ts +251 -0
  36. package/server-edge-tracker/wrangler.toml +24 -6
  37. package/templates/lancamento-imobiliario.md +344 -0
  38. package/docs/PixelBuilder-Documentacao-Completa (2).docx +0 -0
  39. package/server-edge-tracker/modules/dispatch/meta.js +0 -119
  40. package/server-edge-tracker/modules/ml/segmentation.js +0 -316
  41. package/server-edge-tracker/modules/utils.js +0 -89
  42. package/server-edge-tracker/worker.js +0 -4577
@@ -1,22 +1,29 @@
1
1
  /**
2
- * CDP Edge — index.js (ES Module Entry Point)
2
+ * CDP Edge — index.ts (ES Module Entry Point)
3
3
  *
4
4
  * Este arquivo é o novo entry point modular do Worker.
5
5
  * Para usá-lo, altere em wrangler.toml:
6
- * main = "worker.js" → main = "index.js"
6
+ * main = "worker.js" → main = "index.ts"
7
7
  *
8
8
  * O worker.js original permanece intacto como fallback.
9
9
  * Todos os módulos ficam em ./modules/
10
10
  */
11
11
 
12
+ import { ExecutionContext } from '@cloudflare/workers-types';
13
+ import { Env, TrackPayload, BehavioralData, HotmartWebhook, KiwifyWebhook, TictoWebhook } from './types';
14
+
12
15
  // ── Utilitários base ──────────────────────────────────────────────────────────
13
16
  import {
14
- isAllowedOrigin,
15
17
  corsHeaders,
16
18
  sha256,
17
19
  META_TO_GA4,
18
20
  VALID_EVENT_NAMES,
19
- } from './modules/utils.js';
21
+ resolveFunnelStage,
22
+ resolveIntentScore,
23
+ distanceBucketWeight,
24
+ computeMetaSignalWeights,
25
+ metaSignalBucket,
26
+ } from './modules/utils';
20
27
 
21
28
  // ── Banco de dados (D1) ───────────────────────────────────────────────────────
22
29
  import {
@@ -31,23 +38,24 @@ import {
31
38
  saveEdgeFingerprint,
32
39
  resurrectUTM,
33
40
  upsertLtvProfile,
34
- } from './modules/db.js';
41
+ recordLtvFeedback,
42
+ } from './modules/db';
35
43
 
36
44
  // ── Dispatch — plataformas de ads ─────────────────────────────────────────────
37
- import { sendMetaCapi } from './modules/dispatch/meta.js';
38
- import { sendGA4Mp } from './modules/dispatch/ga4.js';
39
- import { sendTikTokApi } from './modules/dispatch/tiktok.js';
45
+ import { sendMetaCapi } from './modules/dispatch/meta';
46
+ import { sendGA4Mp } from './modules/dispatch/ga4';
47
+ import { sendTikTokApi } from './modules/dispatch/tiktok';
40
48
  import {
41
49
  sendPinterestCapi,
42
50
  sendRedditCapi,
43
51
  sendLinkedInCapi,
44
52
  sendSpotifyCapi,
45
- } from './modules/dispatch/platforms.js';
53
+ } from './modules/dispatch/platforms';
46
54
  import {
47
55
  sendWhatsApp,
48
56
  processWhatsAppWebhook,
49
57
  verifyHmac,
50
- } from './modules/dispatch/whatsapp.js';
58
+ } from './modules/dispatch/whatsapp';
51
59
 
52
60
  // ── ML — LTV + A/B Testing ────────────────────────────────────────────────────
53
61
  import {
@@ -58,7 +66,7 @@ import {
58
66
  handleLtvAbTestList,
59
67
  handleLtvAbTestResults,
60
68
  handleLtvAbTestWinner,
61
- } from './modules/ml/ltv.js';
69
+ } from './modules/ml/ltv';
62
70
 
63
71
  // ── ML — Segmentação ──────────────────────────────────────────────────────────
64
72
  import {
@@ -66,14 +74,14 @@ import {
66
74
  handleSegmentationList,
67
75
  handleSegmentationOutliers,
68
76
  handleSegmentationUpdate,
69
- } from './modules/ml/segmentation.js';
77
+ } from './modules/ml/segmentation';
70
78
 
71
79
  // ── ML — Bidding ──────────────────────────────────────────────────────────────
72
80
  import {
73
81
  handleBiddingRecommend,
74
82
  handleBiddingHistory,
75
83
  handleBiddingStatus,
76
- } from './modules/ml/bidding.js';
84
+ } from './modules/ml/bidding';
77
85
 
78
86
  // ── ML — Fraud Detection ──────────────────────────────────────────────────────
79
87
  import {
@@ -84,25 +92,40 @@ import {
84
92
  handleFraudBlocklistAdd,
85
93
  handleFraudBlocklistRemove,
86
94
  handleFraudStats,
87
- } from './modules/ml/fraud.js';
95
+ } from './modules/ml/fraud';
88
96
 
89
97
  // ── Intelligence Agent (Cron) ─────────────────────────────────────────────────
90
98
  import {
91
99
  runIntelligenceAgent,
92
100
  buildGoogleCustomerMatchExport,
93
- } from './modules/intelligence.js';
101
+ } from './modules/intelligence';
102
+
103
+ // ── Haversine distance (km) — sem dependência externa ────────────────────────
104
+ function haversineKm(lat1: number | string | null | undefined, lon1: number | string | null | undefined, lat2: number | string | null | undefined, lon2: number | string | null | undefined): number {
105
+ const R = 6371;
106
+ const lat1Num = parseFloat(String(lat1 ?? '0'));
107
+ const lon1Num = parseFloat(String(lon1 ?? '0'));
108
+ const lat2Num = parseFloat(String(lat2 ?? '0'));
109
+ const lon2Num = parseFloat(String(lon2 ?? '0'));
110
+ const dLat = (lat2Num - lat1Num) * Math.PI / 180;
111
+ const dLon = (lon2Num - lon1Num) * Math.PI / 180;
112
+ const a = Math.sin(dLat / 2) ** 2 +
113
+ Math.cos(lat1Num * Math.PI / 180) * Math.cos(lat2Num * Math.PI / 180) * Math.sin(dLon / 2) ** 2;
114
+ return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
115
+ }
94
116
 
95
117
  // ─────────────────────────────────────────────────────────────────────────────
96
118
  // HANDLER PRINCIPAL
97
119
  // ─────────────────────────────────────────────────────────────────────────────
98
120
  export default {
99
121
 
100
- async fetch(request, env, ctx) {
122
+ async fetch(request: Request, env: Env, ctx: ExecutionContext) {
101
123
  const origin = request.headers.get('Origin') || '';
102
- const headers = {
124
+ const headersObj = {
103
125
  'Content-Type': 'application/json',
104
- ...corsHeaders(origin, env.SITE_DOMAIN),
126
+ ...corsHeaders(origin, env.SITE_DOMAIN || null),
105
127
  };
128
+ const headers = new Headers(headersObj);
106
129
 
107
130
  // Preflight CORS
108
131
  if (request.method === 'OPTIONS') {
@@ -126,10 +149,10 @@ export default {
126
149
  // Roda ANTES de qualquer processamento de evento
127
150
  // Silent drop (200) — bots não sabem que foram detectados
128
151
  if (url.pathname === '/track' && request.method === 'POST') {
129
- let trackBodyForFraud;
152
+ let trackBodyForFraud: TrackPayload = {};
130
153
  try {
131
154
  const cloned = request.clone();
132
- trackBodyForFraud = await cloned.json().catch(() => ({}));
155
+ trackBodyForFraud = await cloned.json().catch(() => ({})) as TrackPayload;
133
156
  } catch { trackBodyForFraud = {}; }
134
157
 
135
158
  const fraudResult = await checkFraudGate(env, request, trackBodyForFraud);
@@ -158,30 +181,30 @@ export default {
158
181
 
159
182
  // ── GET /health ───────────────────────────────────────────────────────────
160
183
  if (request.method === 'GET' && url.pathname === '/health') {
161
- const results = {};
184
+ const results: Record<string, string> = {};
162
185
 
163
186
  try {
164
- await env.DB.prepare('SELECT 1').run();
187
+ await env.DB?.prepare('SELECT 1').run();
165
188
  results.d1 = 'ok';
166
- } catch (err) {
167
- results.d1 = `FAILED: ${err.message}`;
189
+ } catch (err: any) {
190
+ results.d1 = `FAILED: ${err?.message || String(err)}`;
168
191
  }
169
192
 
170
193
  try {
171
- await env.GEO_CACHE.get('__health_check__');
194
+ await env.GEO_CACHE?.get('__health_check__');
172
195
  results.kv = 'ok';
173
- } catch (err) {
174
- results.kv = `FAILED: ${err.message}`;
196
+ } catch (err: any) {
197
+ results.kv = `FAILED: ${err?.message || String(err)}`;
175
198
  }
176
199
 
177
200
  try {
178
- await env.AI.run('@cf/meta/llama-3.1-8b-instruct', {
201
+ await env.AI?.run('@cf/ibm-granite/granite-4.0-h-micro', {
179
202
  messages: [{ role: 'user', content: 'ping' }],
180
203
  max_tokens: 1,
181
204
  });
182
205
  results.ai = 'ok';
183
- } catch (err) {
184
- results.ai = `FAILED: ${err.message}`;
206
+ } catch (err: any) {
207
+ results.ai = `FAILED: ${err?.message || String(err)}`;
185
208
  }
186
209
 
187
210
  const vars = {
@@ -239,7 +262,8 @@ export default {
239
262
  'utmSource','utmMedium','utmCampaign','utmContent','utmTerm',
240
263
  'fbclid','ttclid','gclid','transactionId','productName','currency'];
241
264
 
242
- const { eventName, behavioral_data, ...payload } = body;
265
+ const { eventName, behavioral_data, ...payload } = body as { eventName?: string; behavioral_data?: BehavioralData; [key: string]: any };
266
+ const trackPayload: TrackPayload = payload;
243
267
 
244
268
  if (!eventName) {
245
269
  return new Response(JSON.stringify({ error: 'eventName é obrigatório' }), { status: 400, headers });
@@ -250,19 +274,19 @@ export default {
250
274
  }
251
275
 
252
276
  for (const field of STR_FIELDS) {
253
- if (payload[field] !== undefined && payload[field] !== null) {
254
- if (typeof payload[field] !== 'string' || payload[field].length > 512) {
277
+ if (trackPayload[field as keyof TrackPayload] !== undefined && trackPayload[field as keyof TrackPayload] !== null) {
278
+ if (typeof trackPayload[field as keyof TrackPayload] !== 'string' || String(trackPayload[field as keyof TrackPayload]).length > 512) {
255
279
  return new Response(JSON.stringify({ error: `Campo inválido: ${field}` }), { status: 400, headers });
256
280
  }
257
281
  }
258
282
  }
259
283
 
260
- if (payload.value !== undefined && payload.value !== null) {
261
- const v = Number(payload.value);
284
+ if (trackPayload.value !== undefined && trackPayload.value !== null) {
285
+ const v = Number(trackPayload.value);
262
286
  if (isNaN(v) || v < 0 || v > 9_999_999) {
263
287
  return new Response(JSON.stringify({ error: 'value fora do intervalo permitido' }), { status: 400, headers });
264
288
  }
265
- payload.value = v;
289
+ trackPayload.value = v;
266
290
  }
267
291
 
268
292
  // ── Extrair dados comportamentais do browser ──────────────────────────
@@ -270,6 +294,9 @@ export default {
270
294
  payload.engagementScore = behavioral_data.engagement_score ?? behavioral_data.totalScore ?? null;
271
295
  payload.intentionLevel = behavioral_data.intention_level ?? null;
272
296
  payload.userScore = behavioral_data.user_score ?? null;
297
+ // Sinais de engajamento profundo — usados no anti-falso-positivo e no LTV
298
+ payload.scrollScore = behavioral_data.scroll_score ?? null;
299
+ payload.timeLevel = behavioral_data.time_level ?? null;
273
300
  payload.email = payload.email || behavioral_data.email || null;
274
301
  payload.phone = payload.phone || behavioral_data.phone || null;
275
302
  payload.firstName = payload.firstName || behavioral_data.first_name || behavioral_data.firstName || null;
@@ -280,11 +307,40 @@ export default {
280
307
  payload.dob = payload.dob || behavioral_data.dob || null;
281
308
  }
282
309
 
310
+ // ── Normalização de intent_score → 0.0–1.0 ──────────────────────────
311
+ // Aceita string ('high'/'medium'/'low'), 0-1 ou 0-100 enviados pelo front.
312
+ // intent_bucket mantém a label legível para D1 e logs.
313
+ const intentScoreNum = resolveIntentScore(payload.intent_score);
314
+ if (intentScoreNum !== null) {
315
+ payload.intent_score = intentScoreNum;
316
+ payload.intentScoreNum = intentScoreNum;
317
+ payload.intent_bucket = intentScoreNum >= 0.8 ? 'high'
318
+ : intentScoreNum >= 0.5 ? 'medium' : 'low';
319
+ } else {
320
+ payload.intentScoreNum = null;
321
+ }
322
+
323
+ // ── Anti-falso-positivo ───────────────────────────────────────────────
324
+ // Penaliza intent se engajamento insuficiente: scroll raso E tempo curto.
325
+ // scroll_score < 2.0 ≈ não passou de 50% da página.
326
+ // time_level 'curioso' = menos de 60 segundos na página.
327
+ if (payload.intentScoreNum !== null) {
328
+ const isShallowScroll = payload.scrollScore !== null && payload.scrollScore < 2.0;
329
+ const isShallowTime = payload.timeLevel === 'curioso';
330
+ if (isShallowScroll && isShallowTime) {
331
+ const penalized = Math.round(payload.intentScoreNum * 0.7 * 100) / 100;
332
+ payload.intentScoreNum = penalized;
333
+ payload.intent_score = penalized;
334
+ payload.intent_bucket = penalized >= 0.8 ? 'high' : penalized >= 0.5 ? 'medium' : 'low';
335
+ payload.intent_penalized = true; // flag auditável — visível no D1 e logs
336
+ }
337
+ }
338
+
283
339
  // ── Edge Fingerprint + UTM Resurrection ──────────────────────────────
284
340
  const fingerprint = await generateEdgeFingerprint(request);
285
341
  payload.utmRestored = false;
286
342
 
287
- if (fingerprint) {
343
+ if (fingerprint && env.DB) {
288
344
  if (payload.utmSource) {
289
345
  ctx.waitUntil(saveEdgeFingerprint(env.DB, fingerprint, payload.userId, payload));
290
346
  } else {
@@ -301,13 +357,13 @@ export default {
301
357
  }
302
358
 
303
359
  // ── Bot Mitigation ────────────────────────────────────────────────────
304
- const botScoreStr = request.cf?.botManagement?.score;
305
- const cfBotScore = botScoreStr !== undefined ? parseInt(botScoreStr) : 100;
360
+ const botScoreStr = (request as any).cf?.botManagement?.score;
361
+ const cfBotScore = botScoreStr !== undefined ? parseInt(String(botScoreStr)) : 100;
306
362
  const ua = (request.headers.get('User-Agent') || '').toLowerCase();
307
363
  const isBotPattern = /bot|crawler|spider|lighthouse|postman|curl|inspect|headless/i.test(ua);
308
364
 
309
365
  const isBot = cfBotScore < 30 || isBotPattern;
310
- payload.botScore = isBot ? 2 : (cfBotScore < 60 ? 1 : 0);
366
+ trackPayload.botScore = isBot ? 2 : (cfBotScore < 60 ? 1 : 0);
311
367
 
312
368
  if (isBot && !['Contact', 'Lead', 'Purchase'].includes(eventName)) {
313
369
  return new Response(JSON.stringify({ ok: true, skipped_due_to_bot: true }), { status: 200, headers });
@@ -324,6 +380,34 @@ export default {
324
380
 
325
381
  const ga4Name = META_TO_GA4[eventName] || eventName.toLowerCase();
326
382
 
383
+ // ── Dual-layer semantics ─────────────────────────────────────────────
384
+ // Meta sempre recebe o nome canônico (Schedule, Lead, etc.).
385
+ // Internamente o CDP usa semântica de funil precisa via FUNNEL_TAXONOMY.
386
+ if (payload.funnel_stage) {
387
+ const { depth, funnelDepth } = resolveFunnelStage(payload.funnel_stage);
388
+ payload.funnelDepth = funnelDepth; // ex: 'bottom_intent', 'bottom_conversion'
389
+ payload.funnelLevel = depth; // ex: 'bottom', 'conversion', 'mid'
390
+ }
391
+ if (eventName === 'Schedule' && payload.funnel_stage === 'route_click') {
392
+ payload.internalEvent = 'IntentToVisit';
393
+ }
394
+
395
+ // ── Real Estate Distance Enrichment ──────────────────────────────────
396
+ // Calcula distância entre o IP do usuário (via Cloudflare) e o imóvel.
397
+ // Ativa quando o evento carrega property_lat + property_lng (ex: Schedule de rota).
398
+ const propLat = parseFloat(String(trackPayload.property_lat ?? trackPayload.propertyLat));
399
+ const propLng = parseFloat(String(trackPayload.property_lng ?? trackPayload.propertyLng));
400
+ const userLat = parseFloat(String(request.cf?.latitude ?? '0'));
401
+ const userLng = parseFloat(String(request.cf?.longitude ?? '0'));
402
+ if (!isNaN(propLat) && !isNaN(propLng) && !isNaN(userLat) && !isNaN(userLng)) {
403
+ const distKm = haversineKm(userLat, userLng, propLat, propLng);
404
+ trackPayload.distanceKm = Math.round(distKm * 10) / 10;
405
+ trackPayload.distanceBucket = distKm < 5 ? 'very_close' :
406
+ distKm < 15 ? 'close' :
407
+ distKm < 30 ? 'nearby' :
408
+ distKm < 60 ? 'moderate' : 'far';
409
+ }
410
+
327
411
  // ── LTV Prediction (+ A/B Testing de Prompts) ─────────────────────────
328
412
  const LTV_EVENTS = ['Lead', 'Contact', 'Schedule', 'CompleteRegistration'];
329
413
  if (LTV_EVENTS.includes(eventName) && !payload.value) {
@@ -342,16 +426,45 @@ export default {
342
426
  recordAbAssignment(
343
427
  env,
344
428
  payload.userId,
345
- abVariation.variation_id,
429
+ abVariation.id,
346
430
  abVariation.test_id,
347
431
  ltv.value,
348
432
  ltv.class,
349
- emailHash,
433
+ emailHash ?? null,
350
434
  )
351
435
  );
352
436
  }
353
437
  }
354
438
 
439
+ // ── LTV Feedback Loop — fecha o ciclo preditivo ──────────────────────
440
+ // Quando uma compra real acontece, registra o valor real e recalcula accuracy.
441
+ // Alimenta ltv_ab_variations.accuracy_score → autoDecideAbWinner usa isso.
442
+ if (eventName === 'Purchase' && payload.value > 0) {
443
+ ctx.waitUntil(recordLtvFeedback(env, payload.userId, payload.value));
444
+ }
445
+
446
+ // ── Meta Signal Score (composite, pesos dinâmicos) ───────────────────
447
+ // Pesos variam por profundidade de funil: fundo = comportamento pesa mais.
448
+ {
449
+ const w = computeMetaSignalWeights(payload.funnelLevel);
450
+ const iW = payload.intentScoreNum ?? 0.5;
451
+ const lW = payload.ltvScore ? payload.ltvScore / 100 : 0.5;
452
+ const dW = distanceBucketWeight(payload.distanceBucket);
453
+ payload.metaSignal = Math.round(((iW * w.intent) + (lW * w.ltv) + (dW * w.dist)) * 100) / 100;
454
+ payload.metaSignalBucket = metaSignalBucket(payload.metaSignal); // 'hot'/'warm'/'cold'
455
+ }
456
+
457
+ // ── Hot Lead Trigger (timing + sinal) ────────────────────────────────
458
+ // Reage em tempo real: WhatsApp apenas quando comportamento + sinal justificam.
459
+ // Critérios: rota + muito próximo + (intent alto OU meta_signal alto)
460
+ // + (janela de decisão BRT 18–22h OU sinal excepcional ≥ 0.9)
461
+ const hourBRT = (new Date().getUTCHours() - 3 + 24) % 24;
462
+ const inWindow = hourBRT >= 18 && hourBRT <= 22;
463
+ const isHotLead = payload.funnel_stage === 'route_click'
464
+ && payload.distanceBucket === 'very_close'
465
+ && ((payload.intentScoreNum ?? 0) >= 0.8 || payload.metaSignal >= 0.85)
466
+ && (inWindow || payload.metaSignal >= 0.9);
467
+
355
468
  // Cross-Device Graph — background
356
469
  if (env.DB && payload.userId && (payload.email || payload.phone)) {
357
470
  ctx.waitUntil(resolveDeviceGraph(env.DB, payload.userId, payload.email, payload.phone));
@@ -366,36 +479,37 @@ export default {
366
479
  sendMetaCapi(env, eventName, payload, request, ctx),
367
480
  sendGA4Mp(env, ga4Name, payload, ctx),
368
481
  sendTikTokApi(env, eventName, payload, request, ctx),
369
- saveLead(env, eventName, payload, request, 'website'),
482
+ saveLead(env, payload.internalEvent || eventName, payload, request, 'website'),
370
483
  upsertProfile(env, eventName, payload, request),
371
- ...(WHATSAPP_NOTIFY_EVENTS.includes(eventName)
372
- ? [sendWhatsApp(env, eventName, payload)]
484
+ ...(WHATSAPP_NOTIFY_EVENTS.includes(eventName) || isHotLead
485
+ ? [sendWhatsApp(env, isHotLead ? 'hot_lead_intent_to_visit' : eventName, payload)]
373
486
  : []),
374
487
  ]);
375
488
 
376
489
  // Automação de mensagens
377
490
  const AUTOMATION_EVENTS = ['Lead', 'Purchase', 'InitiateCheckout'];
378
491
  if (AUTOMATION_EVENTS.includes(eventName) && env.DB) {
492
+ const db = env.DB; // Captura em variável local
379
493
  ctx.waitUntil(
380
494
  (async () => {
381
495
  try {
382
- const lastLead = await env.DB
496
+ const lastLead = await db
383
497
  .prepare(`SELECT id FROM leads WHERE event_id = ?1 LIMIT 1`)
384
- .bind(payload.eventId || payload.event_id || '')
385
- .first();
386
- const leadId = lastLead?.id ?? null;
387
- if (leadId) await fireAutomation(env, eventName, leadId, payload);
388
- } catch (e) { console.error('[Automation] lead lookup error:', e.message); }
498
+ .bind(trackPayload.eventId || trackPayload.event_id || '')
499
+ .first() as any;
500
+ const leadId = lastLead?.id ? Number(lastLead.id) : null;
501
+ if (leadId) await fireAutomation(env, eventName, leadId, trackPayload);
502
+ } catch (e: any) { console.error('[Automation] lead lookup error:', e?.message || String(e)); }
389
503
  })()
390
504
  );
391
505
  }
392
506
 
393
507
  // Edge Personalization
394
508
  let currentScore = 0;
395
- if (env.DB && payload.userId) {
509
+ if (env.DB && trackPayload.userId) {
396
510
  try {
397
- const profileRow = await env.DB.prepare('SELECT score FROM user_profiles WHERE user_id = ?').bind(payload.userId).first();
398
- if (profileRow) currentScore = profileRow.score;
511
+ const profileRow = await env.DB.prepare('SELECT score FROM user_profiles WHERE user_id = ?').bind(trackPayload.userId).first();
512
+ if (profileRow) currentScore = Number(profileRow.score) || 0;
399
513
  } catch {}
400
514
  }
401
515
 
@@ -405,9 +519,9 @@ export default {
405
519
  return new Response(JSON.stringify({
406
520
  ok: true,
407
521
  userProfile: { score: currentScore, user_id: finalUserId },
408
- meta: metaRes.value ?? { error: metaRes.reason?.message },
409
- ga4: ga4Res.value ?? { error: ga4Res.reason?.message },
410
- tiktok: ttRes.value ?? { error: ttRes.reason?.message },
522
+ meta: metaRes.status === 'fulfilled' ? metaRes.value : { error: metaRes.reason?.message },
523
+ ga4: ga4Res.status === 'fulfilled' ? ga4Res.value : { error: ga4Res.reason?.message },
524
+ tiktok: ttRes.status === 'fulfilled' ? ttRes.value : { error: ttRes.reason?.message },
411
525
  }), { status: 200, headers: resHeaders });
412
526
  }
413
527
 
@@ -420,8 +534,8 @@ export default {
420
534
  }
421
535
  }
422
536
 
423
- let wh;
424
- try { wh = await request.json(); } catch {
537
+ let wh: HotmartWebhook;
538
+ try { wh = await request.json() as HotmartWebhook; } catch {
425
539
  return new Response('JSON inválido', { status: 400 });
426
540
  }
427
541
 
@@ -498,8 +612,8 @@ export default {
498
612
  }
499
613
  }
500
614
 
501
- let wh;
502
- try { wh = await request.json(); } catch {
615
+ let wh: KiwifyWebhook;
616
+ try { wh = await request.json() as KiwifyWebhook; } catch {
503
617
  return new Response('JSON inválido', { status: 400 });
504
618
  }
505
619
 
@@ -520,8 +634,8 @@ export default {
520
634
  }
521
635
 
522
636
  const customer = wh.Customer || {};
523
- const product = wh.Product || {};
524
- const profile = await getProfileByEmail(env, customer.email);
637
+ const product = wh.Product || {};
638
+ const profile = await getProfileByEmail(env, customer.email || '');
525
639
 
526
640
  const payload = {
527
641
  email: customer.email,
@@ -582,8 +696,8 @@ export default {
582
696
  }
583
697
  }
584
698
 
585
- let wh;
586
- try { wh = JSON.parse(rawBody); } catch {
699
+ let wh: TictoWebhook;
700
+ try { wh = JSON.parse(rawBody) as TictoWebhook; } catch {
587
701
  return new Response('JSON inválido', { status: 400 });
588
702
  }
589
703
 
@@ -598,7 +712,7 @@ export default {
598
712
  const tracking = wh.tracking || wh.url_params || {};
599
713
 
600
714
  const valueRaw = order.paid_amount ?? order.total ?? order.amount;
601
- const value = valueRaw ? parseFloat(valueRaw) / 100 : undefined;
715
+ const value = valueRaw ? parseFloat(String(valueRaw)) / 100 : undefined;
602
716
  const transactionId = order.hash || order.transaction_hash || order.id;
603
717
  const tcTxId = String(order.hash || order.transaction_hash || order.id || '');
604
718
 
@@ -614,7 +728,7 @@ export default {
614
728
  }
615
729
 
616
730
  const urlUserId = tracking.user_id || wh.url_params?.user_id;
617
- let profile = await getProfileByEmail(env, customer.email);
731
+ let profile = await getProfileByEmail(env, customer.email || '');
618
732
  if (!profile && urlUserId && env.DB) {
619
733
  try {
620
734
  profile = await env.DB.prepare(
@@ -760,7 +874,7 @@ export default {
760
874
  },
761
875
 
762
876
  // ── Cron Handler — Intelligence Agent ────────────────────────────────────────
763
- async scheduled(event, env, ctx) {
877
+ async scheduled(event: any, env: Env, ctx: ExecutionContext) {
764
878
  const cron = event.cron;
765
879
  const isMonthly = cron === '0 3 1 * *';
766
880
 
@@ -768,9 +882,9 @@ export default {
768
882
  },
769
883
 
770
884
  // ── Queue Consumer — Retry de eventos com falha ───────────────────────────────
771
- async queue(batch, env) {
885
+ async queue(batch: any, env: Env) {
772
886
  for (const message of batch.messages) {
773
- const { eventType, payload, platform, attempt = 1 } = message.body;
887
+ const { eventType, payload, platform } = message.body as { eventType: string; payload: TrackPayload; platform: string; attempt?: number };
774
888
 
775
889
 
776
890
  try {
@@ -779,8 +893,8 @@ export default {
779
893
  if (platform === 'tiktok') await sendTikTokApi(env, eventType, payload, null, null);
780
894
 
781
895
  message.ack();
782
- } catch (err) {
783
- console.error(`[Queue] Falha ao reprocessar ${platform}/${eventType}:`, err.message);
896
+ } catch (err: any) {
897
+ console.error(`[Queue] Falha ao reprocessar ${platform}/${eventType}:`, err?.message || String(err));
784
898
  message.retry();
785
899
  }
786
900
  }