cdp-edge 2.0.2 → 2.0.4
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 +325 -308
- package/contracts/agent-versions.json +364 -0
- package/dist/commands/install.js +1 -1
- package/dist/commands/setup.js +1 -1
- package/extracted-skill/tracking-events-generator/agents/browser-tracking.md +2 -2
- package/extracted-skill/tracking-events-generator/agents/master-orchestrator.md +14 -20
- package/extracted-skill/tracking-events-generator/agents/premium-tracking-intelligence-agent.md +4 -4
- package/extracted-skill/tracking-events-generator/agents/server-tracking.md +13 -13
- package/extracted-skill/tracking-events-generator/agents/spotify-agent.md +1 -1
- package/extracted-skill/tracking-events-generator/agents/webhook-agent.md +4 -4
- package/extracted-skill/tracking-events-generator/agents/youtube-agent.md +3 -3
- package/package.json +81 -76
- package/server-edge-tracker/index.js +780 -0
- package/server-edge-tracker/modules/db.js +531 -0
- package/server-edge-tracker/modules/dispatch/ga4.js +65 -0
- package/server-edge-tracker/modules/dispatch/meta.js +103 -0
- package/server-edge-tracker/modules/dispatch/platforms.js +237 -0
- package/server-edge-tracker/modules/dispatch/tiktok.js +100 -0
- package/server-edge-tracker/modules/dispatch/whatsapp.js +233 -0
- package/server-edge-tracker/modules/intelligence.js +204 -0
- package/server-edge-tracker/modules/ml/bidding.js +245 -0
- package/server-edge-tracker/modules/ml/fraud.js +301 -0
- package/server-edge-tracker/modules/ml/ltv.js +320 -0
- package/server-edge-tracker/modules/ml/segmentation.js +316 -0
- package/server-edge-tracker/modules/utils.js +89 -0
- package/server-edge-tracker/schema-indexes.sql +67 -0
- package/server-edge-tracker/wrangler.toml +2 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CDP Edge — Intelligence Agent + Customer Match
|
|
3
|
+
* runIntelligenceAgent, customer match Meta/Google, health checks
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { sha256 } from './utils.js';
|
|
7
|
+
import { getHealthMetrics, generateDailyReport, logIntelligence } from './db.js';
|
|
8
|
+
import { sendCallMeBot } from './dispatch/whatsapp.js';
|
|
9
|
+
|
|
10
|
+
// ── Versões esperadas das APIs ────────────────────────────────────────────────
|
|
11
|
+
const EXPECTED_API_VERSIONS = {
|
|
12
|
+
meta: 'v22.0',
|
|
13
|
+
ga4: 'latest',
|
|
14
|
+
tiktok: 'v1.3',
|
|
15
|
+
pinterest: 'v5',
|
|
16
|
+
reddit: 'v2.0',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const ALERT_THRESHOLDS = {
|
|
20
|
+
errorRateCritical: 0.20,
|
|
21
|
+
errorRateWarning: 0.10,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// ── Alerta via CallMeBot ──────────────────────────────────────────────────────
|
|
25
|
+
export async function sendIntelligenceAlert(env, severity, title, details) {
|
|
26
|
+
const icon = severity === 'critical' ? '🚨' : '⚠️';
|
|
27
|
+
const texto = `${icon} CDP Edge — ${title}\n\n${details}\n\n${new Date().toLocaleString('pt-BR', { timeZone: 'America/Sao_Paulo' })}`;
|
|
28
|
+
return sendCallMeBot(env, texto);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ── Check de versões de API ───────────────────────────────────────────────────
|
|
32
|
+
export async function checkApiVersionsIntelligence(env, runType) {
|
|
33
|
+
const results = [];
|
|
34
|
+
|
|
35
|
+
for (const [platform, expected] of Object.entries(EXPECTED_API_VERSIONS)) {
|
|
36
|
+
const currentMap = { meta: 'v22.0', tiktok: 'v1.3', ga4: 'latest', pinterest: 'v5', reddit: 'v2.0' };
|
|
37
|
+
const current = currentMap[platform] || 'unknown';
|
|
38
|
+
const isOk = current === expected || expected === 'latest';
|
|
39
|
+
const status = isOk ? 'ok' : 'warning';
|
|
40
|
+
|
|
41
|
+
await logIntelligence(env.DB, runType, platform, 'api_version', status, current, expected,
|
|
42
|
+
isOk ? `${platform} ${current} — versão correta` : `${platform} ${current} desatualizado, esperado ${expected}`
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
results.push({ platform, current, expected, status });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return results;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ── Auditoria de taxa de erro ─────────────────────────────────────────────────
|
|
52
|
+
export async function auditErrorRates(env, runType) {
|
|
53
|
+
if (!env.DB) return [];
|
|
54
|
+
const alerts = [];
|
|
55
|
+
|
|
56
|
+
for (const platform of ['meta', 'ga4', 'tiktok']) {
|
|
57
|
+
const metrics = await getHealthMetrics(env.DB, platform, 24);
|
|
58
|
+
const errorRate = metrics.events_sent > 0 ? metrics.events_failed / metrics.events_sent : 0;
|
|
59
|
+
|
|
60
|
+
let status = 'ok';
|
|
61
|
+
if (errorRate >= ALERT_THRESHOLDS.errorRateCritical) status = 'critical';
|
|
62
|
+
else if (errorRate >= ALERT_THRESHOLDS.errorRateWarning) status = 'warning';
|
|
63
|
+
|
|
64
|
+
const message = `${platform}: ${metrics.events_sent} eventos, ${metrics.events_failed} falhas (${(errorRate * 100).toFixed(1)}%)`;
|
|
65
|
+
const alertSent = status !== 'ok'
|
|
66
|
+
? await sendIntelligenceAlert(env, status, `Taxa de Erro Alta — ${platform.toUpperCase()}`,
|
|
67
|
+
`📊 ${message}\n🎯 Taxa: ${(errorRate * 100).toFixed(1)}% (limite: ${ALERT_THRESHOLDS.errorRateWarning * 100}%)`)
|
|
68
|
+
: false;
|
|
69
|
+
|
|
70
|
+
await logIntelligence(env.DB, runType, platform, 'error_rate', status,
|
|
71
|
+
`${(errorRate * 100).toFixed(1)}%`, `${ALERT_THRESHOLDS.errorRateWarning * 100}%`, message, alertSent
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
if (status !== 'ok') alerts.push({ platform, errorRate, status });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return alerts;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── Runner principal do Intelligence Agent ────────────────────────────────────
|
|
81
|
+
export async function runIntelligenceAgent(env, runType) {
|
|
82
|
+
console.log(`[Intelligence Agent] Iniciando ${runType}`);
|
|
83
|
+
|
|
84
|
+
// 1. Check de versões
|
|
85
|
+
const versionResults = await checkApiVersionsIntelligence(env, runType);
|
|
86
|
+
console.log(`[Intelligence Agent] Versões verificadas: ${versionResults.length} plataformas`);
|
|
87
|
+
|
|
88
|
+
// 2. Relatório diário
|
|
89
|
+
if (env.DB) {
|
|
90
|
+
const reports = await generateDailyReport(env.DB);
|
|
91
|
+
console.log(`[Intelligence Agent] Relatórios gerados: ${reports.length}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 3. Auditoria de taxas de erro
|
|
95
|
+
const errorAlerts = await auditErrorRates(env, runType);
|
|
96
|
+
if (errorAlerts.length > 0) {
|
|
97
|
+
console.warn(`[Intelligence Agent] ${errorAlerts.length} alertas de taxa de erro enviados`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 4. Auditoria mensal adicional
|
|
101
|
+
if (runType === 'monthly_audit') {
|
|
102
|
+
if (env.DB) {
|
|
103
|
+
try {
|
|
104
|
+
const ltvStats = await env.DB.prepare(`
|
|
105
|
+
SELECT predicted_ltv_class, COUNT(*) as count
|
|
106
|
+
FROM user_profiles
|
|
107
|
+
WHERE predicted_ltv_class IS NOT NULL AND updated_at > datetime('now', '-30 days')
|
|
108
|
+
GROUP BY predicted_ltv_class
|
|
109
|
+
`).all();
|
|
110
|
+
|
|
111
|
+
const summary = ltvStats.results?.map(r => `${r.predicted_ltv_class}: ${r.count}`).join(', ') || 'sem dados';
|
|
112
|
+
await logIntelligence(env.DB, runType, 'all', 'ltv_distribution', 'ok', summary, null,
|
|
113
|
+
`Distribuição LTV últimos 30 dias: ${summary}`);
|
|
114
|
+
console.log(`[Intelligence Agent] LTV distribution: ${summary}`);
|
|
115
|
+
} catch (err) {
|
|
116
|
+
console.error('LTV audit error:', err.message);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 5. Customer Match sync semanal
|
|
122
|
+
const cmResult = await syncMetaCustomAudience(env);
|
|
123
|
+
console.log(`[Intelligence Agent] Customer Match Meta: sent=${cmResult?.sent ?? 0}, received=${cmResult?.num_received ?? 0}`);
|
|
124
|
+
|
|
125
|
+
console.log(`[Intelligence Agent] ${runType} concluído`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ── syncMetaCustomAudience — D1 → Meta Custom Audiences ─────────────────────
|
|
129
|
+
export async function syncMetaCustomAudience(env) {
|
|
130
|
+
if (!env.META_ACCESS_TOKEN || !env.META_AD_ACCOUNT_ID || !env.META_AUDIENCE_ID) {
|
|
131
|
+
console.log('[CustomerMatch] Meta: secrets não configurados — pulando sync');
|
|
132
|
+
return { skipped: 'META_AD_ACCOUNT_ID ou META_AUDIENCE_ID não configurados' };
|
|
133
|
+
}
|
|
134
|
+
if (!env.DB) return { skipped: 'DB não disponível' };
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const profiles = await env.DB.prepare(`
|
|
138
|
+
SELECT email, phone FROM user_profiles
|
|
139
|
+
WHERE cohort_label IN ('high_intent', 'buyer_lookalike')
|
|
140
|
+
AND updated_at > datetime('now', '-30 days')
|
|
141
|
+
AND email IS NOT NULL
|
|
142
|
+
LIMIT 10000
|
|
143
|
+
`).all();
|
|
144
|
+
|
|
145
|
+
if (!profiles.results || profiles.results.length === 0) {
|
|
146
|
+
console.log('[CustomerMatch] Meta: nenhum perfil elegível');
|
|
147
|
+
return { sent: 0 };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const data = await Promise.all(
|
|
151
|
+
profiles.results.map(async (p) => [
|
|
152
|
+
p.email ? await sha256(p.email) : '',
|
|
153
|
+
p.phone ? await sha256(p.phone) : '',
|
|
154
|
+
])
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const body = { payload: { schema: ['EMAIL_SHA256', 'PHONE_SHA256'], data } };
|
|
158
|
+
const endpoint = `https://graph.facebook.com/v22.0/${env.META_AUDIENCE_ID}/users`;
|
|
159
|
+
|
|
160
|
+
const res = await fetch(endpoint, {
|
|
161
|
+
method: 'POST',
|
|
162
|
+
headers: { 'Content-Type': 'application/json' },
|
|
163
|
+
body: JSON.stringify({ ...body, access_token: env.META_ACCESS_TOKEN }),
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const result = await res.json();
|
|
167
|
+
|
|
168
|
+
if (!res.ok) {
|
|
169
|
+
console.error('[CustomerMatch] Meta erro:', res.status, result.error?.message || 'unknown');
|
|
170
|
+
return { error: result.error?.message, sent: 0 };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
console.log(`[CustomerMatch] Meta: ${profiles.results.length} perfis sincronizados`);
|
|
174
|
+
return { sent: profiles.results.length, num_received: result.num_received };
|
|
175
|
+
|
|
176
|
+
} catch (err) {
|
|
177
|
+
console.error('[CustomerMatch] Meta fetch error:', err.message);
|
|
178
|
+
return { error: err.message, sent: 0 };
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ── buildGoogleCustomerMatchExport — gera JSON para Google Ads Customer Match ─
|
|
183
|
+
export async function buildGoogleCustomerMatchExport(env) {
|
|
184
|
+
if (!env.DB) return [];
|
|
185
|
+
|
|
186
|
+
const profiles = await env.DB.prepare(`
|
|
187
|
+
SELECT email, phone, first_name, last_name FROM user_profiles
|
|
188
|
+
WHERE cohort_label IN ('high_intent', 'buyer_lookalike')
|
|
189
|
+
AND updated_at > datetime('now', '-30 days')
|
|
190
|
+
AND email IS NOT NULL
|
|
191
|
+
LIMIT 10000
|
|
192
|
+
`).all();
|
|
193
|
+
|
|
194
|
+
if (!profiles.results?.length) return [];
|
|
195
|
+
|
|
196
|
+
return Promise.all(
|
|
197
|
+
profiles.results.map(async (p) => ({
|
|
198
|
+
hashed_email: p.email ? await sha256(p.email) : '',
|
|
199
|
+
hashed_phone: p.phone ? await sha256(p.phone) : '',
|
|
200
|
+
first_name: p.first_name || '',
|
|
201
|
+
last_name: p.last_name || '',
|
|
202
|
+
}))
|
|
203
|
+
);
|
|
204
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CDP Edge — Bidding Recommendations ML (Fase 2)
|
|
3
|
+
* Handlers das rotas /api/bidding/*
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { tryParseJson } from '../utils.js';
|
|
7
|
+
|
|
8
|
+
// ── Constantes de calibração ──────────────────────────────────────────────────
|
|
9
|
+
const PLATFORM_FACTORS = { meta: 0.85, google: 0.90, tiktok: 0.75 };
|
|
10
|
+
|
|
11
|
+
function getSegmentMultiplier(avgLtvClass, avgBehaviorScore) {
|
|
12
|
+
const ltv = parseFloat(avgLtvClass || 0);
|
|
13
|
+
const eng = parseFloat(avgBehaviorScore || 0);
|
|
14
|
+
if (ltv >= 0.7 && eng >= 0.7) return 1.4;
|
|
15
|
+
if (ltv >= 0.7 && eng >= 0.4) return 1.2;
|
|
16
|
+
if (ltv >= 0.4 && eng >= 0.7) return 1.0;
|
|
17
|
+
if (ltv >= 0.4 && eng >= 0.4) return 0.8;
|
|
18
|
+
return 0.6;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getConfidenceAdjustment(confidence) {
|
|
22
|
+
if (confidence >= 0.7) return 1.00;
|
|
23
|
+
if (confidence >= 0.4) return 0.85;
|
|
24
|
+
return 0.70;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ── POST /api/bidding/recommend ───────────────────────────────────────────────
|
|
28
|
+
export async function handleBiddingRecommend(env, request, headers) {
|
|
29
|
+
if (!env.DB) return new Response(JSON.stringify({ error: 'DB não configurado' }), { status: 503, headers });
|
|
30
|
+
|
|
31
|
+
let body;
|
|
32
|
+
try { body = await request.json(); }
|
|
33
|
+
catch { return new Response(JSON.stringify({ error: 'JSON inválido no body' }), { status: 400, headers }); }
|
|
34
|
+
|
|
35
|
+
const {
|
|
36
|
+
vertical = 'geral', platform = 'meta',
|
|
37
|
+
target_roi = 3.5, period_days = 30,
|
|
38
|
+
campaign_id = null, budget = null,
|
|
39
|
+
} = body;
|
|
40
|
+
|
|
41
|
+
const platforms = platform === 'all'
|
|
42
|
+
? Object.keys(PLATFORM_FACTORS)
|
|
43
|
+
: [platform].filter(p => PLATFORM_FACTORS[p]);
|
|
44
|
+
|
|
45
|
+
if (platforms.length === 0) {
|
|
46
|
+
return new Response(JSON.stringify({ error: 'platform deve ser: meta, google, tiktok ou all' }), { status: 400, headers });
|
|
47
|
+
}
|
|
48
|
+
if (target_roi < 1 || target_roi > 20) {
|
|
49
|
+
return new Response(JSON.stringify({ error: 'target_roi deve estar entre 1 e 20' }), { status: 400, headers });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const segmentsRes = await env.DB.prepare(`
|
|
54
|
+
SELECT ms.id AS segment_id, ms.cluster_name, ms.avg_ltv_class, ms.avg_behavior_score,
|
|
55
|
+
ms.avg_engagement_score, ms.silhouette_score,
|
|
56
|
+
COUNT(msm.id) AS member_count,
|
|
57
|
+
AVG(l.predicted_ltv) AS real_avg_ltv,
|
|
58
|
+
SUM(CASE WHEN l.event_name IN ('Purchase','CompletePayment') THEN 1 ELSE 0 END) AS conversions
|
|
59
|
+
FROM ml_segments ms
|
|
60
|
+
LEFT JOIN ml_segment_members msm ON msm.cluster_id = ms.id
|
|
61
|
+
LEFT JOIN leads l ON CAST(msm.lead_id AS INTEGER) = l.id
|
|
62
|
+
AND l.created_at >= datetime('now', '-' || ? || ' days')
|
|
63
|
+
WHERE ms.is_active = 1 AND ms.client_vertical IN (?, 'general')
|
|
64
|
+
GROUP BY ms.id
|
|
65
|
+
HAVING member_count > 0
|
|
66
|
+
ORDER BY real_avg_ltv DESC
|
|
67
|
+
LIMIT 10
|
|
68
|
+
`).bind(period_days, vertical).all();
|
|
69
|
+
|
|
70
|
+
const segments = segmentsRes.results || [];
|
|
71
|
+
|
|
72
|
+
let globalLtv = 0, globalLeads = 0, globalConversions = 0;
|
|
73
|
+
if (segments.length === 0) {
|
|
74
|
+
const globalRes = await env.DB.prepare(`
|
|
75
|
+
SELECT COUNT(*) AS total_leads, AVG(predicted_ltv) AS avg_ltv,
|
|
76
|
+
SUM(CASE WHEN event_name IN ('Purchase','CompletePayment') THEN 1 ELSE 0 END) AS conversions
|
|
77
|
+
FROM leads
|
|
78
|
+
WHERE created_at >= datetime('now', '-' || ? || ' days')
|
|
79
|
+
AND (bot_score IS NULL OR bot_score < 2)
|
|
80
|
+
`).bind(period_days).first();
|
|
81
|
+
|
|
82
|
+
globalLeads = globalRes?.total_leads || 0;
|
|
83
|
+
globalLtv = globalRes?.avg_ltv || 0;
|
|
84
|
+
globalConversions = globalRes?.conversions || 0;
|
|
85
|
+
|
|
86
|
+
if (globalLeads < 10) {
|
|
87
|
+
return new Response(JSON.stringify({
|
|
88
|
+
error: `Dados insuficientes. Apenas ${globalLeads} leads no período de ${period_days} dias. Mínimo: 10.`,
|
|
89
|
+
leads_found: globalLeads, required: 10,
|
|
90
|
+
}), { status: 400, headers });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const now = new Date().toISOString();
|
|
95
|
+
const recommendations = [];
|
|
96
|
+
|
|
97
|
+
const targetSegments = segments.length > 0
|
|
98
|
+
? segments
|
|
99
|
+
: [{ 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
|
+
|
|
101
|
+
for (const seg of targetSegments) {
|
|
102
|
+
const avgLtv = parseFloat(seg.real_avg_ltv || 0);
|
|
103
|
+
const convs = parseInt(seg.conversions || 0);
|
|
104
|
+
const confidence = Math.min(1, convs / 100);
|
|
105
|
+
|
|
106
|
+
const estimatedLtv = avgLtv > 0 ? avgLtv :
|
|
107
|
+
seg.avg_ltv_class >= 0.7 ? 497 : seg.avg_ltv_class >= 0.4 ? 297 : 97;
|
|
108
|
+
|
|
109
|
+
const cpaTarget = estimatedLtv / target_roi;
|
|
110
|
+
const segMult = getSegmentMultiplier(seg.avg_ltv_class, seg.avg_behavior_score);
|
|
111
|
+
const confAdj = getConfidenceAdjustment(confidence);
|
|
112
|
+
const alertMsg = convs < 30 ? `Atenção: apenas ${convs} conversões no período. Bid baseado em estimativa de LTV — aplique com cautela.` : null;
|
|
113
|
+
|
|
114
|
+
for (const plat of platforms) {
|
|
115
|
+
const platFactor = PLATFORM_FACTORS[plat];
|
|
116
|
+
const recommendedBid = Math.max(5, cpaTarget * platFactor * segMult * confAdj);
|
|
117
|
+
const expectedRoi = estimatedLtv / (recommendedBid / platFactor);
|
|
118
|
+
|
|
119
|
+
const reasoning = `Segmento "${seg.cluster_name}": LTV=${estimatedLtv.toFixed(0)} BRL, ` +
|
|
120
|
+
`CPA_alvo=${cpaTarget.toFixed(0)} BRL, fator_plataforma=${platFactor}, ` +
|
|
121
|
+
`mult_segmento=${segMult}, ajuste_confiança=${confAdj}, ` +
|
|
122
|
+
`base: ${convs} conversões em ${period_days} dias.`;
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
await env.DB.prepare(`
|
|
126
|
+
INSERT INTO bid_recommendations (
|
|
127
|
+
generated_at, vertical, platform, period_days, target_roi,
|
|
128
|
+
segment_id, segment_name, leads_analyzed, conversions_found,
|
|
129
|
+
avg_ltv, cpa_target, recommended_bid, bid_currency,
|
|
130
|
+
confidence, expected_roi, reasoning, ai_used, alert_message,
|
|
131
|
+
platform_factor, confidence_adjustment, segment_multiplier, is_active
|
|
132
|
+
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,0,?,?,?,?,1)
|
|
133
|
+
`).bind(
|
|
134
|
+
now, vertical, plat, period_days, target_roi,
|
|
135
|
+
seg.segment_id || null, seg.cluster_name,
|
|
136
|
+
seg.member_count || globalLeads, convs,
|
|
137
|
+
estimatedLtv, cpaTarget, recommendedBid, 'BRL',
|
|
138
|
+
confidence, expectedRoi, reasoning, alertMsg || null,
|
|
139
|
+
platFactor, confAdj, segMult,
|
|
140
|
+
).run();
|
|
141
|
+
} catch (e) { console.error('[Bidding] D1 insert error:', e.message); }
|
|
142
|
+
|
|
143
|
+
recommendations.push({
|
|
144
|
+
platform: plat, segment: seg.cluster_name, segment_id: seg.segment_id || null,
|
|
145
|
+
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',
|
|
147
|
+
cpa_target: Math.round(cpaTarget * 100) / 100,
|
|
148
|
+
recommended_bid: Math.round(recommendedBid * 100) / 100,
|
|
149
|
+
bid_currency: 'BRL', confidence: Math.round(confidence * 100) / 100,
|
|
150
|
+
expected_roi: Math.round(expectedRoi * 100) / 100, reasoning, alert: alertMsg,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
await env.DB.prepare(`UPDATE bid_recommendations SET is_active = 0 WHERE vertical = ? AND generated_at < ? AND is_active = 1`).bind(vertical, now).run().catch(() => {});
|
|
156
|
+
|
|
157
|
+
const avgConfidence = recommendations.length > 0
|
|
158
|
+
? recommendations.reduce((s, r) => s + r.confidence, 0) / recommendations.length
|
|
159
|
+
: 0;
|
|
160
|
+
|
|
161
|
+
return new Response(JSON.stringify({
|
|
162
|
+
success: true, generated_at: now, vertical, period_days, target_roi,
|
|
163
|
+
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
|
+
segments_active: segments.length, confidence: Math.round(avgConfidence * 100) / 100,
|
|
167
|
+
},
|
|
168
|
+
recommendations,
|
|
169
|
+
global_summary: {
|
|
170
|
+
total_recommendations: recommendations.length,
|
|
171
|
+
avg_confidence: Math.round(avgConfidence * 100) / 100,
|
|
172
|
+
expected_cost_reduction: avgConfidence >= 0.7 ? '-20%' : avgConfidence >= 0.4 ? '-10%' : 'indefinido (dados insuficientes)',
|
|
173
|
+
segments_analyzed: segments.length,
|
|
174
|
+
},
|
|
175
|
+
}), { status: 200, headers });
|
|
176
|
+
|
|
177
|
+
} catch (err) {
|
|
178
|
+
console.error('[Bidding] recommend error:', err.message);
|
|
179
|
+
return new Response(JSON.stringify({ error: 'Erro ao gerar recomendações', message: err.message }), { status: 500, headers });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ── GET /api/bidding/history ──────────────────────────────────────────────────
|
|
184
|
+
export async function handleBiddingHistory(env, request, headers) {
|
|
185
|
+
if (!env.DB) return new Response(JSON.stringify({ error: 'DB não configurado' }), { status: 503, headers });
|
|
186
|
+
|
|
187
|
+
const url = new URL(request.url);
|
|
188
|
+
const vertical = url.searchParams.get('vertical') || null;
|
|
189
|
+
const platform = url.searchParams.get('platform') || null;
|
|
190
|
+
const limit = Math.min(parseInt(url.searchParams.get('limit') || '20'), 100);
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
const conditions = [];
|
|
194
|
+
const bindings = [];
|
|
195
|
+
if (vertical) { conditions.push('vertical = ?'); bindings.push(vertical); }
|
|
196
|
+
if (platform) { conditions.push('platform = ?'); bindings.push(platform); }
|
|
197
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
198
|
+
|
|
199
|
+
const result = await env.DB.prepare(`
|
|
200
|
+
SELECT id, generated_at, vertical, platform, period_days, target_roi,
|
|
201
|
+
segment_name, leads_analyzed, conversions_found, avg_ltv, cpa_target,
|
|
202
|
+
recommended_bid, bid_currency, confidence, expected_roi,
|
|
203
|
+
reasoning, alert_message, ai_used, is_active,
|
|
204
|
+
applied_at, applied_campaign, applied_result
|
|
205
|
+
FROM bid_recommendations
|
|
206
|
+
${where}
|
|
207
|
+
ORDER BY generated_at DESC
|
|
208
|
+
LIMIT ?
|
|
209
|
+
`).bind(...bindings, limit).all();
|
|
210
|
+
|
|
211
|
+
const items = (result.results || []).map(r => ({ ...r, applied_result: tryParseJson(r.applied_result, null) }));
|
|
212
|
+
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.message);
|
|
215
|
+
return new Response(JSON.stringify({ error: err.message }), { status: 500, headers });
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ── GET /api/bidding/status ───────────────────────────────────────────────────
|
|
220
|
+
export async function handleBiddingStatus(env, request, headers) {
|
|
221
|
+
if (!env.DB) return new Response(JSON.stringify({ error: 'DB não configurado' }), { status: 503, headers });
|
|
222
|
+
|
|
223
|
+
const url = new URL(request.url);
|
|
224
|
+
const vertical = url.searchParams.get('vertical') || null;
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
let query = `
|
|
228
|
+
SELECT platform, vertical, MAX(generated_at) as last_generated,
|
|
229
|
+
AVG(confidence) as avg_confidence, AVG(recommended_bid) as avg_bid,
|
|
230
|
+
COUNT(*) as recommendations_count,
|
|
231
|
+
SUM(CASE WHEN alert_message IS NOT NULL THEN 1 ELSE 0 END) as alerts_count
|
|
232
|
+
FROM bid_recommendations
|
|
233
|
+
WHERE is_active = 1
|
|
234
|
+
`;
|
|
235
|
+
const bindings = [];
|
|
236
|
+
if (vertical) { query += ' AND vertical = ?'; bindings.push(vertical); }
|
|
237
|
+
query += ' GROUP BY platform, vertical ORDER BY last_generated DESC';
|
|
238
|
+
|
|
239
|
+
const result = await env.DB.prepare(query).bind(...bindings).all();
|
|
240
|
+
return new Response(JSON.stringify({ success: true, status: result.results || [] }), { status: 200, headers });
|
|
241
|
+
} catch (err) {
|
|
242
|
+
console.error('[Bidding] status error:', err.message);
|
|
243
|
+
return new Response(JSON.stringify({ error: err.message }), { status: 500, headers });
|
|
244
|
+
}
|
|
245
|
+
}
|