cdp-edge 2.2.5 → 2.3.1

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 (24) hide show
  1. package/README.md +57 -2
  2. package/contracts/types.ts +81 -0
  3. package/docs/whatsapp-ctwa.md +3 -2
  4. package/package.json +7 -4
  5. package/server-edge-tracker/.client.env.example +14 -0
  6. package/server-edge-tracker/deploy-client.js +76 -0
  7. package/server-edge-tracker/{index.js → index.ts} +93 -84
  8. package/server-edge-tracker/modules/{db.js → db.ts} +117 -77
  9. package/server-edge-tracker/modules/dispatch/{ga4.js → ga4.ts} +12 -10
  10. package/server-edge-tracker/modules/dispatch/{meta.js → meta.ts} +35 -28
  11. package/server-edge-tracker/modules/dispatch/{platforms.js → platforms.ts} +58 -56
  12. package/server-edge-tracker/modules/dispatch/{tiktok.js → tiktok.ts} +22 -20
  13. package/server-edge-tracker/modules/dispatch/{whatsapp.js → whatsapp.ts} +74 -28
  14. package/server-edge-tracker/modules/{intelligence.js → intelligence.ts} +175 -60
  15. package/server-edge-tracker/modules/ml/{bidding.js → bidding.ts} +37 -35
  16. package/server-edge-tracker/modules/ml/{fraud.js → fraud.ts} +48 -40
  17. package/server-edge-tracker/modules/ml/{logistic.js → logistic.ts} +44 -19
  18. package/server-edge-tracker/modules/ml/{ltv.js → ltv.ts} +135 -90
  19. package/server-edge-tracker/modules/ml/{matchquality.js → matchquality.ts} +70 -26
  20. package/server-edge-tracker/modules/ml/{segmentation.js → segmentation.ts} +109 -48
  21. package/server-edge-tracker/modules/{utils.js → utils.ts} +41 -22
  22. package/server-edge-tracker/types.ts +255 -0
  23. package/server-edge-tracker/wrangler.toml +2 -2
  24. package/docs/PixelBuilder-Documentacao-Completa (2).docx +0 -0
@@ -4,11 +4,55 @@
4
4
  */
5
5
 
6
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
+ }
7
51
 
8
52
  // Cache key para o teste ativo (KV — evita hit no D1 a cada request /track)
9
53
  const AB_LTV_CACHE_KEY = 'ab_ltv_active_test';
10
54
 
11
- // ── Prompt especializado para imóveis ────────────────────────────────────────
55
+ // ── Prompt especializado para imóveis ───────────────────────────────────────
12
56
  // Ativado automaticamente quando property_lat/lng estão presentes no payload.
13
57
  // Override por A/B test tem prioridade sobre este prompt.
14
58
  const REAL_ESTATE_PROMPT = `You are a real estate lead scoring expert for the Brazilian market.
@@ -26,7 +70,7 @@ Scoring rules (apply additively):
26
70
  No explanation. JSON only.`;
27
71
 
28
72
  // ── predictLtv — Heurística em 5 dimensões (0-100 pts) ───────────────────────
29
- export async function predictLtv(env, payload, request, customSystemPrompt = null) {
73
+ export async function predictLtv(env: Env, payload: TrackPayload, request: Request | null, customSystemPrompt: string | null = null): Promise<LtvResult> {
30
74
  // ── Tentar modelo treinado (regressão logística real) ─────────────────────
31
75
  // Se existir modelo ativo no KV/D1, usa-o em vez da heurística manual.
32
76
  // Fallback automático para heurística se modelo não disponível.
@@ -34,10 +78,10 @@ export async function predictLtv(env, payload, request, customSystemPrompt = nul
34
78
  const model = await loadActiveWeights(env);
35
79
  if (model?.weights?.length) {
36
80
  const hour = new Date().getUTCHours();
37
- const country = (payload.country || request?.cf?.country || '').toUpperCase();
81
+ const country = (payload.country || (request as any)?.cf?.country || '').toUpperCase();
38
82
  const features = extractFeatures({
39
83
  utm_source: payload.utmSource,
40
- engagement_score: parseFloat(payload.engagementScore || 0),
84
+ engagement_score: parseFloat(String(payload.engagementScore || '0')),
41
85
  intention_level: payload.intentionLevel,
42
86
  days_since_lead: 0, // evento atual = recência máxima
43
87
  has_email: !!payload.email,
@@ -49,7 +93,7 @@ export async function predictLtv(env, payload, request, customSystemPrompt = nul
49
93
  const score100 = predictWithWeights(model, features);
50
94
  const ltvClass = score100 >= 70 ? 'High' : score100 >= 40 ? 'Medium' : 'Low';
51
95
  const ltvMultiplier = score100 >= 70 ? 3.5 : score100 >= 40 ? 1.8 : 0.8;
52
- const productValue = payload.value ? parseFloat(payload.value) : 0;
96
+ const productValue = payload.value ? parseFloat(String(payload.value)) : 0;
53
97
  const baseValue = productValue > 0 ? productValue : 197;
54
98
  const predictedValue = Math.round(baseValue * ltvMultiplier * 100) / 100;
55
99
 
@@ -60,14 +104,14 @@ export async function predictLtv(env, payload, request, customSystemPrompt = nul
60
104
  let score = 0;
61
105
 
62
106
  // 1. Engajamento browser (0–30)
63
- const engScore = parseFloat(payload.engagementScore || 0);
64
- const userScore = parseFloat(payload.userScore || 0);
107
+ const engScore = parseFloat(String(payload.engagementScore || '0'));
108
+ const userScore = parseFloat(String((payload as any).userScore || '0'));
65
109
  score += Math.min(15, Math.round((engScore / 5) * 15));
66
110
  score += Math.min(15, Math.round((userScore / 100) * 15));
67
111
 
68
112
  // 2. Origem de tráfego (0–25)
69
113
  const src = (payload.utmSource || '').toLowerCase();
70
- const utm_score_map = {
114
+ const utm_score_map: Record<string, number> = {
71
115
  facebook: 25, instagram: 25, meta: 25,
72
116
  google: 22, youtube: 22, tiktok: 20,
73
117
  email: 18, sms: 18,
@@ -77,8 +121,8 @@ export async function predictLtv(env, payload, request, customSystemPrompt = nul
77
121
 
78
122
  // 3. Contexto de rede (0–15)
79
123
  const hour = new Date().getUTCHours();
80
- const country = (payload.country || request.cf?.country || '').toUpperCase();
81
- const org = String(request.cf?.asOrganization || '').toLowerCase();
124
+ const country = (payload.country || (request as any)?.cf?.country || '').toUpperCase();
125
+ const org = String((request as any).cf?.asOrganization || '').toLowerCase();
82
126
 
83
127
  const isHighConvTime = hour >= 21 || hour <= 2;
84
128
  score += isHighConvTime ? 8 : (hour >= 12 && hour <= 20 ? 4 : 1);
@@ -101,27 +145,28 @@ export async function predictLtv(env, payload, request, customSystemPrompt = nul
101
145
  if (payload.firstName) score += 2;
102
146
 
103
147
  // 5b. Tipo de evento imobiliário (0–15) — sinais de intenção de compra física
104
- const evType = (payload.eventType || '').toLowerCase();
148
+ const evType = ((payload as any).eventType || '').toLowerCase();
105
149
  if (evType === 'customizeproduct') score += 15; // simulação de financiamento → intenção máxima
106
150
  else if (evType === 'findlocation') score += 10; // viu mapa/localização → visita física iminente
107
151
  else if (evType === 'addtowishlist') score += 8; // favoritou → interesse persistente
108
152
 
109
153
  // 6. Proximidade ao imóvel físico (0–15) — apenas quando distância calculada
110
- const distKm = parseFloat(payload.distanceKm ?? payload.user_distance_km ?? -1);
154
+ const distKm = parseFloat(String(payload.distanceKm || (payload as any).user_distance_km || '-1'));
111
155
  if (distKm >= 0) {
112
- if (distKm < 5) score += 15;
113
- else if (distKm < 15) score += 10;
114
- else if (distKm < 30) score += 6;
115
- else if (distKm < 60) score += 3;
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;
116
160
  // > 60km: sem bônus — lead distante precisa de argumento diferente
117
- } else if (payload.property_lat || payload.propertyLat) {
161
+ } else if (payload.property_lat || (payload as any).propertyLat) {
118
162
  // Coords no payload mas distância não resolvida: pequeno bônus por intenção de rota
119
163
  score += 3;
120
164
  }
121
165
 
122
166
  score = Math.min(100, score);
123
167
 
124
- let ltvClass, ltvMultiplier;
168
+ let ltvClass: string;
169
+ let ltvMultiplier: number;
125
170
  if (score >= 70) {
126
171
  ltvClass = 'High'; ltvMultiplier = 3.5;
127
172
  } else if (score >= 40) {
@@ -130,7 +175,7 @@ export async function predictLtv(env, payload, request, customSystemPrompt = nul
130
175
  ltvClass = 'Low'; ltvMultiplier = 0.8;
131
176
  }
132
177
 
133
- const productValue = payload.value ? parseFloat(payload.value) : 0;
178
+ const productValue = payload.value ? parseFloat(String(payload.value)) : 0;
134
179
  const baseValue = productValue > 0 ? productValue : 197;
135
180
  const predictedValue = Math.round(baseValue * ltvMultiplier * 100) / 100;
136
181
 
@@ -138,16 +183,16 @@ export async function predictLtv(env, payload, request, customSystemPrompt = nul
138
183
  let aiAdjustment = 0;
139
184
  if (env.AI && score >= 40) {
140
185
  try {
141
- const isRealEstate = !!(payload.property_lat || payload.propertyLat);
186
+ const isRealEstate = !!(payload.property_lat || (payload as any).propertyLat);
142
187
  const systemContent = customSystemPrompt ||
143
188
  (isRealEstate
144
189
  ? REAL_ESTATE_PROMPT
145
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.');
146
191
 
147
- const userContext = {
148
- utm_source: payload.utmSource,
149
- intention: intentionLevel,
150
- engagement: engScore,
192
+ const userContext: Record<string, any> = {
193
+ utm_source: payload.utmSource,
194
+ intention: intentionLevel,
195
+ engagement: engScore,
151
196
  hour_utc: hour,
152
197
  country,
153
198
  has_email: !!payload.email,
@@ -155,11 +200,11 @@ export async function predictLtv(env, payload, request, customSystemPrompt = nul
155
200
  };
156
201
  if (isRealEstate) {
157
202
  userContext.event_type = 'real_estate_schedule';
158
- userContext.distance_km = payload.distanceKm ?? payload.user_distance_km ?? 'unknown';
159
- userContext.distance_bucket = payload.distanceBucket ?? 'unknown';
160
- userContext.scroll_score = payload.scrollScore || payload.scroll_score || 0;
161
- userContext.time_level = payload.timeLevel || payload.time_level || 'unknown';
162
- userContext.intent_score = payload.intent_score || 'high';
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';
163
208
  userContext.hour_brt = (hour - 3 + 24) % 24; // UTC-3 aproximado
164
209
  }
165
210
 
@@ -168,7 +213,7 @@ export async function predictLtv(env, payload, request, customSystemPrompt = nul
168
213
  { role: 'user', content: JSON.stringify(userContext) },
169
214
  ];
170
215
  const aiRes = await env.AI.run('@cf/ibm-granite/granite-4.0-h-micro', { messages: prompt, max_tokens: 32 });
171
- const parsed = JSON.parse(aiRes.response.trim());
216
+ const parsed = JSON.parse((aiRes as any).response.trim());
172
217
  if (typeof parsed.adjustment === 'number') {
173
218
  aiAdjustment = Math.max(-10, Math.min(10, parsed.adjustment));
174
219
  }
@@ -183,13 +228,13 @@ export async function predictLtv(env, payload, request, customSystemPrompt = nul
183
228
  }
184
229
 
185
230
  // ── getLtvAbVariation — busca variação ativa do A/B test ─────────────────────
186
- export async function getLtvAbVariation(env) {
231
+ export async function getLtvAbVariation(env: Env): Promise<AbVariation | null> {
187
232
  if (!env.DB) return null;
188
233
 
189
234
  try {
190
- let testData = null;
235
+ let testData: any = null;
191
236
  if (env.GEO_CACHE) {
192
- const cached = await env.GEO_CACHE.get(AB_LTV_CACHE_KEY, 'json');
237
+ const cached = await env.GEO_CACHE.get(AB_LTV_CACHE_KEY, 'json') as any;
193
238
  if (cached) testData = cached;
194
239
  }
195
240
 
@@ -214,7 +259,7 @@ export async function getLtvAbVariation(env) {
214
259
 
215
260
  if (!testData || testData.length === 0) return null;
216
261
 
217
- const totalWeight = testData.reduce((s, v) => s + (v.weight || 0.5), 0);
262
+ const totalWeight = testData.reduce((s: number, v: AbVariation) => s + (v.weight || 0.5), 0);
218
263
  let rand = Math.random() * totalWeight;
219
264
  for (const variation of testData) {
220
265
  rand -= (variation.weight || 0.5);
@@ -222,14 +267,14 @@ export async function getLtvAbVariation(env) {
222
267
  }
223
268
  return testData[testData.length - 1];
224
269
 
225
- } catch (err) {
226
- console.error('[AB-LTV] getLtvAbVariation error:', err.message);
270
+ } catch (err: any) {
271
+ console.error('[AB-LTV] getLtvAbVariation error:', err?.message || String(err));
227
272
  return null;
228
273
  }
229
274
  }
230
275
 
231
276
  // ── recordAbAssignment — registra variação usada para um lead ─────────────────
232
- export async function recordAbAssignment(env, userId, variationId, testId, predictedLtv, predictedClass, emailHash) {
277
+ export async function recordAbAssignment(env: Env, userId: string, variationId: number, testId: number, predictedLtv: number | null, predictedClass: string | null, emailHash: string | null): Promise<void> {
233
278
  if (!env.DB) return;
234
279
  try {
235
280
  await env.DB.prepare(`
@@ -238,17 +283,17 @@ export async function recordAbAssignment(env, userId, variationId, testId, predi
238
283
  `).bind(testId, variationId, userId, emailHash || null, predictedLtv || null, predictedClass || null).run();
239
284
 
240
285
  await env.DB.prepare(`UPDATE ltv_ab_variations SET total_assigned = total_assigned + 1 WHERE id = ?`).bind(variationId).run();
241
- } catch (err) {
242
- console.error('[AB-LTV] recordAbAssignment error:', err.message);
286
+ } catch (err: any) {
287
+ console.error('[AB-LTV] recordAbAssignment error:', err?.message || String(err));
243
288
  }
244
289
  }
245
290
 
246
291
  // ── POST /api/ltv/ab-test/create ─────────────────────────────────────────────
247
- export async function handleLtvAbTestCreate(env, request, headers) {
292
+ export async function handleLtvAbTestCreate(env: Env, request: Request, headers: Headers): Promise<Response> {
248
293
  if (!env.DB) return new Response(JSON.stringify({ error: 'DB não configurado' }), { status: 503, headers });
249
294
 
250
- let body;
251
- try { body = await request.json(); }
295
+ let body: AbTestCreate;
296
+ try { body = await request.json() as AbTestCreate; }
252
297
  catch { return new Response(JSON.stringify({ error: 'JSON inválido no body' }), { status: 400, headers }); }
253
298
 
254
299
  const { name, description, min_sample = 100, variations } = body;
@@ -259,7 +304,7 @@ export async function handleLtvAbTestCreate(env, request, headers) {
259
304
 
260
305
  const running = await env.DB.prepare(`SELECT id FROM ltv_ab_tests WHERE status = 'running' LIMIT 1`).first();
261
306
  if (running) {
262
- return new Response(JSON.stringify({ error: 'Já existe um teste em andamento.', running_test_id: running.id }), { status: 409, headers });
307
+ return new Response(JSON.stringify({ error: 'Já existe um teste em andamento.', running_test_id: (running as any).id }), { status: 409, headers });
263
308
  }
264
309
 
265
310
  try {
@@ -276,28 +321,28 @@ export async function handleLtvAbTestCreate(env, request, headers) {
276
321
  INSERT INTO ltv_ab_tests (name, description, status, min_sample, created_at) VALUES (?, ?, 'running', ?, ?)
277
322
  `).bind(name, description || null, min_sample, now).run();
278
323
 
279
- const testId = testRes.meta?.last_row_id;
324
+ const testId = (testRes as any).meta?.last_row_id;
280
325
  if (!testId) throw new Error('Falha ao criar o teste no D1');
281
326
 
282
- const createdVariations = [];
327
+ const createdVariations: Array<{ id: number; name: string; weight: number; is_control: boolean }> = [];
283
328
  for (const v of variations) {
284
329
  const vRes = await env.DB.prepare(`
285
330
  INSERT INTO ltv_ab_variations (test_id, name, system_prompt, weight, is_control, created_at) VALUES (?, ?, ?, ?, ?, ?)
286
331
  `).bind(testId, v.name, v.system_prompt, v.weight || 0.5, v.is_control ? 1 : 0, now).run();
287
- createdVariations.push({ id: vRes.meta?.last_row_id, name: v.name, weight: v.weight, is_control: !!v.is_control });
332
+ createdVariations.push({ id: (vRes as any).meta?.last_row_id, name: v.name, weight: v.weight || 0.5, is_control: !!v.is_control });
288
333
  }
289
334
 
290
335
  if (env.GEO_CACHE) await env.GEO_CACHE.delete(AB_LTV_CACHE_KEY);
291
336
 
292
337
  return new Response(JSON.stringify({ success: true, test_id: testId, name, status: 'running', min_sample, variations: createdVariations, started_at: now }), { status: 201, headers });
293
- } catch (err) {
294
- console.error('[AB-LTV] create error:', err.message);
295
- return new Response(JSON.stringify({ error: err.message }), { status: 500, 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 });
296
341
  }
297
342
  }
298
343
 
299
344
  // ── GET /api/ltv/ab-test/list ─────────────────────────────────────────────────
300
- export async function handleLtvAbTestList(env, request, headers) {
345
+ export async function handleLtvAbTestList(env: Env, request: Request, headers: Headers): Promise<Response> {
301
346
  if (!env.DB) return new Response(JSON.stringify({ error: 'DB não configurado' }), { status: 503, headers });
302
347
 
303
348
  const url = new URL(request.url);
@@ -306,7 +351,7 @@ export async function handleLtvAbTestList(env, request, headers) {
306
351
 
307
352
  try {
308
353
  const cond = status ? 'WHERE t.status = ?' : '';
309
- const bindings = status ? [status, limit] : [limit];
354
+ const bindings: (string | number)[] = status ? [status, limit] : [limit];
310
355
 
311
356
  const tests = await env.DB.prepare(`
312
357
  SELECT t.id, t.name, t.description, t.status, t.winner_id,
@@ -322,14 +367,14 @@ export async function handleLtvAbTestList(env, request, headers) {
322
367
  `).bind(...bindings).all();
323
368
 
324
369
  return new Response(JSON.stringify({ success: true, total: (tests.results || []).length, tests: tests.results || [] }), { status: 200, headers });
325
- } catch (err) {
326
- console.error('[AB-LTV] list error:', err.message);
327
- return new Response(JSON.stringify({ error: err.message }), { status: 500, 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 });
328
373
  }
329
374
  }
330
375
 
331
376
  // ── GET /api/ltv/ab-test/results ─────────────────────────────────────────────
332
- export async function handleLtvAbTestResults(env, request, headers) {
377
+ export async function handleLtvAbTestResults(env: Env, request: Request, headers: Headers): Promise<Response> {
333
378
  if (!env.DB) return new Response(JSON.stringify({ error: 'DB não configurado' }), { status: 503, headers });
334
379
 
335
380
  const url = new URL(request.url);
@@ -342,15 +387,15 @@ export async function handleLtvAbTestResults(env, request, headers) {
342
387
  const testRes = await env.DB.prepare(`SELECT id, name, status, min_sample, winner_id, started_at FROM ltv_ab_tests ${cond} LIMIT 1`).bind(...testBind).first();
343
388
  if (!testRes) return new Response(JSON.stringify({ error: 'Nenhum teste encontrado' }), { status: 404, headers });
344
389
 
345
- const perf = await env.DB.prepare(`SELECT * FROM v_ab_test_performance WHERE test_id = ?`).bind(testRes.id).all();
390
+ const perf = await env.DB.prepare(`SELECT * FROM v_ab_test_performance WHERE test_id = ?`).bind((testRes as any).id).all();
346
391
  const variations = perf.results || [];
347
- const ready = variations.every(v => (v.total_assigned || 0) >= testRes.min_sample);
348
- let recommendation = null;
392
+ const ready = variations.every((v: any) => (v.total_assigned || 0) >= (testRes as any).min_sample);
393
+ let recommendation: any = null;
349
394
 
350
395
  if (ready && variations.length > 0) {
351
- const best = variations.reduce((a, b) => (b.accuracy_score || 0) > (a.accuracy_score || 0) ? b : a);
352
- const control = variations.find(v => v.is_control);
353
- const improvement = control ? ((best.accuracy_score || 0) - (control.accuracy_score || 0)) * 100 : null;
396
+ const best = variations.reduce((a: any, b: any) => (Number(b.accuracy_score) || 0) > (Number(a.accuracy_score) || 0) ? b : a);
397
+ const control = variations.find((v: any) => v.is_control);
398
+ const improvement = control ? ((Number(best.accuracy_score) || 0) - (Number(control.accuracy_score) || 0)) * 100 : null;
354
399
  recommendation = {
355
400
  winner_variation_id: best.variation_id, winner_variation_name: best.variation_name,
356
401
  accuracy_score: best.accuracy_score, improvement_vs_control: improvement ? `+${improvement.toFixed(1)}%` : null,
@@ -360,20 +405,20 @@ export async function handleLtvAbTestResults(env, request, headers) {
360
405
 
361
406
  return new Response(JSON.stringify({
362
407
  success: true,
363
- test: { id: testRes.id, name: testRes.name, status: testRes.status, min_sample: testRes.min_sample, started_at: testRes.started_at, is_ready: ready },
408
+ 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 },
364
409
  variations, recommendation,
365
410
  }), { status: 200, headers });
366
- } catch (err) {
367
- console.error('[AB-LTV] results error:', err.message);
368
- return new Response(JSON.stringify({ error: err.message }), { status: 500, headers });
411
+ } catch (err: any) {
412
+ console.error('[AB-LTV] results error:', err?.message || String(err));
413
+ return new Response(JSON.stringify({ error: err?.message || String(err) }), { status: 500, headers });
369
414
  }
370
415
  }
371
416
 
372
417
  // ── POST /api/ltv/ab-test/winner ──────────────────────────────────────────────
373
- export async function handleLtvAbTestWinner(env, request, headers) {
418
+ export async function handleLtvAbTestWinner(env: Env, request: Request, headers: Headers): Promise<Response> {
374
419
  if (!env.DB) return new Response(JSON.stringify({ error: 'DB não configurado' }), { status: 503, headers });
375
420
 
376
- let body;
421
+ let body: any;
377
422
  try { body = await request.json(); }
378
423
  catch { return new Response(JSON.stringify({ error: 'JSON inválido' }), { status: 400, headers }); }
379
424
 
@@ -390,22 +435,22 @@ export async function handleLtvAbTestWinner(env, request, headers) {
390
435
  if (env.GEO_CACHE) await env.GEO_CACHE.delete(AB_LTV_CACHE_KEY);
391
436
 
392
437
  return new Response(JSON.stringify({
393
- success: true, test_id, winner_variation_id: variation_id, winner_name: variation.name,
394
- is_control: variation.is_control === 1, winning_prompt: variation.system_prompt,
395
- message: variation.is_control === 1
438
+ success: true, test_id, winner_variation_id: variation_id, winner_name: (variation as any).name,
439
+ is_control: (variation as any).is_control === 1, winning_prompt: (variation as any).system_prompt,
440
+ message: (variation as any).is_control === 1
396
441
  ? 'O prompt original (controle) venceu. Nenhuma alteração necessária.'
397
442
  : 'Novo prompt vencedor identificado. Copie o campo winning_prompt e aplique ao predictLtv() como novo default.',
398
443
  }), { status: 200, headers });
399
- } catch (err) {
400
- console.error('[AB-LTV] winner error:', err.message);
401
- return new Response(JSON.stringify({ error: err.message }), { status: 500, headers });
444
+ } catch (err: any) {
445
+ console.error('[AB-LTV] winner error:', err?.message || String(err));
446
+ return new Response(JSON.stringify({ error: err?.message || String(err) }), { status: 500, headers });
402
447
  }
403
448
  }
404
449
 
405
450
  // ── autoDecideAbWinner — declara winner automaticamente via cron ──────────────
406
451
  // Critério: todas as variações com amostra >= min_sample
407
452
  // E diferença de accuracy_score >= 5pp entre melhor e controle
408
- export async function autoDecideAbWinner(env) {
453
+ export async function autoDecideAbWinner(env: Env): Promise<AutoDecideResult> {
409
454
  if (!env.DB) return { decided: false, reason: 'no_db' };
410
455
 
411
456
  try {
@@ -419,24 +464,24 @@ export async function autoDecideAbWinner(env) {
419
464
  // Buscar performance das variações
420
465
  const perf = await env.DB.prepare(
421
466
  `SELECT * FROM v_ab_test_performance WHERE test_id = ?`
422
- ).bind(test.id).all();
467
+ ).bind((test as any).id).all();
423
468
 
424
469
  const variations = perf.results || [];
425
470
  if (variations.length < 2) return { decided: false, reason: 'insufficient_variations' };
426
471
 
427
472
  // Verificar se todas têm amostra suficiente
428
- const allReady = variations.every(v => (v.total_assigned || 0) >= test.min_sample);
473
+ const allReady = variations.every((v: any) => (v.total_assigned || 0) >= (test as any).min_sample);
429
474
  if (!allReady) {
430
- const minAssigned = Math.min(...variations.map(v => v.total_assigned || 0));
431
- return { decided: false, reason: `sample_insufficient (${minAssigned}/${test.min_sample})` };
475
+ const minAssigned = Math.min(...variations.map((v: any) => v.total_assigned || 0));
476
+ return { decided: false, reason: `sample_insufficient (${minAssigned}/${(test as any).min_sample})` };
432
477
  }
433
478
 
434
479
  // Encontrar melhor e controle
435
- const best = variations.reduce((a, b) => (b.accuracy_score || 0) > (a.accuracy_score || 0) ? b : a);
436
- const control = variations.find(v => v.is_control) || variations[0];
480
+ const best = variations.reduce((a: any, b: any) => (b.accuracy_score || 0) > (a.accuracy_score || 0) ? b : a);
481
+ const control = variations.find((v: any) => v.is_control) || variations[0];
437
482
 
438
- const bestScore = parseFloat(best.accuracy_score || 0);
439
- const controlScore = parseFloat(control.accuracy_score || 0);
483
+ const bestScore = parseFloat(String(best.accuracy_score) || '0');
484
+ const controlScore = parseFloat(String(control.accuracy_score) || '0');
440
485
  const diff = bestScore - controlScore;
441
486
 
442
487
  // Empate técnico → controle vence (determinístico)
@@ -455,17 +500,17 @@ export async function autoDecideAbWinner(env) {
455
500
 
456
501
  return {
457
502
  decided: true,
458
- test_id: test.id,
459
- test_name: test.name,
460
- winner_id: best.variation_id,
461
- winner_name: best.variation_name,
503
+ test_id: (test as any).id,
504
+ test_name: (test as any).name,
505
+ winner_id: typeof best.variation_id === 'number' ? best.variation_id : undefined,
506
+ winner_name: typeof best.variation_name === 'string' ? best.variation_name : undefined,
462
507
  improvement: `+${(diff * 100).toFixed(1)}pp`,
463
508
  is_control_winner: best.variation_id === control.variation_id,
464
- winning_prompt: best.system_prompt || null,
509
+ winning_prompt: String(best.system_prompt || ''),
465
510
  };
466
511
 
467
- } catch (err) {
468
- console.error('[AB-LTV] autoDecide error:', err.message);
469
- return { decided: false, reason: err.message };
512
+ } catch (err: any) {
513
+ console.error('[AB-LTV] autoDecide error:', err?.message || String(err));
514
+ return { decided: false, reason: err?.message || String(err) };
470
515
  }
471
516
  }