|
|
|
|
@ -663,6 +663,110 @@ export class GroupSyncService {
|
|
|
|
|
}
|
|
|
|
|
const ratio = Math.max(0, Math.min(1, total > 0 ? resolvable / total : 1));
|
|
|
|
|
try { Metrics.set('alias_coverage_ratio', ratio, { group_id: groupId }); } catch {}
|
|
|
|
|
|
|
|
|
|
// A3: publicación condicional del mensaje de onboarding (sin spam)
|
|
|
|
|
try {
|
|
|
|
|
// Flags y parámetros
|
|
|
|
|
const isTest = String(process.env.NODE_ENV || '').toLowerCase() === 'test';
|
|
|
|
|
const enabled =
|
|
|
|
|
isTest
|
|
|
|
|
? String(process.env.ONBOARDING_ENABLE_IN_TEST || '').toLowerCase() === 'true'
|
|
|
|
|
: (() => {
|
|
|
|
|
const v = process.env.ONBOARDING_PROMPTS_ENABLED;
|
|
|
|
|
return v == null ? true : ['true', '1', 'yes'].includes(String(v).toLowerCase());
|
|
|
|
|
})();
|
|
|
|
|
|
|
|
|
|
if (!enabled) {
|
|
|
|
|
try { Metrics.inc('onboarding_prompts_skipped_total', 1, { group_id: groupId, reason: 'disabled' }); } catch {}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const thrRaw = Number(process.env.ONBOARDING_COVERAGE_THRESHOLD);
|
|
|
|
|
const threshold = Number.isFinite(thrRaw) ? Math.min(1, Math.max(0, thrRaw)) : 1;
|
|
|
|
|
if (ratio >= threshold) {
|
|
|
|
|
try { Metrics.inc('onboarding_prompts_skipped_total', 1, { group_id: groupId, reason: 'coverage_100' }); } catch {}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Gating en modo enforce: no publicar en grupos no allowed
|
|
|
|
|
try {
|
|
|
|
|
const mode = String(process.env.GROUP_GATING_MODE || 'off').toLowerCase();
|
|
|
|
|
if (mode === 'enforce') {
|
|
|
|
|
try { (AllowedGroups as any).dbInstance = this.dbInstance; } catch {}
|
|
|
|
|
if (!AllowedGroups.isAllowed(groupId)) {
|
|
|
|
|
try { Metrics.inc('onboarding_prompts_skipped_total', 1, { group_id: groupId, reason: 'not_allowed' }); } catch {}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch {}
|
|
|
|
|
|
|
|
|
|
// Grace y cooldown desde tabla groups
|
|
|
|
|
const rowG = this.dbInstance.prepare(`
|
|
|
|
|
SELECT last_verified, onboarding_prompted_at
|
|
|
|
|
FROM groups
|
|
|
|
|
WHERE id = ?
|
|
|
|
|
`).get(groupId) as any;
|
|
|
|
|
|
|
|
|
|
const nowMs = Date.now();
|
|
|
|
|
const graceRaw = Number(process.env.ONBOARDING_GRACE_SECONDS);
|
|
|
|
|
const graceSec = Number.isFinite(graceRaw) && graceRaw >= 0 ? Math.floor(graceRaw) : 90;
|
|
|
|
|
|
|
|
|
|
const lv = rowG?.last_verified ? String(rowG.last_verified) : null;
|
|
|
|
|
if (lv) {
|
|
|
|
|
const iso = lv.includes('T') ? lv : (lv.replace(' ', 'T') + 'Z');
|
|
|
|
|
const ms = Date.parse(iso);
|
|
|
|
|
if (Number.isFinite(ms)) {
|
|
|
|
|
const ageSec = Math.floor((nowMs - ms) / 1000);
|
|
|
|
|
if (ageSec < graceSec) {
|
|
|
|
|
try { Metrics.inc('onboarding_prompts_skipped_total', 1, { group_id: groupId, reason: 'grace_period' }); } catch {}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const cdRaw = Number(process.env.ONBOARDING_COOLDOWN_DAYS);
|
|
|
|
|
const cdDays = Number.isFinite(cdRaw) && cdRaw >= 0 ? Math.floor(cdRaw) : 7;
|
|
|
|
|
const promptedAt = rowG?.onboarding_prompted_at ? String(rowG.onboarding_prompted_at) : null;
|
|
|
|
|
if (promptedAt) {
|
|
|
|
|
const iso = promptedAt.includes('T') ? promptedAt : (promptedAt.replace(' ', 'T') + 'Z');
|
|
|
|
|
const ms = Date.parse(iso);
|
|
|
|
|
if (Number.isFinite(ms)) {
|
|
|
|
|
const diffMs = nowMs - ms;
|
|
|
|
|
if (diffMs < cdDays * 24 * 60 * 60 * 1000) {
|
|
|
|
|
try { Metrics.inc('onboarding_prompts_skipped_total', 1, { group_id: groupId, reason: 'cooldown_active' }); } catch {}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Número del bot para construir wa.me
|
|
|
|
|
const bot = String(process.env.CHATBOT_PHONE_NUMBER || '').trim();
|
|
|
|
|
if (!bot || !/^\d+$/.test(bot)) {
|
|
|
|
|
try { Metrics.inc('onboarding_prompts_skipped_total', 1, { group_id: groupId, reason: 'missing_bot_number' }); } catch {}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Encolar mensaje en la cola persistente y marcar timestamp en groups
|
|
|
|
|
const msg = `Para poder asignarte tareas y acceder a la web, envía 'hola' al bot por privado. Solo hace falta una vez. Enlace: https://wa.me/${bot}`;
|
|
|
|
|
this.dbInstance.transaction(() => {
|
|
|
|
|
this.dbInstance.prepare(`
|
|
|
|
|
INSERT INTO response_queue (recipient, message, status, attempts, metadata, created_at, updated_at, next_attempt_at)
|
|
|
|
|
VALUES (?, ?, 'queued', 0, NULL, strftime('%Y-%m-%d %H:%M:%f','now'), strftime('%Y-%m-%d %H:%M:%f','now'), strftime('%Y-%m-%d %H:%M:%f','now'))
|
|
|
|
|
`).run(groupId, msg);
|
|
|
|
|
this.dbInstance.prepare(`
|
|
|
|
|
UPDATE groups
|
|
|
|
|
SET onboarding_prompted_at = strftime('%Y-%m-%d %H:%M:%f','now')
|
|
|
|
|
WHERE id = ?
|
|
|
|
|
`).run(groupId);
|
|
|
|
|
})();
|
|
|
|
|
|
|
|
|
|
try { Metrics.inc('onboarding_prompts_sent_total', 1, { group_id: groupId, reason: 'coverage_below_threshold' }); } catch {}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
// Evitar romper el flujo si falla el encolado
|
|
|
|
|
if (process.env.NODE_ENV !== 'test') {
|
|
|
|
|
console.warn('⚠️ Onboarding prompt skipped due to internal error for', groupId, e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.warn('⚠️ No se pudo calcular alias_coverage_ratio para', groupId, e);
|
|
|
|
|
}
|
|
|
|
|
|