/** * Slack Service — 3-Layer Data Delivery * * Layer 1: Daily Digest (1 rich message per day — summary table) * Layer 2: Real-time Alerts (only HOT leads 85+ — immediate) * Layer 3: Commands (/leads, /discover, /status, /pause, /quota) * * NOT Slack blast — organized, formatted, actionable. */ import axios from "axios"; import { getEnv } from "../shared/config/env"; import { getSupabaseClient } from "../shared/supabase/client"; import { logger } from "../shared/utils/logger"; // ─── Slack API helper ──────────────────────────────────────── async function postMessage(channelId: string, blocks: unknown[], text: string): Promise { const env = getEnv(); try { await axios.post("https://slack.com/api/chat.postMessage", { channel: channelId, text, blocks, }, { headers: { Authorization: `Bearer ${env.SLACK_BOT_TOKEN}` }, timeout: 5_000, }); } catch (err) { logger.warn({ err }, "Slack post failed — non-critical"); } } // ─── LAYER 1: Daily Digest ─────────────────────────────────── export async function sendDailyDigest(runSummary: { territory: string; industry: string; companiesSearched: number; leadsQualified: number; hotLeads: number; warmLeads: number; nurtureLeads: number; tokensUsed: number; durationMinutes: number; }): Promise { const env = getEnv(); const db = getSupabaseClient(); // Fetch today's qualified leads const today = new Date(); today.setHours(0, 0, 0, 0); const { data: leads } = await db .from("lead_scores") .select(` total_score, tier, companies (name, domain, industry, employee_count, city, service_match), contacts (full_name, title, email, email_verified, linkedin_personal_url) `) .gte("created_at", today.toISOString()) .order("total_score", { ascending: false }) .limit(20); // Build lead table const leadRows = (leads ?? []).map((lead: any, i: number) => { const emoji = lead.tier === "hot" ? "🔥" : lead.tier === "warm" ? "✅" : "📋"; const company = lead.companies; const contact = lead.contacts; const emailIcon = contact?.email_verified ? "📧✓" : contact?.email ? "📧" : "—"; const linkedinIcon = contact?.linkedin_personal_url ? "💼✓" : "—"; return `${emoji} *${company?.name ?? "Unknown"}* — ${lead.total_score}/100 ${lead.tier.toUpperCase()}\n` + ` ${company?.industry ?? "?"} · ${company?.employee_count ?? "?"} emp · ${company?.city ?? "?"}\n` + ` ${contact?.full_name ?? "?"} (${contact?.title ?? "?"})\n` + ` ${emailIcon} ${linkedinIcon} · Match: ${company?.service_match ?? "—"}`; }).join("\n\n"); const blocks = [ // Header { type: "header", text: { type: "plain_text", text: `📊 Daily Lead Report — ${formatDate(new Date())}` }, }, // Summary stats { type: "section", text: { type: "mrkdwn", text: `*Territory:* ${runSummary.territory} → ${runSummary.industry}\n` + `*Searched:* ${runSummary.companiesSearched} companies\n` + `*Qualified:* ${runSummary.leadsQualified} leads ` + `(🔥 ${runSummary.hotLeads} hot · ✅ ${runSummary.warmLeads} warm · 📋 ${runSummary.nurtureLeads} nurture)\n` + `*Duration:* ${runSummary.durationMinutes} min · *Tokens:* ${runSummary.tokensUsed.toLocaleString()}`, }, }, { type: "divider" }, // Lead list { type: "section", text: { type: "mrkdwn", text: leadRows || "_No qualified leads found today_", }, }, { type: "divider" }, // Actions { type: "context", elements: [ { type: "mrkdwn", text: "Type `/leads` for full details · `/discover region:UK` for manual run · `/status` for system status", }, ], }, ]; await postMessage(env.SLACK_ALERT_CHANNEL_ID, blocks, `Daily Report: ${runSummary.leadsQualified} leads found`); } // ─── LAYER 2: Hot Lead Alert (85+ only) ────────────────────── export async function sendHotLeadAlert(lead: { companyName: string; domain: string; industry: string; employeeCount: number | null; city: string | null; score: number; tier: string; contactName: string; contactTitle: string; email: string | null; emailVerified: boolean; linkedinPersonal: string | null; linkedinCompany: string | null; serviceMatch: string | null; outreachAngle: string; painPoints: string[]; socialProfiles: Record; }): Promise { const env = getEnv(); const emoji = lead.score >= 90 ? "🔥🔥🔥" : lead.score >= 85 ? "🔥🔥" : "🔥"; // Contact channels summary const channels: string[] = []; if (lead.email && lead.emailVerified) channels.push(`📧 ${lead.email} ✓`); else if (lead.email) channels.push(`📧 ${lead.email} (unverified)`); if (lead.linkedinPersonal) channels.push(`💼 <${lead.linkedinPersonal}|LinkedIn>`); if (lead.linkedinCompany) channels.push(`🏢 <${lead.linkedinCompany}|Company LI>`); if (lead.socialProfiles?.instagram) channels.push(`📷 <${lead.socialProfiles.instagram}|Instagram>`); if (lead.socialProfiles?.facebook) channels.push(`👥 <${lead.socialProfiles.facebook}|Facebook>`); const blocks = [ { type: "header", text: { type: "plain_text", text: `${emoji} HOT LEAD — ${lead.companyName}` }, }, { type: "section", fields: [ { type: "mrkdwn", text: `*Score:*\n${lead.score}/100 — ${lead.tier.toUpperCase()}` }, { type: "mrkdwn", text: `*Industry:*\n${lead.industry}` }, { type: "mrkdwn", text: `*Employees:*\n${lead.employeeCount ?? "Unknown"}` }, { type: "mrkdwn", text: `*Location:*\n${lead.city ?? "Unknown"}` }, { type: "mrkdwn", text: `*Service Match:*\n${lead.serviceMatch ?? "General"}` }, { type: "mrkdwn", text: `*Domain:*\n${lead.domain}` }, ], }, { type: "divider" }, { type: "section", text: { type: "mrkdwn", text: `*👤 Decision Maker:*\n${lead.contactName} — ${lead.contactTitle}\n\n` + `*📱 Channels:*\n${channels.join("\n") || "None found"}\n\n` + `*🎯 Outreach Angle:*\n_"${lead.outreachAngle}"_\n\n` + `*💢 Pain Points:*\n${lead.painPoints.map(p => `• ${p}`).join("\n")}`, }, }, ]; await postMessage(env.SLACK_ALERT_CHANNEL_ID, blocks, `🔥 HOT LEAD: ${lead.companyName} — Score ${lead.score}`); } // ─── LAYER 2: Run Progress Updates ────────────────────────── export async function sendRunStarted(territory: string, industry: string, quota: number): Promise { const env = getEnv(); await postMessage(env.SLACK_ALERT_CHANNEL_ID, [ { type: "section", text: { type: "mrkdwn", text: `🚀 *Daily run started*\n` + `Territory: ${territory} → ${industry}\n` + `Quota: ${quota} leads\n` + `Estimated: ~90 min`, }, }, ], `Run started: ${territory} ${industry}`); } export async function sendRunProgress(qualified: number, quota: number, searched: number): Promise { const env = getEnv(); const progress = Math.round((qualified / quota) * 100); const bar = "█".repeat(Math.round(progress / 10)) + "░".repeat(10 - Math.round(progress / 10)); await postMessage(env.SLACK_ALERT_CHANNEL_ID, [ { type: "section", text: { type: "mrkdwn", text: `📊 *Progress:* ${qualified}/${quota} qualified [${bar}] ${progress}%\n` + `Searched: ${searched} companies`, }, }, ], `Progress: ${qualified}/${quota}`); } // ─── LAYER 3: Clarifying Questions ────────────────────────── export async function sendClarifyingQuestions( userMessage: string, questions: { question: string; options: string[] }[] ): Promise { const env = getEnv(); const blocks: unknown[] = [ { type: "section", text: { type: "mrkdwn", text: `🤔 *Got it: "${userMessage}"*\nMujhe kuch clarify karna hai:`, }, }, ]; for (const q of questions) { blocks.push({ type: "section", text: { type: "mrkdwn", text: `*${q.question}*\n${q.options.map((o, i) => `${i + 1}. ${o}`).join("\n")}`, }, }); } blocks.push({ type: "context", elements: [{ type: "mrkdwn", text: "Just reply with numbers (e.g., `1 2 3`) or type your own answer", }], }); await postMessage(env.SLACK_ALERT_CHANNEL_ID, blocks, "Clarifying questions for manual discovery"); } // ─── Helpers ───────────────────────────────────────────────── function formatDate(date: Date): string { return date.toLocaleDateString("en-US", { weekday: "long", year: "numeric", month: "long", day: "numeric", }); }