cdp-edge 1.2.2 → 1.3.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 (141) hide show
  1. package/README.md +153 -306
  2. package/bin/cdp-edge.js +71 -61
  3. package/contracts/agent-versions.json +682 -0
  4. package/contracts/api-versions.json +372 -368
  5. package/contracts/types.ts +81 -0
  6. package/dist/commands/analyze.js +52 -52
  7. package/dist/commands/infra.js +54 -54
  8. package/dist/commands/install.js +26 -3
  9. package/dist/commands/server.js +174 -174
  10. package/dist/commands/setup.js +332 -100
  11. package/dist/commands/validate.js +248 -84
  12. package/dist/index.js +12 -12
  13. package/dist/sdk/cdpTrack.js +2095 -0
  14. package/dist/sdk/cdpTrack.min.js +64 -0
  15. package/dist/sdk/install-snippet.html +10 -0
  16. package/docs/whatsapp-ctwa.md +5 -4
  17. package/extracted-skill/tracking-events-generator/INTEGRACAO-COMPLETA.md +89 -0
  18. package/extracted-skill/tracking-events-generator/MELHORIAS-IMPLEMENTADAS.md +101 -0
  19. package/extracted-skill/tracking-events-generator/advanced-matching.js +364 -364
  20. package/extracted-skill/tracking-events-generator/agents/ab-ltv-agent.md +196 -0
  21. package/extracted-skill/tracking-events-generator/agents/ab-testing-agent.md +1 -1
  22. package/extracted-skill/tracking-events-generator/agents/attribution-agent.md +41 -41
  23. package/extracted-skill/tracking-events-generator/agents/bidding-agent.md +347 -0
  24. package/extracted-skill/tracking-events-generator/agents/bing-agent.md +40 -50
  25. package/extracted-skill/tracking-events-generator/agents/browser-tracking.md +174 -74
  26. package/extracted-skill/tracking-events-generator/agents/code-guardian-agent.md +1 -1
  27. package/extracted-skill/tracking-events-generator/agents/compliance-agent.md +25 -5
  28. package/extracted-skill/tracking-events-generator/agents/dashboard-agent.md +10 -10
  29. package/extracted-skill/tracking-events-generator/agents/database-agent.md +43 -42
  30. package/extracted-skill/tracking-events-generator/agents/debug-agent.md +22 -22
  31. package/extracted-skill/tracking-events-generator/agents/devops-agent.md +232 -0
  32. package/extracted-skill/tracking-events-generator/agents/domain-setup-agent.md +23 -9
  33. package/extracted-skill/tracking-events-generator/agents/email-agent.md +28 -1
  34. package/extracted-skill/tracking-events-generator/agents/evo-crm-agent.md +244 -0
  35. package/extracted-skill/tracking-events-generator/agents/fingerprint-agent.md +206 -1
  36. package/extracted-skill/tracking-events-generator/agents/fraud-detection-agent.md +143 -0
  37. package/extracted-skill/tracking-events-generator/agents/google-agent.md +128 -2
  38. package/extracted-skill/tracking-events-generator/agents/intelligence-agent.md +191 -31
  39. package/extracted-skill/tracking-events-generator/agents/lead-scoring-agent.md +282 -0
  40. package/extracted-skill/tracking-events-generator/agents/linkedin-agent.md +145 -34
  41. package/extracted-skill/tracking-events-generator/agents/localization-agent.md +1 -1
  42. package/extracted-skill/tracking-events-generator/agents/ltv-predictor-agent.md +5 -5
  43. package/extracted-skill/tracking-events-generator/agents/master-feedback-loop.md +81 -21
  44. package/extracted-skill/tracking-events-generator/agents/master-orchestrator.md +313 -93
  45. package/extracted-skill/tracking-events-generator/agents/match-quality-agent.md +304 -0
  46. package/extracted-skill/tracking-events-generator/agents/memory-agent.md +190 -15
  47. package/extracted-skill/tracking-events-generator/agents/meta-agent.md +10 -2
  48. package/extracted-skill/tracking-events-generator/agents/ml-clustering-agent.md +749 -0
  49. package/extracted-skill/tracking-events-generator/agents/page-analyzer.md +21 -4
  50. package/extracted-skill/tracking-events-generator/agents/performance-agent.md +41 -31
  51. package/extracted-skill/tracking-events-generator/agents/performance-optimization-agent.md +18 -8
  52. package/extracted-skill/tracking-events-generator/agents/pinterest-agent.md +14 -6
  53. package/extracted-skill/tracking-events-generator/agents/premium-tracking-intelligence-agent.md +7 -7
  54. package/extracted-skill/tracking-events-generator/agents/r2-setup-agent.md +16 -8
  55. package/extracted-skill/tracking-events-generator/agents/reddit-agent.md +15 -7
  56. package/extracted-skill/tracking-events-generator/agents/security-enterprise-agent.md +157 -48
  57. package/extracted-skill/tracking-events-generator/agents/server-tracking.md +35 -35
  58. package/extracted-skill/tracking-events-generator/agents/spotify-agent.md +15 -7
  59. package/extracted-skill/tracking-events-generator/agents/tiktok-agent.md +73 -2
  60. package/extracted-skill/tracking-events-generator/agents/tracking-plan-agent.md +104 -9
  61. package/extracted-skill/tracking-events-generator/agents/utm-agent.md +322 -0
  62. package/extracted-skill/tracking-events-generator/agents/validator-agent.md +13 -9
  63. package/extracted-skill/tracking-events-generator/agents/webhook-agent.md +112 -4
  64. package/extracted-skill/tracking-events-generator/agents/whatsapp-agent.md +58 -5
  65. package/extracted-skill/tracking-events-generator/agents/whatsapp-ctwa-setup-agent.md +26 -18
  66. package/extracted-skill/tracking-events-generator/agents/youtube-agent.md +152 -37
  67. package/extracted-skill/tracking-events-generator/anti-blocking.js +285 -285
  68. package/extracted-skill/tracking-events-generator/cdpTrack.js +642 -641
  69. package/extracted-skill/tracking-events-generator/contracts/api-versions.json +14 -10
  70. package/extracted-skill/tracking-events-generator/engagement-scoring.js +226 -226
  71. package/extracted-skill/tracking-events-generator/evals/evals.json +235 -235
  72. package/extracted-skill/tracking-events-generator/integration-test.js +497 -497
  73. package/extracted-skill/tracking-events-generator/knowledge-base.md +172 -0
  74. package/extracted-skill/tracking-events-generator/micro-events.js +992 -992
  75. package/extracted-skill/tracking-events-generator/models/lancamento-imobiliario.md +344 -0
  76. package/extracted-skill/tracking-events-generator/models/pinterest/conversions-api-template.js +144 -144
  77. package/extracted-skill/tracking-events-generator/models/pinterest/event-mappings.json +48 -48
  78. package/extracted-skill/tracking-events-generator/models/pinterest/tag-template.js +28 -28
  79. package/extracted-skill/tracking-events-generator/models/quiz-funnel.md +83 -19
  80. package/extracted-skill/tracking-events-generator/models/reddit/conversions-api-template.js +205 -205
  81. package/extracted-skill/tracking-events-generator/models/reddit/event-mappings.json +56 -56
  82. package/extracted-skill/tracking-events-generator/models/reddit/pixel-template.js +19 -19
  83. package/extracted-skill/tracking-events-generator/models/scenarios/behavior-engine.js +425 -425
  84. package/extracted-skill/tracking-events-generator/route-intent-capture.js +222 -0
  85. package/extracted-skill/tracking-events-generator/tracking.config.js +3 -3
  86. package/package.json +89 -75
  87. package/scripts/build-sdk.js +106 -0
  88. package/server-edge-tracker/.client.env.example +14 -0
  89. package/server-edge-tracker/INSTALAR.md +222 -23
  90. package/server-edge-tracker/SEGMENTATION-DOCS.md +513 -0
  91. package/server-edge-tracker/config/utm-mapping.json +64 -0
  92. package/server-edge-tracker/deploy-client.cjs +76 -0
  93. package/server-edge-tracker/index.ts +1230 -0
  94. package/server-edge-tracker/migrate-v7.sql +64 -0
  95. package/server-edge-tracker/modules/db.ts +710 -0
  96. package/server-edge-tracker/modules/dispatch/crm.ts +382 -0
  97. package/server-edge-tracker/modules/dispatch/ga4.ts +72 -0
  98. package/server-edge-tracker/modules/dispatch/meta.ts +143 -0
  99. package/server-edge-tracker/modules/dispatch/platforms.ts +255 -0
  100. package/server-edge-tracker/modules/dispatch/tiktok.ts +107 -0
  101. package/server-edge-tracker/modules/dispatch/whatsapp.ts +296 -0
  102. package/server-edge-tracker/modules/intelligence.ts +589 -0
  103. package/server-edge-tracker/modules/ml/bidding.ts +247 -0
  104. package/server-edge-tracker/modules/ml/fraud.ts +302 -0
  105. package/server-edge-tracker/modules/ml/logistic.ts +226 -0
  106. package/server-edge-tracker/modules/ml/ltv.ts +531 -0
  107. package/server-edge-tracker/modules/ml/matchquality.ts +232 -0
  108. package/server-edge-tracker/modules/ml/quiz.ts +343 -0
  109. package/server-edge-tracker/modules/ml/roas.ts +255 -0
  110. package/server-edge-tracker/modules/ml/segmentation.ts +407 -0
  111. package/server-edge-tracker/modules/nurture.ts +257 -0
  112. package/server-edge-tracker/modules/utils.ts +311 -0
  113. package/server-edge-tracker/modules/utm/utm-enricher.ts +231 -0
  114. package/server-edge-tracker/schema-ab-ltv.sql +97 -0
  115. package/server-edge-tracker/schema-bidding.sql +86 -0
  116. package/server-edge-tracker/schema-fraud.sql +90 -0
  117. package/server-edge-tracker/schema-indexes.sql +67 -0
  118. package/server-edge-tracker/schema-ltv-feedback.sql +11 -0
  119. package/server-edge-tracker/schema-quiz.sql +52 -0
  120. package/server-edge-tracker/schema-sales-engine.sql +113 -0
  121. package/server-edge-tracker/schema-segmentation.sql +219 -0
  122. package/server-edge-tracker/schema-utm.sql +82 -0
  123. package/server-edge-tracker/schema.sql +281 -265
  124. package/server-edge-tracker/types.ts +275 -0
  125. package/server-edge-tracker/wrangler.toml +140 -85
  126. package/templates/lancamento-imobiliario.md +344 -0
  127. package/templates/multi-step-checkout.md +3 -4
  128. package/templates/pinterest/conversions-api-template.js +144 -144
  129. package/templates/pinterest/event-mappings.json +48 -48
  130. package/templates/pinterest/tag-template.js +28 -28
  131. package/templates/quiz-funnel.md +83 -19
  132. package/templates/reddit/conversions-api-template.js +205 -205
  133. package/templates/reddit/event-mappings.json +56 -56
  134. package/templates/reddit/pixel-template.js +12 -39
  135. package/templates/scenarios/behavior-engine.js +45 -22
  136. package/docs/PixelBuilder-Documentacao-Completa (2).docx +0 -0
  137. package/docs/installation.md +0 -155
  138. package/docs/quick-start.md +0 -185
  139. package/extracted-skill/tracking-events-generator/agents/crm-integration-agent.md +0 -1419
  140. package/extracted-skill/tracking-events-generator/agents/intelligence-scheduling.md +0 -643
  141. package/server-edge-tracker/worker.js +0 -2574
@@ -0,0 +1,531 @@
1
+ /**
2
+ * CDP Edge — LTV Prediction + A/B Testing de Prompts (Fases 3 e 4)
3
+ * predictLtv, getLtvAbVariation, recordAbAssignment, handlers /api/ltv/*
4
+ */
5
+
6
+ import { extractFeatures, predictWithWeights, loadActiveWeights } from './logistic.js';
7
+ import { Env, TrackPayload } from '../../types.js';
8
+
9
+ // ── Tipos ───────────────────────────────────────────────────────────────────────
10
+ export interface LtvResult {
11
+ score: number;
12
+ class: string;
13
+ value: number;
14
+ source?: string;
15
+ }
16
+
17
+ export interface AbVariation {
18
+ id: number;
19
+ test_id: number;
20
+ name: string;
21
+ system_prompt: string;
22
+ weight: number;
23
+ is_control: number;
24
+ total_assigned?: number;
25
+ accuracy_score?: number;
26
+ }
27
+
28
+ export interface AbTestCreate {
29
+ name: string;
30
+ description?: string;
31
+ min_sample?: number;
32
+ variations: Array<{
33
+ name: string;
34
+ system_prompt?: string;
35
+ weight?: number;
36
+ is_control?: boolean;
37
+ }>;
38
+ }
39
+
40
+ export interface AutoDecideResult {
41
+ decided: boolean;
42
+ reason?: string;
43
+ test_id?: number;
44
+ test_name?: string;
45
+ winner_id?: number;
46
+ winner_name?: string;
47
+ improvement?: string;
48
+ is_control_winner?: boolean;
49
+ winning_prompt?: string;
50
+ }
51
+
52
+ // Cache key para o teste ativo (KV — evita hit no D1 a cada request /track)
53
+ const AB_LTV_CACHE_KEY = 'ab_ltv_active_test';
54
+
55
+ // ── Prompt especializado para imóveis ───────────────────────────────────────
56
+ // Ativado automaticamente quando property_lat/lng estão presentes no payload.
57
+ // Override por A/B test tem prioridade sobre este prompt.
58
+ const REAL_ESTATE_PROMPT = `You are a real estate lead scoring expert for the Brazilian market.
59
+ Reply ONLY with a JSON object {"adjustment": <integer between -15 and 15>} based on the lead data.
60
+ Scoring rules (apply additively):
61
+ - distance_km < 5: +12 (lives nearby, buys fast)
62
+ - distance_km 5-15: +8
63
+ - distance_km 15-30: +3
64
+ - distance_km > 30: 0
65
+ - distance_km unknown: +3 (gave intent signal without geo)
66
+ - event = Schedule or route click: +5 (physical visit intent)
67
+ - scroll_score >= 3 AND time_level = comprador: +4 (deep engagement)
68
+ - hour_brt between 18-22 (weekday): +3 (active decision window)
69
+ - has_phone = true: +2 (reachable for follow-up)
70
+ No explanation. JSON only.`;
71
+
72
+ // ── predictLtv — Heurística em 5 dimensões (0-100 pts) ───────────────────────
73
+ export async function predictLtv(env: Env, payload: TrackPayload, request: Request | null, customSystemPrompt: string | null = null): Promise<LtvResult> {
74
+ // ── Tentar modelo treinado (regressão logística real) ─────────────────────
75
+ // Se existir modelo ativo no KV/D1, usa-o em vez da heurística manual.
76
+ // Fallback automático para heurística se modelo não disponível.
77
+ try {
78
+ const model = await loadActiveWeights(env);
79
+ if (model?.weights?.length) {
80
+ const hour = new Date().getUTCHours();
81
+ const country = (payload.country || (request as any)?.cf?.country || '').toUpperCase();
82
+ const features = extractFeatures({
83
+ utm_source: payload.utmSource,
84
+ engagement_score: parseFloat(String(payload.engagementScore || '0')),
85
+ intention_level: payload.intentionLevel,
86
+ days_since_lead: 0, // evento atual = recência máxima
87
+ has_email: !!payload.email,
88
+ has_phone: !!payload.phone,
89
+ is_br: country === 'BR',
90
+ hour,
91
+ });
92
+
93
+ const score100 = predictWithWeights(model, features);
94
+ const ltvClass = score100 >= 70 ? 'High' : score100 >= 40 ? 'Medium' : 'Low';
95
+ const ltvMultiplier = score100 >= 70 ? 3.5 : score100 >= 40 ? 1.8 : 0.8;
96
+ const productValue = payload.value ? parseFloat(String(payload.value)) : 0;
97
+ const baseValue = productValue > 0 ? productValue : 197;
98
+ const predictedValue = Math.round(baseValue * ltvMultiplier * 100) / 100;
99
+
100
+ return { score: score100, class: ltvClass, value: predictedValue, source: 'model' };
101
+ }
102
+ } catch { /* fallback para heurística */ }
103
+
104
+ let score = 0;
105
+
106
+ // 1. Engajamento browser (0–30)
107
+ const engScore = parseFloat(String(payload.engagementScore || '0'));
108
+ const userScore = parseFloat(String((payload as any).userScore || '0'));
109
+ score += Math.min(15, Math.round((engScore / 5) * 15));
110
+ score += Math.min(15, Math.round((userScore / 100) * 15));
111
+
112
+ // 2. Origem de tráfego (0–25)
113
+ const src = (payload.utmSource || '').toLowerCase();
114
+ const utm_score_map: Record<string, number> = {
115
+ facebook: 25, instagram: 25, meta: 25,
116
+ google: 22, youtube: 22, tiktok: 20,
117
+ email: 18, sms: 18,
118
+ organic: 10, direct: 5,
119
+ };
120
+ score += utm_score_map[src] ?? (src ? 8 : 3);
121
+
122
+ // 3. Contexto de rede (0–15)
123
+ const hour = new Date().getUTCHours();
124
+ const country = (payload.country || (request as any)?.cf?.country || '').toUpperCase();
125
+ const org = String((request as any).cf?.asOrganization || '').toLowerCase();
126
+
127
+ const isHighConvTime = hour >= 21 || hour <= 2;
128
+ score += isHighConvTime ? 8 : (hour >= 12 && hour <= 20 ? 4 : 1);
129
+
130
+ const latam = ['AR', 'CL', 'CO', 'MX', 'PE', 'UY', 'PY', 'BO'];
131
+ score += country === 'BR' ? 5 : (latam.includes(country) ? 3 : 1);
132
+
133
+ const isCorp = /ltda|s\.a\.|corp|telecom|fibra|claro|vivo|tim|oi/.test(org);
134
+ score += isCorp ? 2 : 0;
135
+
136
+ // 4. Contexto do evento (0–20)
137
+ const intentionLevel = (payload.intentionLevel || '').toLowerCase();
138
+ if (intentionLevel === 'comprador' || intentionLevel === 'high_intent') score += 20;
139
+ else if (intentionLevel === 'interessado') score += 12;
140
+ else if (intentionLevel === 'nurture') score += 6;
141
+
142
+ // 5. Dados PII disponíveis (0–10)
143
+ if (payload.email) score += 4;
144
+ if (payload.phone) score += 4;
145
+ if (payload.firstName) score += 2;
146
+
147
+ // 5b. Tipo de evento imobiliário (0–15) — sinais de intenção de compra física
148
+ const evType = ((payload as any).eventType || '').toLowerCase();
149
+ if (evType === 'customizeproduct') score += 15; // simulação de financiamento → intenção máxima
150
+ else if (evType === 'findlocation') score += 10; // viu mapa/localização → visita física iminente
151
+ else if (evType === 'addtowishlist') score += 8; // favoritou → interesse persistente
152
+
153
+ // 6. Proximidade ao imóvel físico (0–15) — apenas quando distância calculada
154
+ const distKm = parseFloat(String(payload.distanceKm || (payload as any).user_distance_km || '-1'));
155
+ if (distKm >= 0) {
156
+ if (distKm < 5) score += 15;
157
+ else if (distKm < 15) score += 10;
158
+ else if (distKm < 30) score += 6;
159
+ else if (distKm < 60) score += 3;
160
+ // > 60km: sem bônus — lead distante precisa de argumento diferente
161
+ } else if (payload.property_lat || (payload as any).propertyLat) {
162
+ // Coords no payload mas distância não resolvida: pequeno bônus por intenção de rota
163
+ score += 3;
164
+ }
165
+
166
+ score = Math.min(100, score);
167
+
168
+ let ltvClass: string;
169
+ let ltvMultiplier: number;
170
+ if (score >= 70) {
171
+ ltvClass = 'High'; ltvMultiplier = 3.5;
172
+ } else if (score >= 40) {
173
+ ltvClass = 'Medium'; ltvMultiplier = 1.8;
174
+ } else {
175
+ ltvClass = 'Low'; ltvMultiplier = 0.8;
176
+ }
177
+
178
+ const productValue = payload.value ? parseFloat(String(payload.value)) : 0;
179
+ const baseValue = productValue > 0 ? productValue : 197;
180
+ const predictedValue = Math.round(baseValue * ltvMultiplier * 100) / 100;
181
+
182
+ // Enriquecimento opcional via Workers AI
183
+ let aiAdjustment = 0;
184
+ if (env.AI && score >= 40) {
185
+ try {
186
+ const isRealEstate = !!(payload.property_lat || (payload as any).propertyLat);
187
+ const systemContent = customSystemPrompt ||
188
+ (isRealEstate
189
+ ? REAL_ESTATE_PROMPT
190
+ : 'You are a conversion rate expert. Reply ONLY with a JSON object {"adjustment": <number between -10 and 10>} based on the lead data provided. No explanation.');
191
+
192
+ const userContext: Record<string, any> = {
193
+ utm_source: payload.utmSource,
194
+ intention: intentionLevel,
195
+ engagement: engScore,
196
+ hour_utc: hour,
197
+ country,
198
+ has_email: !!payload.email,
199
+ has_phone: !!payload.phone,
200
+ };
201
+ if (isRealEstate) {
202
+ userContext.event_type = 'real_estate_schedule';
203
+ userContext.distance_km = payload.distanceKm || (payload as any).user_distance_km || 'unknown';
204
+ userContext.distance_bucket = payload.distanceBucket || 'unknown';
205
+ userContext.scroll_score = payload.scrollScore || (payload as any).scroll_score || 0;
206
+ userContext.time_level = payload.timeLevel || (payload as any).timeLevel || 'unknown';
207
+ userContext.intent_score = payload.intentScoreNum || payload.intent_score || 'high';
208
+ userContext.hour_brt = (hour - 3 + 24) % 24; // UTC-3 aproximado
209
+ }
210
+
211
+ const prompt = [
212
+ { role: 'system', content: systemContent },
213
+ { role: 'user', content: JSON.stringify(userContext) },
214
+ ];
215
+ const aiRes = await env.AI.run('@cf/ibm-granite/granite-4.0-h-micro', { messages: prompt, max_tokens: 32 });
216
+ const parsed = JSON.parse((aiRes as any).response.trim());
217
+ if (typeof parsed.adjustment === 'number') {
218
+ aiAdjustment = Math.max(-10, Math.min(10, parsed.adjustment));
219
+ }
220
+ } catch { /* graceful fallback */ }
221
+ }
222
+
223
+ return {
224
+ score: Math.min(100, Math.max(0, score + aiAdjustment)),
225
+ class: ltvClass,
226
+ value: predictedValue,
227
+ };
228
+ }
229
+
230
+ // ── getLtvAbVariation — busca variação ativa do A/B test ─────────────────────
231
+ export async function getLtvAbVariation(env: Env): Promise<AbVariation | null> {
232
+ if (!env.DB) return null;
233
+
234
+ try {
235
+ let testData: any = null;
236
+ if (env.GEO_CACHE) {
237
+ const cached = await env.GEO_CACHE.get(AB_LTV_CACHE_KEY, 'json') as any;
238
+ if (cached) testData = cached;
239
+ }
240
+
241
+ if (!testData) {
242
+ const test = await env.DB.prepare(`
243
+ SELECT t.id AS test_id, v.id AS variation_id,
244
+ v.name, v.system_prompt, v.weight, v.is_control
245
+ FROM ltv_ab_tests t
246
+ JOIN ltv_ab_variations v ON v.test_id = t.id
247
+ WHERE t.status = 'running'
248
+ ORDER BY t.id DESC
249
+ `).all();
250
+
251
+ if (!test.results || test.results.length === 0) {
252
+ if (env.GEO_CACHE) await env.GEO_CACHE.put(AB_LTV_CACHE_KEY, JSON.stringify(null), { expirationTtl: 300 });
253
+ return null;
254
+ }
255
+
256
+ testData = test.results;
257
+ if (env.GEO_CACHE) await env.GEO_CACHE.put(AB_LTV_CACHE_KEY, JSON.stringify(testData), { expirationTtl: 300 });
258
+ }
259
+
260
+ if (!testData || testData.length === 0) return null;
261
+
262
+ const totalWeight = testData.reduce((s: number, v: AbVariation) => s + (v.weight || 0.5), 0);
263
+ let rand = Math.random() * totalWeight;
264
+ for (const variation of testData) {
265
+ rand -= (variation.weight || 0.5);
266
+ if (rand <= 0) return variation;
267
+ }
268
+ return testData[testData.length - 1];
269
+
270
+ } catch (err: any) {
271
+ console.error('[AB-LTV] getLtvAbVariation error:', err?.message || String(err));
272
+ return null;
273
+ }
274
+ }
275
+
276
+ // ── recordAbAssignment — registra variação usada para um lead ─────────────────
277
+ export async function recordAbAssignment(env: Env, userId: string, variationId: number, testId: number, predictedLtv: number | null, predictedClass: string | null, emailHash: string | null): Promise<void> {
278
+ if (!env.DB) return;
279
+ try {
280
+ await env.DB.prepare(`
281
+ INSERT INTO ltv_ab_assignments (test_id, variation_id, user_id, email_hash, predicted_ltv, predicted_class, assigned_at)
282
+ VALUES (?, ?, ?, ?, ?, ?, datetime('now'))
283
+ `).bind(testId, variationId, userId, emailHash || null, predictedLtv || null, predictedClass || null).run();
284
+
285
+ await env.DB.prepare(`UPDATE ltv_ab_variations SET total_assigned = total_assigned + 1 WHERE id = ?`).bind(variationId).run();
286
+ } catch (err: any) {
287
+ console.error('[AB-LTV] recordAbAssignment error:', err?.message || String(err));
288
+ }
289
+ }
290
+
291
+ // ── POST /api/ltv/ab-test/create ─────────────────────────────────────────────
292
+ export async function handleLtvAbTestCreate(env: Env, request: Request, headers: Headers): Promise<Response> {
293
+ if (!env.DB) return new Response(JSON.stringify({ error: 'DB não configurado' }), { status: 503, headers });
294
+
295
+ let body: AbTestCreate;
296
+ try { body = await request.json() as AbTestCreate; }
297
+ catch { return new Response(JSON.stringify({ error: 'JSON inválido no body' }), { status: 400, headers }); }
298
+
299
+ const { name, description, min_sample = 100, variations } = body;
300
+ if (!name) return new Response(JSON.stringify({ error: 'name é obrigatório' }), { status: 400, headers });
301
+ if (!Array.isArray(variations) || variations.length < 2) {
302
+ return new Response(JSON.stringify({ error: 'Mínimo 2 variações são necessárias' }), { status: 400, headers });
303
+ }
304
+
305
+ const running = await env.DB.prepare(`SELECT id FROM ltv_ab_tests WHERE status = 'running' LIMIT 1`).first();
306
+ if (running) {
307
+ return new Response(JSON.stringify({ error: 'Já existe um teste em andamento.', running_test_id: (running as any).id }), { status: 409, headers });
308
+ }
309
+
310
+ try {
311
+ const now = new Date().toISOString();
312
+ const totalWeight = variations.reduce((s, v) => s + (v.weight || 0.5), 0);
313
+ if (Math.abs(totalWeight - 1.0) > 0.05) {
314
+ return new Response(JSON.stringify({ error: `A soma dos weights deve ser 1.0. Recebido: ${totalWeight.toFixed(3)}` }), { status: 400, headers });
315
+ }
316
+ if (!variations.some(v => v.is_control)) {
317
+ return new Response(JSON.stringify({ error: 'Pelo menos uma variação deve ter is_control: true' }), { status: 400, headers });
318
+ }
319
+
320
+ const testRes = await env.DB.prepare(`
321
+ INSERT INTO ltv_ab_tests (name, description, status, min_sample, created_at) VALUES (?, ?, 'running', ?, ?)
322
+ `).bind(name, description || null, min_sample, now).run();
323
+
324
+ const testId = (testRes as any).meta?.last_row_id;
325
+ if (!testId) throw new Error('Falha ao criar o teste no D1');
326
+
327
+ const createdVariations: Array<{ id: number; name: string; weight: number; is_control: boolean }> = [];
328
+ for (const v of variations) {
329
+ const vRes = await env.DB.prepare(`
330
+ INSERT INTO ltv_ab_variations (test_id, name, system_prompt, weight, is_control, created_at) VALUES (?, ?, ?, ?, ?, ?)
331
+ `).bind(testId, v.name, v.system_prompt, v.weight || 0.5, v.is_control ? 1 : 0, now).run();
332
+ createdVariations.push({ id: (vRes as any).meta?.last_row_id, name: v.name, weight: v.weight || 0.5, is_control: !!v.is_control });
333
+ }
334
+
335
+ if (env.GEO_CACHE) await env.GEO_CACHE.delete(AB_LTV_CACHE_KEY);
336
+
337
+ return new Response(JSON.stringify({ success: true, test_id: testId, name, status: 'running', min_sample, variations: createdVariations, started_at: now }), { status: 201, headers });
338
+ } catch (err: any) {
339
+ console.error('[AB-LTV] create error:', err?.message || String(err));
340
+ return new Response(JSON.stringify({ error: err?.message || String(err) }), { status: 500, headers });
341
+ }
342
+ }
343
+
344
+ // ── GET /api/ltv/ab-test/list ─────────────────────────────────────────────────
345
+ export async function handleLtvAbTestList(env: Env, request: Request, headers: Headers): Promise<Response> {
346
+ if (!env.DB) return new Response(JSON.stringify({ error: 'DB não configurado' }), { status: 503, headers });
347
+
348
+ const url = new URL(request.url);
349
+ const status = url.searchParams.get('status') || null;
350
+ const limit = Math.min(parseInt(url.searchParams.get('limit') || '20'), 50);
351
+
352
+ try {
353
+ const cond = status ? 'WHERE t.status = ?' : '';
354
+ const bindings: (string | number)[] = status ? [status, limit] : [limit];
355
+
356
+ const tests = await env.DB.prepare(`
357
+ SELECT t.id, t.name, t.description, t.status, t.winner_id,
358
+ t.started_at, t.completed_at, t.created_at, t.min_sample,
359
+ COUNT(DISTINCT v.id) AS variation_count,
360
+ SUM(v.total_assigned) AS total_assigned
361
+ FROM ltv_ab_tests t
362
+ LEFT JOIN ltv_ab_variations v ON v.test_id = t.id
363
+ ${cond}
364
+ GROUP BY t.id
365
+ ORDER BY t.created_at DESC
366
+ LIMIT ?
367
+ `).bind(...bindings).all();
368
+
369
+ return new Response(JSON.stringify({ success: true, total: (tests.results || []).length, tests: tests.results || [] }), { status: 200, headers });
370
+ } catch (err: any) {
371
+ console.error('[AB-LTV] list error:', err?.message || String(err));
372
+ return new Response(JSON.stringify({ error: err?.message || String(err) }), { status: 500, headers });
373
+ }
374
+ }
375
+
376
+ // ── GET /api/ltv/ab-test/results ─────────────────────────────────────────────
377
+ export async function handleLtvAbTestResults(env: Env, request: Request, headers: Headers): Promise<Response> {
378
+ if (!env.DB) return new Response(JSON.stringify({ error: 'DB não configurado' }), { status: 503, headers });
379
+
380
+ const url = new URL(request.url);
381
+ const testId = url.searchParams.get('test_id');
382
+
383
+ try {
384
+ let testRes: any;
385
+
386
+ if (testId) {
387
+ // Query específica para teste por ID
388
+ testRes = await env.DB.prepare(`
389
+ SELECT id, name, status, min_sample, winner_id, started_at
390
+ FROM ltv_ab_tests
391
+ WHERE test_id = ?
392
+ LIMIT 1
393
+ `).bind(parseInt(testId)).first();
394
+ } else {
395
+ // Query padrão: busca teste ativo em execução
396
+ testRes = await env.DB.prepare(`
397
+ SELECT id, name, status, min_sample, winner_id, started_at
398
+ FROM ltv_ab_tests
399
+ WHERE status = 'running'
400
+ LIMIT 1
401
+ `).first();
402
+ }
403
+ if (!testRes) return new Response(JSON.stringify({ error: 'Nenhum teste encontrado' }), { status: 404, headers });
404
+
405
+ const perf = await env.DB.prepare(`SELECT * FROM v_ab_test_performance WHERE test_id = ?`).bind((testRes as any).id).all();
406
+ const variations = perf.results || [];
407
+ const ready = variations.every((v: any) => (v.total_assigned || 0) >= (testRes as any).min_sample);
408
+ let recommendation: any = null;
409
+
410
+ if (ready && variations.length > 0) {
411
+ const best = variations.reduce((a: any, b: any) => (Number(b.accuracy_score) || 0) > (Number(a.accuracy_score) || 0) ? b : a);
412
+ const control = variations.find((v: any) => v.is_control);
413
+ const improvement = control ? ((Number(best.accuracy_score) || 0) - (Number(control.accuracy_score) || 0)) * 100 : null;
414
+ recommendation = {
415
+ winner_variation_id: best.variation_id, winner_variation_name: best.variation_name,
416
+ accuracy_score: best.accuracy_score, improvement_vs_control: improvement ? `+${improvement.toFixed(1)}%` : null,
417
+ ready_to_declare: true,
418
+ };
419
+ }
420
+
421
+ return new Response(JSON.stringify({
422
+ success: true,
423
+ test: { id: (testRes as any).id, name: (testRes as any).name, status: (testRes as any).status, min_sample: (testRes as any).min_sample, started_at: (testRes as any).started_at, is_ready: ready },
424
+ variations, recommendation,
425
+ }), { status: 200, headers });
426
+ } catch (err: any) {
427
+ console.error('[AB-LTV] results error:', err?.message || String(err));
428
+ return new Response(JSON.stringify({ error: err?.message || String(err) }), { status: 500, headers });
429
+ }
430
+ }
431
+
432
+ // ── POST /api/ltv/ab-test/winner ──────────────────────────────────────────────
433
+ export async function handleLtvAbTestWinner(env: Env, request: Request, headers: Headers): Promise<Response> {
434
+ if (!env.DB) return new Response(JSON.stringify({ error: 'DB não configurado' }), { status: 503, headers });
435
+
436
+ let body: any;
437
+ try { body = await request.json(); }
438
+ catch { return new Response(JSON.stringify({ error: 'JSON inválido' }), { status: 400, headers }); }
439
+
440
+ const { test_id, variation_id } = body;
441
+ if (!test_id || !variation_id) {
442
+ return new Response(JSON.stringify({ error: 'test_id e variation_id são obrigatórios' }), { status: 400, headers });
443
+ }
444
+
445
+ try {
446
+ const variation = await env.DB.prepare(`SELECT id, name, system_prompt, is_control FROM ltv_ab_variations WHERE id = ? AND test_id = ?`).bind(variation_id, test_id).first();
447
+ if (!variation) return new Response(JSON.stringify({ error: 'Variação não encontrada para este teste' }), { status: 404, headers });
448
+
449
+ await env.DB.prepare(`UPDATE ltv_ab_tests SET winner_id = ?, status = 'completed', completed_at = datetime('now') WHERE id = ?`).bind(variation_id, test_id).run();
450
+ if (env.GEO_CACHE) await env.GEO_CACHE.delete(AB_LTV_CACHE_KEY);
451
+
452
+ return new Response(JSON.stringify({
453
+ success: true, test_id, winner_variation_id: variation_id, winner_name: (variation as any).name,
454
+ is_control: (variation as any).is_control === 1, winning_prompt: (variation as any).system_prompt,
455
+ message: (variation as any).is_control === 1
456
+ ? 'O prompt original (controle) venceu. Nenhuma alteração necessária.'
457
+ : 'Novo prompt vencedor identificado. Copie o campo winning_prompt e aplique ao predictLtv() como novo default.',
458
+ }), { status: 200, headers });
459
+ } catch (err: any) {
460
+ console.error('[AB-LTV] winner error:', err?.message || String(err));
461
+ return new Response(JSON.stringify({ error: err?.message || String(err) }), { status: 500, headers });
462
+ }
463
+ }
464
+
465
+ // ── autoDecideAbWinner — declara winner automaticamente via cron ──────────────
466
+ // Critério: todas as variações com amostra >= min_sample
467
+ // E diferença de accuracy_score >= 5pp entre melhor e controle
468
+ export async function autoDecideAbWinner(env: Env): Promise<AutoDecideResult> {
469
+ if (!env.DB) return { decided: false, reason: 'no_db' };
470
+
471
+ try {
472
+ // Buscar teste ativo
473
+ const test = await env.DB.prepare(
474
+ `SELECT id, name, min_sample, status FROM ltv_ab_tests WHERE status = 'running' ORDER BY id DESC LIMIT 1`
475
+ ).first();
476
+
477
+ if (!test) return { decided: false, reason: 'no_running_test' };
478
+
479
+ // Buscar performance das variações
480
+ const perf = await env.DB.prepare(
481
+ `SELECT * FROM v_ab_test_performance WHERE test_id = ?`
482
+ ).bind((test as any).id).all();
483
+
484
+ const variations = perf.results || [];
485
+ if (variations.length < 2) return { decided: false, reason: 'insufficient_variations' };
486
+
487
+ // Verificar se todas têm amostra suficiente
488
+ const allReady = variations.every((v: any) => (v.total_assigned || 0) >= (test as any).min_sample);
489
+ if (!allReady) {
490
+ const minAssigned = Math.min(...variations.map((v: any) => v.total_assigned || 0));
491
+ return { decided: false, reason: `sample_insufficient (${minAssigned}/${(test as any).min_sample})` };
492
+ }
493
+
494
+ // Encontrar melhor e controle
495
+ const best = variations.reduce((a: any, b: any) => (b.accuracy_score || 0) > (a.accuracy_score || 0) ? b : a);
496
+ const control = variations.find((v: any) => v.is_control) || variations[0];
497
+
498
+ const bestScore = parseFloat(String(best.accuracy_score) || '0');
499
+ const controlScore = parseFloat(String(control.accuracy_score) || '0');
500
+ const diff = bestScore - controlScore;
501
+
502
+ // Empate técnico → controle vence (determinístico)
503
+ if (diff < 0.05) {
504
+ return { decided: false, reason: `difference_too_small (${(diff * 100).toFixed(1)}pp < 5pp)` };
505
+ }
506
+
507
+ // Declarar winner
508
+ await env.DB.prepare(
509
+ `UPDATE ltv_ab_tests SET winner_id = ?, status = 'completed', completed_at = datetime('now') WHERE id = ?`
510
+ ).bind(best.variation_id, test.id).run();
511
+
512
+ if (env.GEO_CACHE) await env.GEO_CACHE.delete(AB_LTV_CACHE_KEY);
513
+
514
+ console.log(`[AB-LTV] Winner auto-declarado: teste ${test.id}, variação "${best.variation_name}" (+${(diff * 100).toFixed(1)}pp)`);
515
+
516
+ return {
517
+ decided: true,
518
+ test_id: (test as any).id,
519
+ test_name: (test as any).name,
520
+ winner_id: typeof best.variation_id === 'number' ? best.variation_id : undefined,
521
+ winner_name: typeof best.variation_name === 'string' ? best.variation_name : undefined,
522
+ improvement: `+${(diff * 100).toFixed(1)}pp`,
523
+ is_control_winner: best.variation_id === control.variation_id,
524
+ winning_prompt: String(best.system_prompt || ''),
525
+ };
526
+
527
+ } catch (err: any) {
528
+ console.error('[AB-LTV] autoDecide error:', err?.message || String(err));
529
+ return { decided: false, reason: err?.message || String(err) };
530
+ }
531
+ }