cdp-edge 1.23.3 → 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.
- package/README.md +39 -2
- package/bin/cdp-edge.js +10 -1
- package/contracts/types.ts +81 -0
- package/dist/commands/install.js +6 -1
- package/docs/whatsapp-ctwa.md +3 -2
- package/package.json +7 -4
- package/server-edge-tracker/{index.js → index.ts} +91 -82
- package/server-edge-tracker/modules/{db.js → db.ts} +116 -76
- package/server-edge-tracker/modules/dispatch/{ga4.js → ga4.ts} +12 -10
- package/server-edge-tracker/modules/dispatch/{meta.js → meta.ts} +35 -28
- package/server-edge-tracker/modules/dispatch/{platforms.js → platforms.ts} +58 -56
- package/server-edge-tracker/modules/dispatch/{tiktok.js → tiktok.ts} +22 -20
- package/server-edge-tracker/modules/dispatch/{whatsapp.js → whatsapp.ts} +59 -25
- package/server-edge-tracker/modules/{intelligence.js → intelligence.ts} +175 -60
- package/server-edge-tracker/modules/ml/{bidding.js → bidding.ts} +37 -35
- package/server-edge-tracker/modules/ml/{fraud.js → fraud.ts} +48 -40
- package/server-edge-tracker/modules/ml/{logistic.js → logistic.ts} +44 -19
- package/server-edge-tracker/modules/ml/{ltv.js → ltv.ts} +135 -90
- package/server-edge-tracker/modules/ml/{matchquality.js → matchquality.ts} +70 -26
- package/server-edge-tracker/modules/ml/{segmentation.js → segmentation.ts} +109 -48
- package/server-edge-tracker/modules/{utils.js → utils.ts} +41 -22
- package/server-edge-tracker/types.ts +251 -0
- package/server-edge-tracker/wrangler.toml +8 -8
- package/docs/PixelBuilder-Documentacao-Completa (2).docx +0 -0
|
@@ -9,9 +9,73 @@ import { sendCallMeBot } from './dispatch/whatsapp.js';
|
|
|
9
9
|
import { autoDecideAbWinner } from './ml/ltv.js';
|
|
10
10
|
import { analyzeMatchQuality, alertMatchQuality, purgeOldMatchQualityLogs } from './ml/matchquality.js';
|
|
11
11
|
import { trainLogisticRegression, extractFeatures, saveWeights, LTV_WEIGHTS_KV_KEY } from './ml/logistic.js';
|
|
12
|
+
import { Env } from '../types.js';
|
|
13
|
+
|
|
14
|
+
// ── Tipos ───────────────────────────────────────────────────────────────────────
|
|
15
|
+
export interface ApiVersionCheck {
|
|
16
|
+
platform: string;
|
|
17
|
+
current: string;
|
|
18
|
+
expected: string;
|
|
19
|
+
status: 'ok' | 'warning';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ErrorRateAlert {
|
|
23
|
+
platform: string;
|
|
24
|
+
errorRate: number;
|
|
25
|
+
status: 'ok' | 'warning' | 'critical';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface LtvTrainResult {
|
|
29
|
+
trained?: boolean;
|
|
30
|
+
skipped?: string;
|
|
31
|
+
samples?: number;
|
|
32
|
+
accuracy?: number;
|
|
33
|
+
positiveRate?: number;
|
|
34
|
+
error?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface IntelligenceAgentResult {
|
|
38
|
+
versionResults: ApiVersionCheck[];
|
|
39
|
+
errorAlerts: ErrorRateAlert[];
|
|
40
|
+
ltvTrainResult: LtvTrainResult;
|
|
41
|
+
abResult?: {
|
|
42
|
+
decided: boolean;
|
|
43
|
+
test_id?: number;
|
|
44
|
+
winner_name?: string;
|
|
45
|
+
improvement?: number;
|
|
46
|
+
};
|
|
47
|
+
mqAnalysis?: {
|
|
48
|
+
total?: number;
|
|
49
|
+
composite_score?: number;
|
|
50
|
+
email_rate?: number;
|
|
51
|
+
fbp_rate?: number;
|
|
52
|
+
alerts?: any[];
|
|
53
|
+
};
|
|
54
|
+
cmResult?: {
|
|
55
|
+
sent?: number;
|
|
56
|
+
received?: number;
|
|
57
|
+
skipped?: string;
|
|
58
|
+
error?: string;
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface CustomerMatchResult {
|
|
63
|
+
sent?: number;
|
|
64
|
+
received?: number;
|
|
65
|
+
num_received?: number;
|
|
66
|
+
skipped?: string;
|
|
67
|
+
error?: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface GoogleCustomerMatchExport {
|
|
71
|
+
hashed_email: string;
|
|
72
|
+
hashed_phone: string;
|
|
73
|
+
first_name: string;
|
|
74
|
+
last_name: string;
|
|
75
|
+
}
|
|
12
76
|
|
|
13
77
|
// ── Versões esperadas das APIs ────────────────────────────────────────────────
|
|
14
|
-
const EXPECTED_API_VERSIONS = {
|
|
78
|
+
const EXPECTED_API_VERSIONS: Record<string, string> = {
|
|
15
79
|
meta: 'v22.0',
|
|
16
80
|
ga4: 'latest',
|
|
17
81
|
tiktok: 'v1.3',
|
|
@@ -25,25 +89,35 @@ const ALERT_THRESHOLDS = {
|
|
|
25
89
|
};
|
|
26
90
|
|
|
27
91
|
// ── Alerta via CallMeBot ──────────────────────────────────────────────────────
|
|
28
|
-
export async function sendIntelligenceAlert(
|
|
29
|
-
|
|
92
|
+
export async function sendIntelligenceAlert(
|
|
93
|
+
env: Env,
|
|
94
|
+
severity: 'critical' | 'warning' | 'info',
|
|
95
|
+
title: string,
|
|
96
|
+
details: string
|
|
97
|
+
): Promise<void> {
|
|
98
|
+
const icon = severity === 'critical' ? '🚨' : severity === 'warning' ? '⚠️' : 'ℹ️';
|
|
30
99
|
const texto = `${icon} CDP Edge — ${title}\n\n${details}\n\n${new Date().toLocaleString('pt-BR', { timeZone: 'America/Sao_Paulo' })}`;
|
|
31
100
|
return sendCallMeBot(env, texto);
|
|
32
101
|
}
|
|
33
102
|
|
|
34
103
|
// ── Check de versões de API ───────────────────────────────────────────────────
|
|
35
|
-
export async function checkApiVersionsIntelligence(
|
|
36
|
-
|
|
104
|
+
export async function checkApiVersionsIntelligence(
|
|
105
|
+
env: Env,
|
|
106
|
+
runType: string
|
|
107
|
+
): Promise<ApiVersionCheck[]> {
|
|
108
|
+
const results: ApiVersionCheck[] = [];
|
|
37
109
|
|
|
38
110
|
for (const [platform, expected] of Object.entries(EXPECTED_API_VERSIONS)) {
|
|
39
|
-
const currentMap = { meta: 'v22.0', tiktok: 'v1.3', ga4: 'latest', pinterest: 'v5', reddit: 'v2.0' };
|
|
111
|
+
const currentMap: Record<string, string> = { meta: 'v22.0', tiktok: 'v1.3', ga4: 'latest', pinterest: 'v5', reddit: 'v2.0' };
|
|
40
112
|
const current = currentMap[platform] || 'unknown';
|
|
41
113
|
const isOk = current === expected || expected === 'latest';
|
|
42
114
|
const status = isOk ? 'ok' : 'warning';
|
|
43
115
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
116
|
+
if (env.DB) {
|
|
117
|
+
await logIntelligence(env.DB, runType, platform, 'api_version', status, current, expected,
|
|
118
|
+
isOk ? `${platform} ${current} — versão correta` : `${platform} ${current} desatualizado, esperado ${expected}`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
47
121
|
|
|
48
122
|
results.push({ platform, current, expected, status });
|
|
49
123
|
}
|
|
@@ -52,27 +126,34 @@ export async function checkApiVersionsIntelligence(env, runType) {
|
|
|
52
126
|
}
|
|
53
127
|
|
|
54
128
|
// ── Auditoria de taxa de erro ─────────────────────────────────────────────────
|
|
55
|
-
export async function auditErrorRates(
|
|
129
|
+
export async function auditErrorRates(
|
|
130
|
+
env: Env,
|
|
131
|
+
runType: string
|
|
132
|
+
): Promise<ErrorRateAlert[]> {
|
|
56
133
|
if (!env.DB) return [];
|
|
57
|
-
const alerts = [];
|
|
134
|
+
const alerts: ErrorRateAlert[] = [];
|
|
58
135
|
|
|
59
136
|
for (const platform of ['meta', 'ga4', 'tiktok']) {
|
|
60
137
|
const metrics = await getHealthMetrics(env.DB, platform, 24);
|
|
61
138
|
const errorRate = metrics.events_sent > 0 ? metrics.events_failed / metrics.events_sent : 0;
|
|
62
139
|
|
|
63
|
-
let status = 'ok';
|
|
140
|
+
let status: 'ok' | 'warning' | 'critical' = 'ok';
|
|
64
141
|
if (errorRate >= ALERT_THRESHOLDS.errorRateCritical) status = 'critical';
|
|
65
142
|
else if (errorRate >= ALERT_THRESHOLDS.errorRateWarning) status = 'warning';
|
|
66
143
|
|
|
67
144
|
const message = `${platform}: ${metrics.events_sent} eventos, ${metrics.events_failed} falhas (${(errorRate * 100).toFixed(1)}%)`;
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
145
|
+
let alertSent: boolean | undefined = false;
|
|
146
|
+
if (status !== 'ok') {
|
|
147
|
+
await sendIntelligenceAlert(env, status, `Taxa de Erro Alta — ${platform.toUpperCase()}`,
|
|
148
|
+
`📊 ${message}\n🎯 Taxa: ${(errorRate * 100).toFixed(1)}% (limite: ${ALERT_THRESHOLDS.errorRateWarning * 100}%)`);
|
|
149
|
+
alertSent = true;
|
|
150
|
+
}
|
|
72
151
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
152
|
+
if (env.DB) {
|
|
153
|
+
await logIntelligence(env.DB, runType, platform, 'error_rate', status,
|
|
154
|
+
`${(errorRate * 100).toFixed(1)}%`, `${ALERT_THRESHOLDS.errorRateWarning * 100}%`, message, alertSent
|
|
155
|
+
);
|
|
156
|
+
}
|
|
76
157
|
|
|
77
158
|
if (status !== 'ok') alerts.push({ platform, errorRate, status });
|
|
78
159
|
}
|
|
@@ -81,7 +162,7 @@ export async function auditErrorRates(env, runType) {
|
|
|
81
162
|
}
|
|
82
163
|
|
|
83
164
|
// ── Treinar modelo LTV (regressão logística com dados reais do D1) ────────────
|
|
84
|
-
export async function trainLtvModel(env) {
|
|
165
|
+
export async function trainLtvModel(env: Env): Promise<LtvTrainResult> {
|
|
85
166
|
if (!env.DB) return { skipped: 'DB não disponível' };
|
|
86
167
|
|
|
87
168
|
try {
|
|
@@ -108,7 +189,7 @@ export async function trainLtvModel(env) {
|
|
|
108
189
|
LIMIT 5000
|
|
109
190
|
`).all();
|
|
110
191
|
|
|
111
|
-
const dataset = (rows.results || []).map(row => ({
|
|
192
|
+
const dataset = (rows.results || []).map((row: any) => ({
|
|
112
193
|
features: extractFeatures(row),
|
|
113
194
|
label: row.label || 0,
|
|
114
195
|
}));
|
|
@@ -130,14 +211,17 @@ export async function trainLtvModel(env) {
|
|
|
130
211
|
console.log(`[LTV Train] Modelo treinado: ${dataset.length} samples, accuracy=${(model.accuracy * 100).toFixed(1)}%, positive_rate=${(model.positiveRate * 100).toFixed(1)}%`);
|
|
131
212
|
return { trained: true, samples: dataset.length, accuracy: model.accuracy, positiveRate: model.positiveRate };
|
|
132
213
|
|
|
133
|
-
} catch (err) {
|
|
134
|
-
console.error('[LTV Train] Erro:', err
|
|
135
|
-
return { error: err
|
|
214
|
+
} catch (err: any) {
|
|
215
|
+
console.error('[LTV Train] Erro:', err?.message || String(err));
|
|
216
|
+
return { error: err?.message || String(err) };
|
|
136
217
|
}
|
|
137
218
|
}
|
|
138
219
|
|
|
139
220
|
// ── Runner principal do Intelligence Agent ────────────────────────────────────
|
|
140
|
-
export async function runIntelligenceAgent(
|
|
221
|
+
export async function runIntelligenceAgent(
|
|
222
|
+
env: Env,
|
|
223
|
+
runType: string
|
|
224
|
+
): Promise<IntelligenceAgentResult> {
|
|
141
225
|
console.log(`[Intelligence Agent] Iniciando ${runType}`);
|
|
142
226
|
|
|
143
227
|
// 1. Check de versões
|
|
@@ -159,10 +243,10 @@ export async function runIntelligenceAgent(env, runType) {
|
|
|
159
243
|
// 4. Treinar modelo LTV (toda semana)
|
|
160
244
|
const ltvTrainResult = await trainLtvModel(env);
|
|
161
245
|
if (ltvTrainResult.trained) {
|
|
162
|
-
console.log(`[Intelligence Agent] LTV model treinado: accuracy=${(ltvTrainResult.accuracy * 100).toFixed(1)}%`);
|
|
246
|
+
console.log(`[Intelligence Agent] LTV model treinado: accuracy=${(ltvTrainResult.accuracy! * 100).toFixed(1)}%`);
|
|
163
247
|
if (env.DB) {
|
|
164
248
|
await logIntelligence(env.DB, runType, 'ltv', 'model_training', 'ok',
|
|
165
|
-
`accuracy=${(ltvTrainResult.accuracy * 100).toFixed(1)}%`, null,
|
|
249
|
+
`accuracy=${(ltvTrainResult.accuracy! * 100).toFixed(1)}%`, null,
|
|
166
250
|
`Modelo LTV re-treinado com ${ltvTrainResult.samples} amostras`
|
|
167
251
|
).catch(() => {});
|
|
168
252
|
}
|
|
@@ -171,9 +255,16 @@ export async function runIntelligenceAgent(env, runType) {
|
|
|
171
255
|
}
|
|
172
256
|
|
|
173
257
|
// 5. Auto-decisão de winner no A/B LTV Test
|
|
258
|
+
let abResult: IntelligenceAgentResult['abResult'] = undefined;
|
|
174
259
|
try {
|
|
175
|
-
const
|
|
176
|
-
if (
|
|
260
|
+
const abRes = await autoDecideAbWinner(env);
|
|
261
|
+
if (abRes?.decided) {
|
|
262
|
+
abResult = {
|
|
263
|
+
decided: abRes.decided,
|
|
264
|
+
test_id: abRes.test_id,
|
|
265
|
+
winner_name: abRes.winner_name,
|
|
266
|
+
improvement: abRes.improvement ? parseFloat(abRes.improvement) : undefined,
|
|
267
|
+
};
|
|
177
268
|
console.log(`[Intelligence Agent] A/B LTV winner auto-decidido: test_id=${abResult.test_id}, winner=${abResult.winner_name}`);
|
|
178
269
|
|
|
179
270
|
await sendIntelligenceAlert(env, 'info',
|
|
@@ -188,26 +279,28 @@ export async function runIntelligenceAgent(env, runType) {
|
|
|
188
279
|
).catch(() => {});
|
|
189
280
|
}
|
|
190
281
|
}
|
|
191
|
-
} catch (err) {
|
|
192
|
-
console.error('[Intelligence Agent] A/B auto-decide error:', err
|
|
282
|
+
} catch (err: any) {
|
|
283
|
+
console.error('[Intelligence Agent] A/B auto-decide error:', err?.message || String(err));
|
|
193
284
|
}
|
|
194
285
|
|
|
195
286
|
// 6. Match Quality — análise + alertas
|
|
287
|
+
let mqAnalysis: IntelligenceAgentResult['mqAnalysis'] = undefined;
|
|
196
288
|
try {
|
|
197
|
-
const
|
|
198
|
-
if (
|
|
289
|
+
const mqRes = await analyzeMatchQuality(env);
|
|
290
|
+
if (mqRes) {
|
|
291
|
+
mqAnalysis = mqRes;
|
|
199
292
|
console.log(`[Intelligence Agent] Match Quality: score=${mqAnalysis.composite_score ?? 0}%, alerts=${mqAnalysis.alerts?.length ?? 0}`);
|
|
200
|
-
await alertMatchQuality(env,
|
|
293
|
+
await alertMatchQuality(env, mqRes);
|
|
201
294
|
|
|
202
|
-
if (env.DB && mqAnalysis.total > 0) {
|
|
203
|
-
await logIntelligence(env.DB, runType, 'meta', 'match_quality', mqAnalysis.alerts
|
|
295
|
+
if (env.DB && mqAnalysis.total && mqAnalysis.total > 0) {
|
|
296
|
+
await logIntelligence(env.DB, runType, 'meta', 'match_quality', (mqAnalysis.alerts && mqAnalysis.alerts.length > 0) ? 'warning' : 'ok',
|
|
204
297
|
`${mqAnalysis.composite_score ?? 0}%`, '45%',
|
|
205
298
|
`Match quality 2h: email=${mqAnalysis.email_rate ?? 0}%, fbp=${mqAnalysis.fbp_rate ?? 0}%, score=${mqAnalysis.composite_score ?? 0}%`
|
|
206
299
|
).catch(() => {});
|
|
207
300
|
}
|
|
208
301
|
}
|
|
209
|
-
} catch (err) {
|
|
210
|
-
console.error('[Intelligence Agent] Match quality analysis error:', err
|
|
302
|
+
} catch (err: any) {
|
|
303
|
+
console.error('[Intelligence Agent] Match quality analysis error:', err?.message || String(err));
|
|
211
304
|
}
|
|
212
305
|
|
|
213
306
|
// 7. Auditoria mensal adicional
|
|
@@ -221,29 +314,40 @@ export async function runIntelligenceAgent(env, runType) {
|
|
|
221
314
|
GROUP BY predicted_ltv_class
|
|
222
315
|
`).all();
|
|
223
316
|
|
|
224
|
-
const summary = ltvStats.results?.map(r => `${r.predicted_ltv_class}: ${r.count}`).join(', ') || 'sem dados';
|
|
317
|
+
const summary = ltvStats.results?.map((r: any) => `${r.predicted_ltv_class}: ${r.count}`).join(', ') || 'sem dados';
|
|
225
318
|
await logIntelligence(env.DB, runType, 'all', 'ltv_distribution', 'ok', summary, null,
|
|
226
319
|
`Distribuição LTV últimos 30 dias: ${summary}`);
|
|
227
320
|
console.log(`[Intelligence Agent] LTV distribution: ${summary}`);
|
|
228
|
-
} catch (err) {
|
|
229
|
-
console.error('LTV audit error:', err
|
|
321
|
+
} catch (err: any) {
|
|
322
|
+
console.error('LTV audit error:', err?.message || String(err));
|
|
230
323
|
}
|
|
231
324
|
|
|
232
325
|
// Purge de logs antigos de match quality (> 30 dias)
|
|
233
|
-
|
|
234
|
-
|
|
326
|
+
if (env.DB) {
|
|
327
|
+
await purgeOldMatchQualityLogs(env.DB);
|
|
328
|
+
console.log('[Intelligence Agent] Match quality logs antigos purgados');
|
|
329
|
+
}
|
|
235
330
|
}
|
|
236
331
|
}
|
|
237
332
|
|
|
238
333
|
// 8. Customer Match sync semanal
|
|
239
334
|
const cmResult = await syncMetaCustomAudience(env);
|
|
240
|
-
console.log(`[Intelligence Agent] Customer Match Meta: sent=${cmResult?.sent ?? 0}, received=${cmResult?.
|
|
335
|
+
console.log(`[Intelligence Agent] Customer Match Meta: sent=${cmResult?.sent ?? 0}, received=${cmResult?.received ?? 0}`);
|
|
241
336
|
|
|
242
337
|
console.log(`[Intelligence Agent] ${runType} concluído — LTV model, A/B auto-decide, match quality, customer match`);
|
|
338
|
+
|
|
339
|
+
return {
|
|
340
|
+
versionResults,
|
|
341
|
+
errorAlerts,
|
|
342
|
+
ltvTrainResult,
|
|
343
|
+
abResult,
|
|
344
|
+
mqAnalysis,
|
|
345
|
+
cmResult,
|
|
346
|
+
};
|
|
243
347
|
}
|
|
244
348
|
|
|
245
349
|
// ── syncMetaCustomAudience — D1 → Meta Custom Audiences ─────────────────────
|
|
246
|
-
export async function syncMetaCustomAudience(env) {
|
|
350
|
+
export async function syncMetaCustomAudience(env: Env): Promise<CustomerMatchResult> {
|
|
247
351
|
if (!env.META_ACCESS_TOKEN || !env.META_AD_ACCOUNT_ID || !env.META_AUDIENCE_ID) {
|
|
248
352
|
console.log('[CustomerMatch] Meta: secrets não configurados — pulando sync');
|
|
249
353
|
return { skipped: 'META_AD_ACCOUNT_ID ou META_AUDIENCE_ID não configurados' };
|
|
@@ -265,7 +369,7 @@ export async function syncMetaCustomAudience(env) {
|
|
|
265
369
|
}
|
|
266
370
|
|
|
267
371
|
const data = await Promise.all(
|
|
268
|
-
profiles.results.map(async (p) => [
|
|
372
|
+
profiles.results.map(async (p: any) => [
|
|
269
373
|
p.email ? await sha256(p.email) : '',
|
|
270
374
|
p.phone ? await sha256(p.phone) : '',
|
|
271
375
|
])
|
|
@@ -280,7 +384,7 @@ export async function syncMetaCustomAudience(env) {
|
|
|
280
384
|
body: JSON.stringify({ ...body, access_token: env.META_ACCESS_TOKEN }),
|
|
281
385
|
});
|
|
282
386
|
|
|
283
|
-
const result = await res.json();
|
|
387
|
+
const result = await res.json() as any;
|
|
284
388
|
|
|
285
389
|
if (!res.ok) {
|
|
286
390
|
console.error('[CustomerMatch] Meta erro:', res.status, result.error?.message || 'unknown');
|
|
@@ -288,16 +392,16 @@ export async function syncMetaCustomAudience(env) {
|
|
|
288
392
|
}
|
|
289
393
|
|
|
290
394
|
console.log(`[CustomerMatch] Meta: ${profiles.results.length} perfis sincronizados`);
|
|
291
|
-
return { sent: profiles.results.length, num_received: result.num_received };
|
|
395
|
+
return { sent: profiles.results.length, num_received: result.num_received, received: result.num_received };
|
|
292
396
|
|
|
293
|
-
} catch (err) {
|
|
294
|
-
console.error('[CustomerMatch] Meta fetch error:', err
|
|
295
|
-
return { error: err
|
|
397
|
+
} catch (err: any) {
|
|
398
|
+
console.error('[CustomerMatch] Meta fetch error:', err?.message || String(err));
|
|
399
|
+
return { error: err?.message || String(err), sent: 0 };
|
|
296
400
|
}
|
|
297
401
|
}
|
|
298
402
|
|
|
299
403
|
// ── buildGoogleCustomerMatchExport — gera JSON para Google Ads Customer Match ─
|
|
300
|
-
export async function buildGoogleCustomerMatchExport(env) {
|
|
404
|
+
export async function buildGoogleCustomerMatchExport(env: Env): Promise<GoogleCustomerMatchExport[]> {
|
|
301
405
|
if (!env.DB) return [];
|
|
302
406
|
|
|
303
407
|
const profiles = await env.DB.prepare(`
|
|
@@ -310,12 +414,23 @@ export async function buildGoogleCustomerMatchExport(env) {
|
|
|
310
414
|
|
|
311
415
|
if (!profiles.results?.length) return [];
|
|
312
416
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
417
|
+
const results: GoogleCustomerMatchExport[] = [];
|
|
418
|
+
for (const p of profiles.results) {
|
|
419
|
+
const email = p.email as string | null | undefined;
|
|
420
|
+
const phone = p.phone as string | null | undefined;
|
|
421
|
+
const firstName = p.first_name as string | undefined;
|
|
422
|
+
const lastName = p.last_name as string | undefined;
|
|
423
|
+
|
|
424
|
+
const hashed_email = email ? await sha256(email) : '';
|
|
425
|
+
const hashed_phone = phone ? await sha256(phone) : '';
|
|
426
|
+
if (hashed_email || hashed_phone) {
|
|
427
|
+
results.push({
|
|
428
|
+
hashed_email: hashed_email || '',
|
|
429
|
+
hashed_phone: hashed_phone || '',
|
|
430
|
+
first_name: firstName || '',
|
|
431
|
+
last_name: lastName || '',
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return results;
|
|
321
436
|
}
|
|
@@ -4,13 +4,15 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { tryParseJson } from '../utils.js';
|
|
7
|
+
import { Env } from '../../types.js';
|
|
8
|
+
import { ExecutionContext } from '@cloudflare/workers-types';
|
|
7
9
|
|
|
8
10
|
// ── Constantes de calibração ──────────────────────────────────────────────────
|
|
9
|
-
const PLATFORM_FACTORS = { meta: 0.85, google: 0.90, tiktok: 0.75 };
|
|
11
|
+
const PLATFORM_FACTORS: Record<string, number> = { meta: 0.85, google: 0.90, tiktok: 0.75 };
|
|
10
12
|
|
|
11
|
-
function getSegmentMultiplier(avgLtvClass, avgBehaviorScore) {
|
|
12
|
-
const ltv = parseFloat(avgLtvClass
|
|
13
|
-
const eng = parseFloat(avgBehaviorScore || 0);
|
|
13
|
+
function getSegmentMultiplier(avgLtvClass: string | number, avgBehaviorScore: string | number): number {
|
|
14
|
+
const ltv = parseFloat(String(avgLtvClass) || '0');
|
|
15
|
+
const eng = parseFloat(String(avgBehaviorScore) || '0');
|
|
14
16
|
if (ltv >= 0.7 && eng >= 0.7) return 1.4;
|
|
15
17
|
if (ltv >= 0.7 && eng >= 0.4) return 1.2;
|
|
16
18
|
if (ltv >= 0.4 && eng >= 0.7) return 1.0;
|
|
@@ -18,17 +20,17 @@ function getSegmentMultiplier(avgLtvClass, avgBehaviorScore) {
|
|
|
18
20
|
return 0.6;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
|
-
function getConfidenceAdjustment(confidence) {
|
|
23
|
+
function getConfidenceAdjustment(confidence: number): number {
|
|
22
24
|
if (confidence >= 0.7) return 1.00;
|
|
23
25
|
if (confidence >= 0.4) return 0.85;
|
|
24
26
|
return 0.70;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
// ── POST /api/bidding/recommend ───────────────────────────────────────────────
|
|
28
|
-
export async function handleBiddingRecommend(env, request, headers) {
|
|
30
|
+
export async function handleBiddingRecommend(env: Env, request: Request, headers: Headers): Promise<Response> {
|
|
29
31
|
if (!env.DB) return new Response(JSON.stringify({ error: 'DB não configurado' }), { status: 503, headers });
|
|
30
32
|
|
|
31
|
-
let body;
|
|
33
|
+
let body: any;
|
|
32
34
|
try { body = await request.json(); }
|
|
33
35
|
catch { return new Response(JSON.stringify({ error: 'JSON inválido no body' }), { status: 400, headers }); }
|
|
34
36
|
|
|
@@ -79,9 +81,9 @@ export async function handleBiddingRecommend(env, request, headers) {
|
|
|
79
81
|
AND (bot_score IS NULL OR bot_score < 2)
|
|
80
82
|
`).bind(period_days).first();
|
|
81
83
|
|
|
82
|
-
globalLeads = globalRes?.total_leads || 0;
|
|
83
|
-
globalLtv = globalRes?.avg_ltv || 0;
|
|
84
|
-
globalConversions = globalRes?.conversions || 0;
|
|
84
|
+
globalLeads = Number((globalRes as any)?.total_leads) || 0;
|
|
85
|
+
globalLtv = Number((globalRes as any)?.avg_ltv) || 0;
|
|
86
|
+
globalConversions = Number((globalRes as any)?.conversions) || 0;
|
|
85
87
|
|
|
86
88
|
if (globalLeads < 10) {
|
|
87
89
|
return new Response(JSON.stringify({
|
|
@@ -92,27 +94,27 @@ export async function handleBiddingRecommend(env, request, headers) {
|
|
|
92
94
|
}
|
|
93
95
|
|
|
94
96
|
const now = new Date().toISOString();
|
|
95
|
-
const recommendations = [];
|
|
97
|
+
const recommendations: any[] = [];
|
|
96
98
|
|
|
97
99
|
const targetSegments = segments.length > 0
|
|
98
100
|
? segments
|
|
99
101
|
: [{ segment_id: null, cluster_name: 'Global (sem segmentação)', avg_ltv_class: 0.5, avg_behavior_score: 0.5, avg_engagement_score: 0.5, member_count: globalLeads, real_avg_ltv: globalLtv, conversions: globalConversions }];
|
|
100
102
|
|
|
101
103
|
for (const seg of targetSegments) {
|
|
102
|
-
const avgLtv = parseFloat(seg.real_avg_ltv || 0);
|
|
103
|
-
const convs = parseInt(seg.conversions || 0);
|
|
104
|
+
const avgLtv = parseFloat(String(seg.real_avg_ltv) || '0');
|
|
105
|
+
const convs = parseInt(String(seg.conversions || '0'));
|
|
104
106
|
const confidence = Math.min(1, convs / 100);
|
|
105
107
|
|
|
106
108
|
const estimatedLtv = avgLtv > 0 ? avgLtv :
|
|
107
|
-
seg.avg_ltv_class >= 0.7 ? 497 : seg.avg_ltv_class >= 0.4 ? 297 : 97;
|
|
109
|
+
Number(seg.avg_ltv_class) >= 0.7 ? 497 : Number(seg.avg_ltv_class) >= 0.4 ? 297 : 97;
|
|
108
110
|
|
|
109
111
|
const cpaTarget = estimatedLtv / target_roi;
|
|
110
|
-
const segMult = getSegmentMultiplier(seg.avg_ltv_class, seg.avg_behavior_score);
|
|
112
|
+
const segMult = getSegmentMultiplier(String(seg.avg_ltv_class), String(seg.avg_behavior_score));
|
|
111
113
|
const confAdj = getConfidenceAdjustment(confidence);
|
|
112
114
|
const alertMsg = convs < 30 ? `Atenção: apenas ${convs} conversões no período. Bid baseado em estimativa de LTV — aplique com cautela.` : null;
|
|
113
115
|
|
|
114
116
|
for (const plat of platforms) {
|
|
115
|
-
const platFactor = PLATFORM_FACTORS[plat];
|
|
117
|
+
const platFactor = PLATFORM_FACTORS[plat] || 0.8;
|
|
116
118
|
const recommendedBid = Math.max(5, cpaTarget * platFactor * segMult * confAdj);
|
|
117
119
|
const expectedRoi = estimatedLtv / (recommendedBid / platFactor);
|
|
118
120
|
|
|
@@ -138,12 +140,12 @@ export async function handleBiddingRecommend(env, request, headers) {
|
|
|
138
140
|
confidence, expectedRoi, reasoning, alertMsg || null,
|
|
139
141
|
platFactor, confAdj, segMult,
|
|
140
142
|
).run();
|
|
141
|
-
} catch (e) { console.error('[Bidding] D1 insert error:', e
|
|
143
|
+
} catch (e: any) { console.error('[Bidding] D1 insert error:', e?.message || String(e)); }
|
|
142
144
|
|
|
143
145
|
recommendations.push({
|
|
144
146
|
platform: plat, segment: seg.cluster_name, segment_id: seg.segment_id || null,
|
|
145
147
|
avg_ltv: Math.round(estimatedLtv * 100) / 100,
|
|
146
|
-
avg_ltv_class: seg.avg_ltv_class >= 0.7 ? 'High' : seg.avg_ltv_class >= 0.4 ? 'Medium' : 'Low',
|
|
148
|
+
avg_ltv_class: Number(seg.avg_ltv_class) >= 0.7 ? 'High' : Number(seg.avg_ltv_class) >= 0.4 ? 'Medium' : 'Low',
|
|
147
149
|
cpa_target: Math.round(cpaTarget * 100) / 100,
|
|
148
150
|
recommended_bid: Math.round(recommendedBid * 100) / 100,
|
|
149
151
|
bid_currency: 'BRL', confidence: Math.round(confidence * 100) / 100,
|
|
@@ -161,8 +163,8 @@ export async function handleBiddingRecommend(env, request, headers) {
|
|
|
161
163
|
return new Response(JSON.stringify({
|
|
162
164
|
success: true, generated_at: now, vertical, period_days, target_roi,
|
|
163
165
|
data_quality: {
|
|
164
|
-
leads_analyzed: targetSegments.reduce((s, sg) => s + (sg.member_count || 0), 0),
|
|
165
|
-
conversions_found: targetSegments.reduce((s, sg) => s + (sg.conversions || 0), 0),
|
|
166
|
+
leads_analyzed: targetSegments.reduce((s: number, sg: any) => s + (sg.member_count || 0), 0),
|
|
167
|
+
conversions_found: targetSegments.reduce((s: number, sg: any) => s + (sg.conversions || 0), 0),
|
|
166
168
|
segments_active: segments.length, confidence: Math.round(avgConfidence * 100) / 100,
|
|
167
169
|
},
|
|
168
170
|
recommendations,
|
|
@@ -174,14 +176,14 @@ export async function handleBiddingRecommend(env, request, headers) {
|
|
|
174
176
|
},
|
|
175
177
|
}), { status: 200, headers });
|
|
176
178
|
|
|
177
|
-
} catch (err) {
|
|
178
|
-
console.error('[Bidding] recommend error:', err
|
|
179
|
-
return new Response(JSON.stringify({ error: 'Erro ao gerar recomendações', message: err
|
|
179
|
+
} catch (err: any) {
|
|
180
|
+
console.error('[Bidding] recommend error:', err?.message || String(err));
|
|
181
|
+
return new Response(JSON.stringify({ error: 'Erro ao gerar recomendações', message: err?.message || String(err) }), { status: 500, headers });
|
|
180
182
|
}
|
|
181
183
|
}
|
|
182
184
|
|
|
183
185
|
// ── GET /api/bidding/history ──────────────────────────────────────────────────
|
|
184
|
-
export async function handleBiddingHistory(env, request, headers) {
|
|
186
|
+
export async function handleBiddingHistory(env: Env, request: Request, headers: Headers): Promise<Response> {
|
|
185
187
|
if (!env.DB) return new Response(JSON.stringify({ error: 'DB não configurado' }), { status: 503, headers });
|
|
186
188
|
|
|
187
189
|
const url = new URL(request.url);
|
|
@@ -190,8 +192,8 @@ export async function handleBiddingHistory(env, request, headers) {
|
|
|
190
192
|
const limit = Math.min(parseInt(url.searchParams.get('limit') || '20'), 100);
|
|
191
193
|
|
|
192
194
|
try {
|
|
193
|
-
const conditions = [];
|
|
194
|
-
const bindings = [];
|
|
195
|
+
const conditions: string[] = [];
|
|
196
|
+
const bindings: (string | number)[] = [];
|
|
195
197
|
if (vertical) { conditions.push('vertical = ?'); bindings.push(vertical); }
|
|
196
198
|
if (platform) { conditions.push('platform = ?'); bindings.push(platform); }
|
|
197
199
|
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
@@ -208,16 +210,16 @@ export async function handleBiddingHistory(env, request, headers) {
|
|
|
208
210
|
LIMIT ?
|
|
209
211
|
`).bind(...bindings, limit).all();
|
|
210
212
|
|
|
211
|
-
const items = (result.results || []).map(r => ({ ...r, applied_result: tryParseJson(r.applied_result, null) }));
|
|
213
|
+
const items = (result.results || []).map((r: any) => ({ ...r, applied_result: tryParseJson(r.applied_result, null) }));
|
|
212
214
|
return new Response(JSON.stringify({ success: true, total: items.length, history: items }), { status: 200, headers });
|
|
213
|
-
} catch (err) {
|
|
214
|
-
console.error('[Bidding] history error:', err
|
|
215
|
-
return new Response(JSON.stringify({ error: err
|
|
215
|
+
} catch (err: any) {
|
|
216
|
+
console.error('[Bidding] history error:', err?.message || String(err));
|
|
217
|
+
return new Response(JSON.stringify({ error: err?.message || String(err) }), { status: 500, headers });
|
|
216
218
|
}
|
|
217
219
|
}
|
|
218
220
|
|
|
219
221
|
// ── GET /api/bidding/status ───────────────────────────────────────────────────
|
|
220
|
-
export async function handleBiddingStatus(env, request, headers) {
|
|
222
|
+
export async function handleBiddingStatus(env: Env, request: Request, headers: Headers): Promise<Response> {
|
|
221
223
|
if (!env.DB) return new Response(JSON.stringify({ error: 'DB não configurado' }), { status: 503, headers });
|
|
222
224
|
|
|
223
225
|
const url = new URL(request.url);
|
|
@@ -232,14 +234,14 @@ export async function handleBiddingStatus(env, request, headers) {
|
|
|
232
234
|
FROM bid_recommendations
|
|
233
235
|
WHERE is_active = 1
|
|
234
236
|
`;
|
|
235
|
-
const bindings = [];
|
|
237
|
+
const bindings: (string | number)[] = [];
|
|
236
238
|
if (vertical) { query += ' AND vertical = ?'; bindings.push(vertical); }
|
|
237
239
|
query += ' GROUP BY platform, vertical ORDER BY last_generated DESC';
|
|
238
240
|
|
|
239
241
|
const result = await env.DB.prepare(query).bind(...bindings).all();
|
|
240
242
|
return new Response(JSON.stringify({ success: true, status: result.results || [] }), { status: 200, headers });
|
|
241
|
-
} catch (err) {
|
|
242
|
-
console.error('[Bidding] status error:', err
|
|
243
|
-
return new Response(JSON.stringify({ error: err
|
|
243
|
+
} catch (err: any) {
|
|
244
|
+
console.error('[Bidding] status error:', err?.message || String(err));
|
|
245
|
+
return new Response(JSON.stringify({ error: err?.message || String(err) }), { status: 500, headers });
|
|
244
246
|
}
|
|
245
247
|
}
|