     1→/**
     2→ * SMS Sending API routes
     3→ */
     4→
     5→const express = require('express');
     6→const router = express.Router();
     7→const { supabase } = require('../config/database');
     8→const { sendSMS, sendBatchSMS, isConfigured } = require('../services/twilio');
     9→const { getTemplate, getAllTemplates, renderTemplate, buildLeadVariables } = require('../services/templates');
    10→const { normalizeAustralianPhone } = require('../utils/phone');
    11→const { validateSMSLength, isValidUUID, isValidAustralianPhone } = require('../utils/validation');
    12→const { smsLimiter, smsBatchLimiter } = require('../middleware/rateLimiter');
    13→const { awardXPForSMS } = require('../services/xpService');
    14→
    15→/**
    16→ * GET /api/sms/templates
    17→ * List all active SMS templates
    18→ */
    19→router.get('/templates', async (req, res) => {
    20→  try {
    21→    const { category } = req.query;
    22→
    23→    let query = supabase
    24→      .from('sms_templates')
    25→      .select('*')
    26→      .eq('is_active', true)
    27→      .order('name');
    28→
    29→    // Note: category column not in schema yet, filter disabled
    30→    // if (category) {
    31→    //   query = query.eq('category', category);
    32→    // }
    33→
    34→    const { data, error } = await query;
    35→
    36→    if (error) {
    37→      console.error('Templates fetch error:', error);
    38→      return res.status(500).json({ error: 'Failed to fetch templates' });
    39→    }
    40→
    41→    res.json({ templates: data || [] });
    42→  } catch (error) {
    43→    console.error('Templates list error:', error);
    44→    res.status(500).json({ error: 'Failed to fetch templates' });
    45→  }
    46→});
    47→
    48→/**
    49→ * GET /api/sms/templates/:id
    50→ * Get a single template
    51→ */
    52→router.get('/templates/:id', async (req, res) => {
    53→  try {
    54→    const { id } = req.params;
    55→
    56→    const { data, error } = await supabase
    57→      .from('sms_templates')
    58→      .select('*')
    59→      .eq('id', id)
    60→      .single();
    61→
    62→    if (error || !data) {
    63→      return res.status(404).json({ error: 'Template not found' });
    64→    }
    65→
    66→    res.json({ template: data });
    67→  } catch (error) {
    68→    console.error('Template fetch error:', error);
    69→    res.status(500).json({ error: 'Failed to fetch template' });
    70→  }
    71→});
    72→
    73→/**
    74→ * POST /api/sms/templates
    75→ * Create a new template
    76→ */
    77→router.post('/templates', async (req, res) => {
    78→  try {
    79→    const { name, category, content, variables } = req.body;
    80→
    81→    if (!name || !content) {
    82→      return res.status(400).json({ error: 'name and content are required' });
    83→    }
    84→
    85→    // Generate slug from name
    86→    const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_|_$/g, '');
    87→
    88→    // Extract variables from content ({{variable_name}})
    89→    const extractedVars = [];
    90→    const varRegex = /\{\{\s*(\w+)\s*\}\}/g;
    91→    let match;
    92→    while ((match = varRegex.exec(content)) !== null) {
    93→      if (!extractedVars.includes(match[1])) {
    94→        extractedVars.push(match[1]);
    95→      }
    96→    }
    97→
    98→    const { data, error } = await supabase
    99→      .from('sms_templates')
   100→      .insert({
   101→        name,
   102→        slug,
   103→        category: category || 'general',
   104→        content,
   105→        variables: variables || extractedVars,
   106→        is_active: true
   107→      })
   108→      .select()
   109→      .single();
   110→
   111→    if (error) {
   112→      console.error('Template create error:', error);
   113→      return res.status(500).json({ error: 'Failed to create template' });
   114→    }
   115→
   116→    res.json({ template: data });
   117→  } catch (error) {
   118→    console.error('Template create error:', error);
   119→    res.status(500).json({ error: 'Failed to create template' });
   120→  }
   121→});
   122→
   123→/**
   124→ * PUT /api/sms/templates/:id
   125→ * Update a template
   126→ */
   127→router.put('/templates/:id', async (req, res) => {
   128→  try {
   129→    const { id } = req.params;
   130→    const { name, category, content, is_active } = req.body;
   131→
   132→    const updates = { updated_at: new Date().toISOString() };
   133→
   134→    if (name !== undefined) {
   135→      updates.name = name;
   136→      updates.slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_|_$/g, '');
   137→    }
   138→    if (category !== undefined) updates.category = category;
   139→    if (content !== undefined) {
   140→      updates.content = content;
   141→      // Re-extract variables
   142→      const extractedVars = [];
   143→      const varRegex = /\{\{\s*(\w+)\s*\}\}/g;
   144→      let match;
   145→      while ((match = varRegex.exec(content)) !== null) {
   146→        if (!extractedVars.includes(match[1])) {
   147→          extractedVars.push(match[1]);
   148→        }
   149→      }
   150→      updates.variables = extractedVars;
   151→    }
   152→    if (is_active !== undefined) updates.is_active = is_active;
   153→
   154→    const { data, error } = await supabase
   155→      .from('sms_templates')
   156→      .update(updates)
   157→      .eq('id', id)
   158→      .select()
   159→      .single();
   160→
   161→    if (error) {
   162→      console.error('Template update error:', error);
   163→      return res.status(500).json({ error: 'Failed to update template' });
   164→    }
   165→
   166→    res.json({ template: data });
   167→  } catch (error) {
   168→    console.error('Template update error:', error);
   169→    res.status(500).json({ error: 'Failed to update template' });
   170→  }
   171→});
   172→
   173→/**
   174→ * DELETE /api/sms/templates/:id
   175→ * Soft delete (deactivate) a template
   176→ */
   177→router.delete('/templates/:id', async (req, res) => {
   178→  try {
   179→    const { id } = req.params;
   180→
   181→    const { error } = await supabase
   182→      .from('sms_templates')
   183→      .update({ is_active: false, updated_at: new Date().toISOString() })
   184→      .eq('id', id);
   185→
   186→    if (error) {
   187→      console.error('Template delete error:', error);
   188→      return res.status(500).json({ error: 'Failed to delete template' });
   189→    }
   190→
   191→    res.json({ success: true });
   192→  } catch (error) {
   193→    console.error('Template delete error:', error);
   194→    res.status(500).json({ error: 'Failed to delete template' });
   195→  }
   196→});
   197→
   198→/**
   199→ * POST /api/sms/templates/:id/use
   200→ * Increment template use count
   201→ */
   202→router.post('/templates/:id/use', async (req, res) => {
   203→  try {
   204→    const { id } = req.params;
   205→
   206→    const { data, error } = await supabase.rpc('increment_template_use', { template_id: id });
   207→
   208→    // Fallback if RPC doesn't exist
   209→    if (error && error.code === '42883') {
   210→      await supabase
   211→        .from('sms_templates')
   212→        .update({ use_count: supabase.raw('use_count + 1') })
   213→        .eq('id', id);
   214→    }
   215→
   216→    res.json({ success: true });
   217→  } catch (error) {
   218→    console.error('Template use count error:', error);
   219→    res.status(500).json({ error: 'Failed to update use count' });
   220→  }
   221→});
   222→
   223→/**
   224→ * POST /api/sms/send
   225→ * Send a single SMS (or schedule for later)
   226→ * Body: { leadId, phone, templateSlug, message, variables, scheduledAt }
   227→ */
   228→router.post('/send', smsLimiter, async (req, res) => {
   229→  try {
   230→    if (!isConfigured()) {
   231→      return res.status(503).json({ error: 'Twilio not configured' });
   232→    }
   233→
   234→    const { leadId, phone, templateSlug, message, variables = {}, scheduledAt } = req.body;
   235→
   236→    // Must have either leadId or phone
   237→    if (!leadId && !phone) {
   238→      return res.status(400).json({ success: false, error: 'Either leadId or phone is required' });
   239→    }
   240→
   241→    // Validate leadId format if provided
   242→    if (leadId && !isValidUUID(leadId)) {
   243→      return res.status(400).json({ success: false, error: 'Invalid leadId format' });
   244→    }
   245→
   246→    // Validate phone format if provided directly
   247→    if (phone && !isValidAustralianPhone(phone)) {
   248→      return res.status(400).json({ success: false, error: 'Invalid Australian phone number format' });
   249→    }
   250→
   251→    // Must have either templateSlug or message
   252→    if (!templateSlug && !message) {
   253→      return res.status(400).json({ success: false, error: 'Either templateSlug or message is required' });
   254→    }
   255→
   256→    // Validate message length if provided directly
   257→    if (message) {
   258→      const smsValidation = validateSMSLength(message);
   259→      if (!smsValidation.valid) {
   260→        return res.status(400).json({
   261→          success: false,
   262→          error: `Message too long (${smsValidation.length} chars). Maximum is 1600 characters.`
   263→        });
   264→      }
   265→    }
   266→
   267→    // Get lead if leadId provided
   268→    let lead = null;
   269→    let toPhone = phone ? normalizeAustralianPhone(phone) : null;
   270→
   271→    if (leadId && supabase) {
   272→      const { data, error } = await supabase
   273→        .from('leads')
   274→        .select('*')
   275→        .eq('id', leadId)
   276→        .single();
   277→
   278→      if (error || !data) {
   279→        return res.status(404).json({ error: 'Lead not found' });
   280→      }
   281→
   282→      lead = data;
   283→      toPhone = normalizeAustralianPhone(lead.phone) || lead.phone;
   284→    }
   285→
   286→    if (!toPhone) {
   287→      return res.status(400).json({ error: 'No valid phone number' });
   288→    }
   289→
   290→    // Validate phone format before sending
   291→    if (!toPhone.startsWith('+61')) {
   292→      return res.status(400).json({
   293→        error: `Invalid phone format: ${toPhone}. Expected Australian mobile (+61...)`
   294→      });
   295→    }
   296→
   297→    // Build message content
   298→    let messageContent = message;
   299→
   300→    if (templateSlug) {
   301→      const template = await getTemplate(templateSlug);
   302→      if (!template) {
   303→        return res.status(404).json({ error: `Template '${templateSlug}' not found` });
   304→      }
   305→
   306→      const leadVars = buildLeadVariables(lead);
   307→      const allVars = { ...leadVars, ...variables };
   308→      messageContent = renderTemplate(template.content, allVars);
   309→    }
   310→
   311→    // Check if this is a scheduled message
   312→    if (scheduledAt) {
   313→      const scheduledTime = new Date(scheduledAt);
   314→      if (scheduledTime <= new Date()) {
   315→        return res.status(400).json({ error: 'Scheduled time must be in the future' });
   316→      }
   317→
   318→      // Save as scheduled message
   319→      if (supabase) {
   320→        const { data: scheduled, error: scheduleError } = await supabase
   321→          .from('communications')
   322→          .insert({
   323→            lead_id: lead?.id || null,
   324→            type: 'sms_outbound',
   325→            direction: 'outbound',
   326→            phone: toPhone,
   327→            content: messageContent,
   328→            status: 'scheduled',
   329→            scheduled_at: scheduledTime.toISOString(),
   330→            metadata: {
   331→              template_slug: templateSlug || null,
   332→              variables: variables
   333→            }
   334→          })
   335→          .select()
   336→          .single();
   337→
   338→        if (scheduleError) {
   339→          console.error('Failed to schedule SMS:', scheduleError);
   340→          return res.status(500).json({ error: 'Failed to schedule message' });
   341→        }
   342→
   343→        console.log(`SMS scheduled for ${scheduledTime.toISOString()} to ${toPhone}`);
   344→
   345→        return res.json({
   346→          success: true,
   347→          scheduled: true,
   348→          scheduledAt: scheduledTime.toISOString(),
   349→          messageId: scheduled.id
   350→        });
   351→      }
   352→    }
   353→
   354→    // Send SMS immediately
   355→    const result = await sendSMS(toPhone, messageContent);
   356→
   357→    if (!result.success) {
   358→      console.error(`SMS send failed to ${toPhone}:`, result.error);
   359→      const userMessage = result.error.includes('not a valid phone')
   360→        ? 'Invalid phone number format. Please check the lead\'s phone number.'
   361→        : result.error.includes('blacklist')
   362→        ? 'This number has been blocked from receiving SMS.'
   363→        : `Failed to send SMS: ${result.error}`;
   364→      return res.status(500).json({ error: userMessage });
   365→    }
   366→
   367→    // Log communication
   368→    const userId = req.user?.id || null;
   369→    if (supabase) {
   370→      const { data: insertedMsg, error: insertError } = await supabase
   371→        .from('communications')
   372→        .insert({
   373→          lead_id: lead?.id || null,
   374→          type: 'sms_outbound',
   375→          direction: 'outbound',
   376→          phone: toPhone,
   377→          content: messageContent,
   378→          status: 'sent',
   379→          external_id: result.messageSid,
   380→          user_id: userId, // User who sent the SMS (for assignment tracking)
   381→          metadata: {
   382→            template_slug: templateSlug || null,
   383→            variables: variables
   384→          }
   385→        })
   386→        .select('id')
   387→        .single();
   388→
   389→      // Calculate response time if this is a reply to an inbound message
   390→      if (insertedMsg && lead?.id) {
   391→        try {
   392→          const { data: lastInbound } = await supabase
   393→            .from('communications')
   394→            .select('created_at')
   395→            .eq('lead_id', lead.id)
   396→            .eq('direction', 'inbound')
   397→            .order('created_at', { ascending: false })
   398→            .limit(1)
   399→            .single();
   400→
   401→          if (lastInbound) {
   402→            const responseTimeSeconds = Math.floor(
   403→              (Date.now() - new Date(lastInbound.created_at).getTime()) / 1000
   404→            );
   405→
   406→            // Update the outbound message with response time
   407→            await supabase
   408→              .from('communications')
   409→              .update({ response_time_seconds: responseTimeSeconds })
   410→              .eq('id', insertedMsg.id);
   411→
   412→            console.log(`Response time tracked: ${responseTimeSeconds}s for lead ${lead.id}`);
   413→          }
   414→        } catch (rtError) {
   415→          console.warn('Response time tracking failed:', rtError.message);
   416→        }
   417→      }
   418→
   419→      // Update lead last_contact
   420→      if (lead) {
   421→        await supabase
   422→          .from('leads')
   423→          .update({
   424→            last_contact_at: new Date().toISOString(),
   425→            last_contact_type: 'sms_outbound'
   426→          })
   427→          .eq('id', lead.id);
   428→      }
   429→    }
   430→
   431→    console.log(`SMS sent to ${toPhone}: ${result.messageSid}`);
   432→
   433→    // Award XP for sending SMS (fire and forget)
   434→    let xpResult = null;
   435→    try {
   436→      xpResult = await awardXPForSMS(userId, lead?.id);
   437→      if (xpResult.success) {
   438→        console.log(`XP awarded for SMS: +${xpResult.totalAwarded} XP`);
   439→      }
   440→    } catch (xpError) {
   441→      console.warn('Failed to award XP:', xpError.message);
   442→    }
   443→
   444→    res.json({
   445→      success: true,
   446→      messageSid: result.messageSid,
   447→      to: toPhone,
   448→      content: messageContent,
   449→      xp: xpResult?.success ? {
   450→        awarded: xpResult.totalAwarded,
   451→        streakBonus: xpResult.streakBonus,
   452→        newTotal: xpResult.newTotal,
   453→        level: xpResult.level,
   454→        levelName: xpResult.levelName,
   455→        levelUp: xpResult.levelUp,
   456→      } : null,
   457→    });
   458→
   459→  } catch (error) {
   460→    console.error('SMS send error:', error);
   461→    res.status(500).json({ error: 'Failed to send SMS' });
   462→  }
   463→});
   464→
   465→/**
   466→ * POST /api/sms/send-batch
   467→ * Send personalized SMS to multiple leads
   468→ * Body: { messages: [{ leadId, message }] }
   469→ */
   470→router.post('/send-batch', smsLimiter, async (req, res) => {
   471→  try {
   472→    if (!isConfigured()) {
   473→      return res.status(503).json({ error: 'Twilio not configured' });
   474→    }
   475→
   476→    const { messages } = req.body;
   477→
   478→    if (!messages || !Array.isArray(messages) || messages.length === 0) {
   479→      return res.status(400).json({ error: 'messages array required' });
   480→    }
   481→
   482→    // Limit batch size
   483→    if (messages.length > 10) {
   484→      return res.status(400).json({ error: 'Maximum 10 messages per batch' });
   485→    }
   486→
   487→    const results = {
   488→      sent: 0,
   489→      failed: 0,
   490→      details: []
   491→    };
   492→
   493→    // Process each message
   494→    for (const msg of messages) {
   495→      const { leadId, message } = msg;
   496→
   497→      try {
   498→        // Get lead
   499→        const { data: lead, error: leadError } = await supabase
   500→          .from('leads')
   501→          .select('id, first_name, last_name, phone')
   502→          .eq('id', leadId)
   503→          .single();
   504→
   505→        if (leadError || !lead || !lead.phone) {
   506→          results.failed++;
   507→          results.details.push({
   508→            leadId,
   509→            success: false,
   510→            error: 'Lead not found or no phone'
   511→          });
   512→          continue;
   513→        }
   514→
   515→        // Normalize phone
   516→        let toPhone = lead.phone;
   517→        if (!toPhone.startsWith('+')) {
   518→          toPhone = toPhone.startsWith('0')
   519→            ? `+61${toPhone.slice(1)}`
   520→            : `+61${toPhone}`;
   521→        }
   522→
   523→        // Send SMS
   524→        const result = await sendSMS(toPhone, message);
   525→
   526→        if (!result.success) {
   527→          results.failed++;
   528→          results.details.push({
   529→            leadId,
   530→            success: false,
   531→            error: result.error
   532→          });
   533→          continue;
   534→        }
   535→
   536→        // Log communication
   537→        await supabase.from('communications').insert({
   538→          lead_id: lead.id,
   539→          type: 'sms_outbound',
   540→          direction: 'outbound',
   541→          phone: toPhone,
   542→          content: message,
   543→          status: 'sent',
   544→          external_id: result.messageSid,
   545→          metadata: {
   546→            batch_send: true,
   547→            source: 'similar_leads'
   548→          }
   549→        });
   550→
   551→        // Update lead last_contact
   552→        await supabase
   553→          .from('leads')
   554→          .update({
   555→            last_contact_at: new Date().toISOString(),
   556→            last_contact_type: 'sms_outbound'
   557→          })
   558→          .eq('id', lead.id);
   559→
   560→        results.sent++;
   561→        results.details.push({
   562→          leadId,
   563→          success: true,
   564→          messageSid: result.messageSid,
   565→          name: `${lead.first_name || ''} ${lead.last_name || ''}`.trim()
   566→        });
   567→
   568→        // Small delay between messages to avoid rate limits
   569→        await new Promise(resolve => setTimeout(resolve, 200));
   570→
   571→      } catch (err) {
   572→        results.failed++;
   573→        results.details.push({
   574→          leadId,
   575→          success: false,
   576→          error: err.message
   577→        });
   578→      }
   579→    }
   580→
   581→    console.log(`Batch SMS: ${results.sent} sent, ${results.failed} failed`);
   582→
   583→    res.json({
   584→      success: true,
   585→      sent: results.sent,
   586→      failed: results.failed,
   587→      details: results.details
   588→    });
   589→
   590→  } catch (error) {
   591→    console.error('Batch SMS send error:', error);
   592→    res.status(500).json({ error: 'Failed to send batch SMS' });
   593→  }
   594→});
   595→
   596→/**
   597→ * GET /api/sms/scheduled
   598→ * List all scheduled messages
   599→ */
   600→router.get('/scheduled', async (req, res) => {
   601→  try {
   602→    const { leadId } = req.query;
   603→
   604→    let query = supabase
   605→      .from('communications')
   606→      .select(`
   607→        id,
   608→        lead_id,
   609→        phone,
   610→        content,
   611→        scheduled_at,
   612→        created_at,
   613→        leads (
   614→          first_name,
   615→          last_name,
   616→          company
   617→        )
   618→      `)
   619→      .eq('status', 'scheduled')
   620→      .order('scheduled_at', { ascending: true });
   621→
   622→    if (leadId) {
   623→      query = query.eq('lead_id', leadId);
   624→    }
   625→
   626→    const { data, error } = await query;
   627→
   628→    if (error) {
   629→      console.error('Failed to fetch scheduled messages:', error);
   630→      return res.status(500).json({ error: 'Failed to fetch scheduled messages' });
   631→    }
   632→
   633→    res.json({ scheduled: data || [] });
   634→  } catch (error) {
   635→    console.error('Scheduled messages error:', error);
   636→    res.status(500).json({ error: 'Failed to fetch scheduled messages' });
   637→  }
   638→});
   639→
   640→/**
   641→ * DELETE /api/sms/scheduled/:id
   642→ * Cancel a scheduled message
   643→ */
   644→router.delete('/scheduled/:id', async (req, res) => {
   645→  try {
   646→    const { id } = req.params;
   647→
   648→    // Verify it's a scheduled message
   649→    const { data: existing, error: fetchError } = await supabase
   650→      .from('communications')
   651→      .select('id, status')
   652→      .eq('id', id)
   653→      .single();
   654→
   655→    if (fetchError || !existing) {
   656→      return res.status(404).json({ error: 'Scheduled message not found' });
   657→    }
   658→
   659→    if (existing.status !== 'scheduled') {
   660→      return res.status(400).json({ error: 'Message is not scheduled or has already been sent' });
   661→    }
   662→
   663→    // Delete the scheduled message
   664→    const { error: deleteError } = await supabase
   665→      .from('communications')
   666→      .delete()
   667→      .eq('id', id);
   668→
   669→    if (deleteError) {
   670→      console.error('Failed to cancel scheduled message:', deleteError);
   671→      return res.status(500).json({ error: 'Failed to cancel scheduled message' });
   672→    }
   673→
   674→    res.json({ success: true, message: 'Scheduled message cancelled' });
   675→  } catch (error) {
   676→    console.error('Cancel scheduled error:', error);
   677→    res.status(500).json({ error: 'Failed to cancel scheduled message' });
   678→  }
   679→});
   680→
   681→/**
   682→ * POST /api/sms/batch
   683→ * Send SMS to multiple leads
   684→ * Body: { leadIds, templateSlug, message, variables }
   685→ * Rate limited to 10 batch operations per hour
   686→ */
   687→router.post('/batch', smsBatchLimiter, async (req, res) => {
   688→  try {
   689→    if (!isConfigured()) {
   690→      return res.status(503).json({ error: 'Twilio not configured' });
   691→    }
   692→
   693→    if (!supabase) {
   694→      return res.status(503).json({ error: 'Database not configured' });
   695→    }
   696→
   697→    const { leadIds, templateSlug, message, variables = {} } = req.body;
   698→
   699→    if (!leadIds || !Array.isArray(leadIds) || leadIds.length === 0) {
   700→      return res.status(400).json({ error: 'leadIds array is required' });
   701→    }
   702→
   703→    if (!templateSlug && !message) {
   704→      return res.status(400).json({ error: 'Either templateSlug or message is required' });
   705→    }
   706→
   707→    // Limit batch size
   708→    if (leadIds.length > 100) {
   709→      return res.status(400).json({ error: 'Maximum 100 leads per batch' });
   710→    }
   711→
   712→    // Get template if specified
   713→    let template = null;
   714→    if (templateSlug) {
   715→      template = await getTemplate(templateSlug);
   716→      if (!template) {
   717→        return res.status(404).json({ error: `Template '${templateSlug}' not found` });
   718→      }
   719→    }
   720→
   721→    // Get leads
   722→    const { data: leads, error } = await supabase
   723→      .from('leads')
   724→      .select('*')
   725→      .in('id', leadIds)
   726→      .is('deleted_at', null)
   727→      .not('phone', 'is', null);
   728→
   729→    if (error) {
   730→      return res.status(500).json({ error: 'Failed to fetch leads' });
   731→    }
   732→
   733→    if (!leads || leads.length === 0) {
   734→      return res.status(404).json({ error: 'No valid leads found' });
   735→    }
   736→
   737→    // Build messages
   738→    const messages = leads.map(lead => {
   739→      let content = message;
   740→
   741→      if (template) {
   742→        const leadVars = buildLeadVariables(lead);
   743→        const allVars = { ...leadVars, ...variables };
   744→        content = renderTemplate(template.content, allVars);
   745→      }
   746→
   747→      return {
   748→        to: lead.phone,
   749→        body: content,
   750→        leadId: lead.id
   751→      };
   752→    });
   753→
   754→    // Send batch
   755→    const results = [];
   756→    let sent = 0;
   757→    let failed = 0;
   758→
   759→    for (const msg of messages) {
   760→      const result = await sendSMS(msg.to, msg.body);
   761→
   762→      if (result.success) {
   763→        sent++;
   764→
   765→        // Log communication
   766→        await supabase
   767→          .from('communications')
   768→          .insert({
   769→            lead_id: msg.leadId,
   770→            type: 'sms_outbound',
   771→            direction: 'outbound',
   772→            phone: msg.to,
   773→            content: msg.body,
   774→            external_id: result.messageSid,
   775→            metadata: {
   776→              template_slug: templateSlug || null,
   777→              batch: true
   778→            }
   779→          });
   780→
   781→        // Update lead
   782→        await supabase
   783→          .from('leads')
   784→          .update({
   785→            last_contact_at: new Date().toISOString(),
   786→            last_contact_type: 'sms_outbound'
   787→          })
   788→          .eq('id', msg.leadId);
   789→
   790→        results.push({ leadId: msg.leadId, success: true, messageSid: result.messageSid });
   791→      } else {
   792→        failed++;
   793→        results.push({ leadId: msg.leadId, success: false, error: result.error });
   794→      }
   795→
   796→      // Small delay between messages
   797→      await new Promise(resolve => setTimeout(resolve, 100));
   798→    }
   799→
   800→    console.log(`Batch SMS: ${sent} sent, ${failed} failed`);
   801→
   802→    res.json({
   803→      sent,
   804→      failed,
   805→      total: messages.length,
   806→      results
   807→    });
   808→
   809→  } catch (error) {
   810→    console.error('Batch SMS error:', error);
   811→    res.status(500).json({ error: 'Failed to send batch SMS' });
   812→  }
   813→});
   814→
   815→/**
   816→ * POST /api/sms/request-contact
   817→ * Send contact request SMS to a worker lead
   818→ * Body: { leadId }
   819→ */
   820→router.post('/request-contact', smsLimiter, async (req, res) => {
   821→  try {
   822→    if (!isConfigured()) {
   823→      return res.status(503).json({ error: 'Twilio not configured' });
   824→    }
   825→
   826→    if (!supabase) {
   827→      return res.status(503).json({ error: 'Database not configured' });
   828→    }
   829→
   830→    const { leadId } = req.body;
   831→
   832→    if (!leadId) {
   833→      return res.status(400).json({ error: 'leadId is required' });
   834→    }
   835→
   836→    // Get lead
   837→    const { data: lead, error: leadError } = await supabase
   838→      .from('leads')
   839→      .select('*')
   840→      .eq('id', leadId)
   841→      .single();
   842→
   843→    if (leadError || !lead) {
   844→      return res.status(404).json({ error: 'Lead not found' });
   845→    }
   846→
   847→    if (!lead.phone) {
   848→      return res.status(400).json({ error: 'Lead has no phone number' });
   849→    }
   850→
   851→    // Get the request_contact template
   852→    const template = await getTemplate('request_contact');
   853→    if (!template) {
   854→      return res.status(404).json({ error: 'Request contact template not found' });
   855→    }
   856→
   857→    // Render template with lead variables
   858→    const leadVars = buildLeadVariables(lead);
   859→    const messageContent = renderTemplate(template.content, leadVars);
   860→
   861→    // Send SMS
   862→    const result = await sendSMS(lead.phone, messageContent);
   863→
   864→    if (!result.success) {
   865→      return res.status(500).json({ error: result.error });
   866→    }
   867→
   868→    // Log communication
   869→    await supabase
   870→      .from('communications')
   871→      .insert({
   872→        lead_id: lead.id,
   873→        type: 'sms_outbound',
   874→        direction: 'outbound',
   875→        phone: lead.phone,
   876→        content: messageContent,
   877→        external_id: result.messageSid,
   878→        metadata: {
   879→          template_slug: 'request_contact',
   880→          contact_request: true
   881→        }
   882→      });
   883→
   884→    // Mark lead as having pending contact request
   885→    await supabase
   886→      .from('leads')
   887→      .update({
   888→        last_contact_at: new Date().toISOString(),
   889→        last_contact_type: 'sms_outbound',
   890→        contact_request_pending: true,
   891→        contact_request_sent_at: new Date().toISOString()
   892→      })
   893→      .eq('id', lead.id);
   894→
   895→    console.log(`Contact request SMS sent to ${lead.phone}: ${result.messageSid}`);
   896→
   897→    res.json({
   898→      success: true,
   899→      messageSid: result.messageSid,
   900→      to: lead.phone,
   901→      content: messageContent
   902→    });
   903→
   904→  } catch (error) {
   905→    console.error('Contact request SMS error:', error);
   906→    res.status(500).json({ error: 'Failed to send contact request SMS' });
   907→  }
   908→});
   909→
   910→/**
   911→ * GET /api/sms/conversations
   912→ * Get all SMS conversations with latest message
   913→ */
   914→router.get('/conversations', async (req, res) => {
   915→  try {
   916→    if (!supabase) {
   917→      return res.status(503).json({ error: 'Database not configured' });
   918→    }
   919→
   920→    const { filter = 'all', limit = 50 } = req.query;
   921→
   922→    // Get leads with SMS communications
   923→    let query = supabase
   924→      .from('leads')
   925→      .select(`
   926→        id,
   927→        first_name,
   928→        last_name,
   929→        company,
   930→        phone,
   931→        score,
   932→        platform_stage,
   933→        lead_type
   934→      `)
   935→      .is('deleted_at', null)
   936→      .not('phone', 'is', null)
   937→      .order('last_contact_at', { ascending: false })
   938→      .limit(parseInt(limit));
   939→
   940→    const { data: leads, error: leadsError } = await query;
   941→
   942→    if (leadsError) {
   943→      console.error('Failed to fetch leads:', leadsError);
   944→      return res.status(500).json({ error: 'Failed to fetch conversations' });
   945→    }
   946→
   947→    // For each lead, get their latest message and unread count
   948→    const conversations = [];
   949→
   950→    for (const lead of leads) {
   951→      // Get latest SMS message
   952→      const { data: latestMsg } = await supabase
   953→        .from('communications')
   954→        .select('id, content, direction, created_at, read_at')
   955→        .eq('lead_id', lead.id)
   956→        .in('type', ['sms_inbound', 'sms_outbound'])
   957→        .order('created_at', { ascending: false })
   958→        .limit(1)
   959→        .single();
   960→
   961→      if (!latestMsg) continue; // Skip leads with no SMS history
   962→
   963→      // Get unread count
   964→      const { count: unreadCount } = await supabase
   965→        .from('communications')
   966→        .select('id', { count: 'exact', head: true })
   967→        .eq('lead_id', lead.id)
   968→        .eq('type', 'sms_inbound')
   969→        .is('read_at', null);
   970→
   971→      // Apply filters
   972→      if (filter === 'unread' && unreadCount === 0) continue;
   973→      if (filter === 'needs_reply' && latestMsg.direction !== 'inbound') continue;
   974→
   975→      // Check if lead should be suggested for sequence enrollment
   976→      let sequenceSuggestion = null;
   977→      const earlyStages = ['not_signed_up', 'signed_up', 'profile_complete'];
   978→      const isEarlyStage = earlyStages.includes(lead.platform_stage);
   979→
   980→      if (isEarlyStage) {
   981→        // Check if already in active sequence or opted out
   982→        const { data: activeEnrollment } = await supabase
   983→          .from('sequence_enrollments')
   984→          .select('id')
   985→          .eq('lead_id', lead.id)
   986→          .eq('status', 'active')
   987→          .limit(1)
   988→          .single();
   989→
   990→        const { data: leadOptOut } = await supabase
   991→          .from('leads')
   992→          .select('sequence_opted_out')
   993→          .eq('id', lead.id)
   994→          .single();
   995→
   996→        const isOptedOut = leadOptOut?.sequence_opted_out === true;
   997→        const hasActiveSequence = !!activeEnrollment;
   998→
   999→        if (!hasActiveSequence && !isOptedOut) {
  1000→          // Check if we sent last message and no reply for 2+ days
  1001→          const daysSinceMessage = Math.floor(
  1002→            (Date.now() - new Date(latestMsg.created_at).getTime()) / (1000 * 60 * 60 * 24)
  1003→          );
  1004→
  1005→          if (latestMsg.direction === 'outbound' && daysSinceMessage >= 2) {
  1006→            sequenceSuggestion = {
  1007→              reason: `No reply in ${daysSinceMessage} days`,
  1008→              stage: lead.platform_stage,
  1009→              priority: daysSinceMessage >= 5 ? 'high' : 'medium'
  1010→            };
  1011→          } else if (latestMsg.direction === 'inbound' && daysSinceMessage >= 3) {
  1012→            sequenceSuggestion = {
  1013→              reason: `Hasn't heard from us in ${daysSinceMessage} days`,
  1014→              stage: lead.platform_stage,
  1015→              priority: 'medium'
  1016→            };
  1017→          } else if (!lead.platform_stage || lead.platform_stage === 'not_signed_up') {
  1018→            sequenceSuggestion = {
  1019→              reason: 'New lead - not signed up yet',
  1020→              stage: lead.platform_stage || 'not_signed_up',
  1021→              priority: 'low'
  1022→            };
  1023→          }
  1024→        }
  1025→      }
  1026→
  1027→      conversations.push({
  1028→        lead,
  1029→        lastMessage: latestMsg,
  1030→        unreadCount: unreadCount || 0,
  1031→        sequenceSuggestion
  1032→      });
  1033→    }
  1034→
  1035→    // Sort by last message time
  1036→    conversations.sort((a, b) =>
  1037→      new Date(b.lastMessage.created_at) - new Date(a.lastMessage.created_at)
  1038→    );
  1039→
  1040→    // Get total unread across all conversations
  1041→    const { count: totalUnread } = await supabase
  1042→      .from('communications')
  1043→      .select('id', { count: 'exact', head: true })
  1044→      .eq('type', 'sms_inbound')
  1045→      .is('read_at', null);
  1046→
  1047→    res.json({
  1048→      conversations,
  1049→      totalUnread: totalUnread || 0,
  1050→      count: conversations.length
  1051→    });
  1052→
  1053→  } catch (error) {
  1054→    console.error('Conversations error:', error);
  1055→    res.status(500).json({ error: 'Failed to fetch conversations' });
  1056→  }
  1057→});
  1058→
  1059→/**
  1060→ * GET /api/sms/conversation/:leadId
  1061→ * Get all messages for a specific lead
  1062→ */
  1063→router.get('/conversation/:leadId', async (req, res) => {
  1064→  try {
  1065→    if (!supabase) {
  1066→      return res.status(503).json({ error: 'Database not configured' });
  1067→    }
  1068→
  1069→    const { leadId } = req.params;
  1070→    const { limit = 100 } = req.query;
  1071→
  1072→    // Get lead info
  1073→    const { data: lead, error: leadError } = await supabase
  1074→      .from('leads')
  1075→      .select('id, first_name, last_name, company, phone, score, platform_stage, lead_type')
  1076→      .eq('id', leadId)
  1077→      .single();
  1078→
  1079→    if (leadError || !lead) {
  1080→      return res.status(404).json({ error: 'Lead not found' });
  1081→    }
  1082→
  1083→    // Get all SMS messages
  1084→    const { data: messages, error: messagesError } = await supabase
  1085→      .from('communications')
  1086→      .select('id, content, direction, created_at, read_at, delivery_status, metadata')
  1087→      .eq('lead_id', leadId)
  1088→      .in('type', ['sms_inbound', 'sms_outbound'])
  1089→      .order('created_at', { ascending: true })
  1090→      .limit(parseInt(limit));
  1091→
  1092→    if (messagesError) {
  1093→      console.error('Failed to fetch messages:', messagesError);
  1094→      return res.status(500).json({ error: 'Failed to fetch messages' });
  1095→    }
  1096→
  1097→    // Mark all inbound messages as read
  1098→    const unreadIds = messages
  1099→      .filter(m => m.direction === 'inbound' && !m.read_at)
  1100→      .map(m => m.id);
  1101→
  1102→    if (unreadIds.length > 0) {
  1103→      await supabase
  1104→        .from('communications')
  1105→        .update({ read_at: new Date().toISOString() })
  1106→        .in('id', unreadIds);
  1107→    }
  1108→
  1109→    res.json({
  1110→      lead,
  1111→      messages,
  1112→      markedAsRead: unreadIds.length
  1113→    });
  1114→
  1115→  } catch (error) {
  1116→    console.error('Conversation error:', error);
  1117→    res.status(500).json({ error: 'Failed to fetch conversation' });
  1118→  }
  1119→});
  1120→
  1121→/**
  1122→ * PATCH /api/sms/conversation/:leadId/read
  1123→ * Mark all messages in a conversation as read
  1124→ */
  1125→router.patch('/conversation/:leadId/read', async (req, res) => {
  1126→  try {
  1127→    if (!supabase) {
  1128→      return res.status(503).json({ error: 'Database not configured' });
  1129→    }
  1130→
  1131→    const { leadId } = req.params;
  1132→
  1133→    const { data, error } = await supabase
  1134→      .from('communications')
  1135→      .update({ read_at: new Date().toISOString() })
  1136→      .eq('lead_id', leadId)
  1137→      .eq('type', 'sms_inbound')
  1138→      .is('read_at', null)
  1139→      .select();
  1140→
  1141→    if (error) {
  1142→      console.error('Failed to mark as read:', error);
  1143→      return res.status(500).json({ error: 'Failed to mark as read' });
  1144→    }
  1145→
  1146→    res.json({
  1147→      success: true,
  1148→      markedAsRead: data?.length || 0
  1149→    });
  1150→
  1151→  } catch (error) {
  1152→    console.error('Mark read error:', error);
  1153→    res.status(500).json({ error: 'Failed to mark as read' });
  1154→  }
  1155→});
  1156→
  1157→/**
  1158→ * PATCH /api/sms/conversation/:leadId/handle
  1159→ * Mark a conversation as handled (Done)
  1160→ * Sets metadata.handled = true and handled_at timestamp on the last inbound message
  1161→ */
  1162→router.patch('/conversation/:leadId/handle', async (req, res) => {
  1163→  try {
  1164→    if (!supabase) {
  1165→      return res.status(503).json({ error: 'Database not configured' });
  1166→    }
  1167→
  1168→    const { leadId } = req.params;
  1169→    const { handled = true } = req.body;
  1170→
  1171→    // Get the most recent inbound message for this lead
  1172→    const { data: lastMessage, error: fetchError } = await supabase
  1173→      .from('communications')
  1174→      .select('id, metadata')
  1175→      .eq('lead_id', leadId)
  1176→      .eq('type', 'sms_inbound')
  1177→      .order('created_at', { ascending: false })
  1178→      .limit(1)
  1179→      .single();
  1180→
  1181→    if (fetchError || !lastMessage) {
  1182→      return res.status(404).json({ error: 'No conversation found' });
  1183→    }
  1184→
  1185→    // Update metadata with handled flag
  1186→    const updatedMetadata = {
  1187→      ...(lastMessage.metadata || {}),
  1188→      handled: handled,
  1189→      handled_at: handled ? new Date().toISOString() : null
  1190→    };
  1191→
  1192→    const { data, error } = await supabase
  1193→      .from('communications')
  1194→      .update({ metadata: updatedMetadata })
  1195→      .eq('id', lastMessage.id)
  1196→      .select();
  1197→
  1198→    if (error) {
  1199→      console.error('Failed to mark as handled:', error);
  1200→      return res.status(500).json({ error: 'Failed to mark as handled' });
  1201→    }
  1202→
  1203→    res.json({
  1204→      success: true,
  1205→      handled: handled,
  1206→      messageId: lastMessage.id
  1207→    });
  1208→
  1209→  } catch (error) {
  1210→    console.error('Handle conversation error:', error);
  1211→    res.status(500).json({ error: 'Failed to mark as handled' });
  1212→  }
  1213→});
  1214→
  1215→/**
  1216→ * GET /api/sms/conversations/prioritized
  1217→ * Get conversations sorted by AI triage priority
  1218→ * Returns conversations with triage category and AI suggestions
  1219→ */
  1220→router.get('/conversations/prioritized', async (req, res) => {
  1221→  try {
  1222→    if (!supabase) {
  1223→      return res.status(503).json({ error: 'Database not configured' });
  1224→    }
  1225→
  1226→    const { limit = 50 } = req.query;
  1227→
  1228→    // Get all SMS communications with metadata
  1229→    const { data: communications, error: commsError } = await supabase
  1230→      .from('communications')
  1231→      .select(`
  1232→        id,
  1233→        lead_id,
  1234→        content,
  1235→        direction,
  1236→        created_at,
  1237→        read_at,
  1238→        delivery_status,
  1239→        metadata
  1240→      `)
  1241→      .in('type', ['sms_inbound', 'sms_outbound'])
  1242→      .order('created_at', { ascending: false })
  1243→      .limit(500);
  1244→
  1245→    if (commsError) {
  1246→      console.error('Failed to fetch communications:', commsError);
  1247→      return res.status(500).json({ error: 'Failed to fetch conversations' });
  1248→    }
  1249→
  1250→    // Group by lead_id and get latest message per lead
  1251→    const leadMessages = {};
  1252→    for (const comm of communications || []) {
  1253→      if (!comm.lead_id) continue;
  1254→      if (!leadMessages[comm.lead_id]) {
  1255→        leadMessages[comm.lead_id] = {
  1256→          messages: [],
  1257→          latestInbound: null,
  1258→          unreadCount: 0
  1259→        };
  1260→      }
  1261→      leadMessages[comm.lead_id].messages.push(comm);
  1262→
  1263→      if (comm.direction === 'inbound') {
  1264→        if (!leadMessages[comm.lead_id].latestInbound) {
  1265→          leadMessages[comm.lead_id].latestInbound = comm;
  1266→        }
  1267→        if (!comm.read_at) {
  1268→          leadMessages[comm.lead_id].unreadCount++;
  1269→        }
  1270→      }
  1271→    }
  1272→
  1273→    // Get lead details for all leads with messages
  1274→    const leadIds = Object.keys(leadMessages);
  1275→    if (leadIds.length === 0) {
  1276→      return res.json({ conversations: [], totalUnread: 0 });
  1277→    }
  1278→
  1279→    const { data: leads, error: leadsError } = await supabase
  1280→      .from('leads')
  1281→      .select('id, first_name, last_name, company, phone, score, platform_stage, lead_type, metadata')
  1282→      .in('id', leadIds)
  1283→      .is('deleted_at', null);
  1284→
  1285→    if (leadsError) {
  1286→      console.error('Failed to fetch leads:', leadsError);
  1287→      return res.status(500).json({ error: 'Failed to fetch leads' });
  1288→    }
  1289→
  1290→    // Build conversations with triage
  1291→    const conversations = [];
  1292→    let totalUnread = 0;
  1293→
  1294→    for (const lead of leads || []) {
  1295→      const data = leadMessages[lead.id];
  1296→      if (!data || data.messages.length === 0) continue;
  1297→
  1298→      const lastMessage = data.messages[0]; // Most recent
  1299→      const latestInbound = data.latestInbound;
  1300→
  1301→      // Determine triage category based on metadata
  1302→      let triage = {
  1303→        category: 'acknowledgment',
  1304→        priority: 0,
  1305→        emoji: '👋',
  1306→        label: 'Acknowledgment'
  1307→      };
  1308→
  1309→      if (latestInbound) {
  1310→        const meta = latestInbound.metadata || {};
  1311→        const intent = meta.intent;
  1312→        const buyingSignal = meta.buyingSignal || {};
  1313→        const content = (latestInbound.content || '').toLowerCase();
  1314→
  1315→        // Check for buying signals first (highest priority)
  1316→        if (buyingSignal.level === 'high' || buyingSignal.score >= 15) {
  1317→          triage = {
  1318→            category: 'buying_signal',
  1319→            priority: 100 + (buyingSignal.score || 15),
  1320→            emoji: '🔥',
  1321→            label: 'Buying Signal'
  1322→          };
  1323→        }
  1324→        // Check for callback request
  1325→        else if (content.includes('call me') || content.includes('give me a call') ||
  1326→                 content.includes('ring me') || content.includes('phone me')) {
  1327→          triage = {
  1328→            category: 'callback_request',
  1329→            priority: 90,
  1330→            emoji: '⏰',
  1331→            label: 'Callback Request'
  1332→          };
  1333→        }
  1334→        // Check for questions
  1335→        else if (intent === 'question' || content.includes('?') ||
  1336→                 content.includes('how') || content.includes('what') ||
  1337→                 content.includes('when') || content.includes('where')) {
  1338→          triage = {
  1339→            category: 'question',
  1340→            priority: 70,
  1341→            emoji: '❓',
  1342→            label: 'Question'
  1343→          };
  1344→        }
  1345→        // Check for negative/not interested
  1346→        else if (intent === 'negative') {
  1347→          triage = {
  1348→            category: 'not_interested',
  1349→            priority: 10,
  1350→            emoji: '❌',
  1351→            label: 'Not Interested'
  1352→          };
  1353→        }
  1354→        // Positive but not buying signal
  1355→        else if (intent === 'positive') {
  1356→          triage = {
  1357→            category: 'positive',
  1358→            priority: 50,
  1359→            emoji: '👍',
  1360→            label: 'Positive'
  1361→          };
  1362→        }
  1363→      }
  1364→
  1365→      // Boost priority for unread messages
  1366→      if (data.unreadCount > 0) {
  1367→        triage.priority += 20;
  1368→      }
  1369→
  1370→      // Boost priority for needs reply (their last message was inbound)
  1371→      if (lastMessage.direction === 'inbound') {
  1372→        triage.priority += 10;
  1373→      }
  1374→
  1375→      totalUnread += data.unreadCount;
  1376→
  1377→      conversations.push({
  1378→        lead,
  1379→        lastMessage,
  1380→        latestInbound,
  1381→        unreadCount: data.unreadCount,
  1382→        triage,
  1383→        needsReply: lastMessage.direction === 'inbound'
  1384→      });
  1385→    }
  1386→
  1387→    // Sort by triage priority (highest first), then by recency
  1388→    conversations.sort((a, b) => {
  1389→      if (b.triage.priority !== a.triage.priority) {
  1390→        return b.triage.priority - a.triage.priority;
  1391→      }
  1392→      return new Date(b.lastMessage.created_at) - new Date(a.lastMessage.created_at);
  1393→    });
  1394→
  1395→    // Limit results
  1396→    const limited = conversations.slice(0, parseInt(limit));
  1397→
  1398→    res.json({
  1399→      conversations: limited,
  1400→      totalUnread,
  1401→      count: limited.length,
  1402→      triageSummary: {
  1403→        buying_signal: conversations.filter(c => c.triage.category === 'buying_signal').length,
  1404→        callback_request: conversations.filter(c => c.triage.category === 'callback_request').length,
  1405→        question: conversations.filter(c => c.triage.category === 'question').length,
  1406→        positive: conversations.filter(c => c.triage.category === 'positive').length,
  1407→        not_interested: conversations.filter(c => c.triage.category === 'not_interested').length,
  1408→        acknowledgment: conversations.filter(c => c.triage.category === 'acknowledgment').length
  1409→      }
  1410→    });
  1411→
  1412→  } catch (error) {
  1413→    console.error('Prioritized conversations error:', error);
  1414→    res.status(500).json({ error: 'Failed to fetch prioritized conversations' });
  1415→  }
  1416→});
  1417→
  1418→/**
  1419→ * POST /api/sms/ai-suggest
  1420→ * Get AI-suggested replies for a conversation (returns 3 options)
  1421→ */
  1422→router.post('/ai-suggest', async (req, res) => {
  1423→  try {
  1424→    if (!supabase) {
  1425→      return res.status(503).json({ error: 'Database not configured' });
  1426→    }
  1427→
  1428→    const { leadId, lastMessage } = req.body;
  1429→
  1430→    if (!leadId) {
  1431→      return res.status(400).json({ error: 'leadId is required' });
  1432→    }
  1433→
  1434→    // Get lead
  1435→    const { data: lead, error: leadError } = await supabase
  1436→      .from('leads')
  1437→      .select('*')
  1438→      .eq('id', leadId)
  1439→      .single();
  1440→
  1441→    if (leadError || !lead) {
  1442→      return res.status(404).json({ error: 'Lead not found' });
  1443→    }
  1444→
  1445→    // Get recent messages for context
  1446→    const { data: messages } = await supabase
  1447→      .from('communications')
  1448→      .select('content, direction, created_at, metadata')
  1449→      .eq('lead_id', leadId)
  1450→      .in('type', ['sms_inbound', 'sms_outbound'])
  1451→      .order('created_at', { ascending: false })
  1452→      .limit(10);
  1453→
  1454→    // Build context for AI
  1455→    const leadName = lead.first_name || 'there';
  1456→    const company = lead.company || '';
  1457→    const stage = lead.platform_stage || 'unknown';
  1458→    const lastInbound = lastMessage || messages?.find(m => m.direction === 'inbound')?.content || '';
  1459→    const intent = messages?.[0]?.metadata?.intent || 'unclear';
  1460→    const buyingSignal = messages?.[0]?.metadata?.buyingSignal?.level || 'none';
  1461→
  1462→    // Generate rule-based fallback suggestions (3 options)
  1463→    let suggestions = [];
  1464→
  1465→    if (buyingSignal === 'high' || intent === 'positive') {
  1466→      suggestions = [
  1467→        { text: `Great to hear from you${leadName !== 'there' ? ', ' + leadName : ''}! I'd love to help you get started. When's a good time for a quick call today?`, confidence: 'high' },
  1468→        { text: `${leadName !== 'there' ? leadName + ', ' : ''}Thanks for getting in touch! I can give you a call to discuss - what time suits you?`, confidence: 'high' },
  1469→        { text: `Perfect timing! Let me know the best way to reach you and I'll be in touch shortly.`, confidence: 'medium' }
  1470→      ];
  1471→    } else if (intent === 'question') {
  1472→      suggestions = [
  1473→        { text: `Hi ${leadName}! Great question. Let me help you with that. Would you prefer I call you to explain, or send through more details here?`, confidence: 'high' },
  1474→        { text: `Good question! Happy to walk you through it - easiest over a quick call if you have 5 mins?`, confidence: 'high' },
  1475→        { text: `${leadName !== 'there' ? leadName + ', ' : ''}I can definitely help with that. Shall I call you to explain?`, confidence: 'medium' }
  1476→      ];
  1477→    } else if (lastInbound.toLowerCase().includes('call')) {
  1478→      suggestions = [
  1479→        { text: `Absolutely ${leadName}! I'll give you a call shortly. If I miss you, when's the best time to try again?`, confidence: 'high' },
  1480→        { text: `Sure thing! I'll call you in the next few minutes. If you miss it, just text me a good time.`, confidence: 'high' },
  1481→        { text: `On it! Calling you now - if I miss you, let me know when works better.`, confidence: 'medium' }
  1482→      ];
  1483→    } else if (intent === 'negative') {
  1484→      suggestions = [
  1485→        { text: `No worries at all, ${leadName}. If anything changes, feel free to reach out. All the best!`, confidence: 'medium' },
  1486→        { text: `Totally understand! We're here if you need us down the track. Take care!`, confidence: 'medium' },
  1487→        { text: `No problem at all. Thanks for letting me know - feel free to reach out anytime!`, confidence: 'medium' }
  1488→      ];
  1489→    } else {
  1490→      suggestions = [
  1491→        { text: `Thanks for getting back to me${leadName !== 'there' ? ', ' + leadName : ''}! Is there anything specific I can help you with?`, confidence: 'medium' },
  1492→        { text: `Great to hear from you! Let me know how I can help.`, confidence: 'medium' },
  1493→        { text: `${leadName !== 'there' ? leadName + ', ' : ''}Thanks for the reply! What can I help you with today?`, confidence: 'medium' }
  1494→      ];
  1495→    }
  1496→
  1497→    // If we have OpenAI configured, try to get better AI suggestions
  1498→    try {
  1499→      const openaiKey = process.env.OPENAI_API_KEY;
  1500→      if (openaiKey && lastInbound) {
  1501→        const OpenAI = require('openai');
  1502→        const openai = new OpenAI({ apiKey: openaiKey });
  1503→
  1504→        const conversationHistory = (messages || [])
  1505→          .slice(0, 5)
  1506→          .reverse()
  1507→          .map(m => `${m.direction === 'inbound' ? 'Them' : 'You'}: ${m.content}`)
  1508→          .join('\n');
  1509→
  1510→        const completion = await openai.chat.completions.create({
  1511→          model: 'gpt-4o-mini',
  1512→          messages: [
  1513→            {
  1514→              role: 'system',
  1515→              content: `You are a helpful sales assistant for RateRight, a platform connecting contractors with workers in Australia.
  1516→
  1517→Generate exactly 3 SHORT, friendly SMS reply options (max 160 chars each, max 200 chars).
  1518→Be conversational, not corporate. Use Australian English.
  1519→Lead info: ${leadName} from ${company || 'unknown company'}, stage: ${stage}
  1520→Their intent seems: ${intent}, buying signal: ${buyingSignal}
  1521→
  1522→Return ONLY a JSON array with 3 objects, each with "text" and "confidence" fields.
  1523→Confidence should be "high" or "medium".
  1524→Example: [{"text": "Hey! Thanks for getting back to me...", "confidence": "high"}]`
  1525→            },
  1526→            {
  1527→              role: 'user',
  1528→              content: `Recent conversation:\n${conversationHistory}\n\nGenerate 3 reply options to their last message.`
  1529→            }
  1530→          ],
  1531→          max_tokens: 400,
  1532→          temperature: 0.7
  1533→        });
  1534→
  1535→        const aiResponse = completion.choices[0]?.message?.content?.trim();
  1536→        if (aiResponse) {
  1537→          try {
  1538→            // Extract JSON from response (handle markdown code blocks)
  1539→            let jsonStr = aiResponse;
  1540→            if (aiResponse.includes('```')) {
  1541→              jsonStr = aiResponse.replace(/```json?\n?/g, '').replace(/```/g, '').trim();
  1542→            }
  1543→            const parsed = JSON.parse(jsonStr);
  1544→            if (Array.isArray(parsed) && parsed.length >= 1) {
  1545→              suggestions = parsed.slice(0, 3).map(s => ({
  1546→                text: s.text || s.message || '',
  1547→                confidence: s.confidence || 'high'
  1548→              })).filter(s => s.text.length > 10 && s.text.length < 500);
  1549→
  1550→              // Ensure we have 3 suggestions
  1551→              while (suggestions.length < 3) {
  1552→                suggestions.push({ text: `Thanks for getting back! Let me know how I can help.`, confidence: 'medium' });
  1553→              }
  1554→            }
  1555→          } catch (parseError) {
  1556→            console.warn('Failed to parse AI suggestions, using fallbacks:', parseError.message);
  1557→          }
  1558→        }
  1559→      }
  1560→    } catch (aiError) {
  1561→      console.error('AI suggestion failed, using fallback:', aiError.message);
  1562→      // Keep the rule-based suggestions
  1563→    }
  1564→
  1565→    // Return backwards-compatible response (single suggestion) + new suggestions array
  1566→    res.json({
  1567→      suggestion: suggestions[0]?.text || '',
  1568→      confidence: suggestions[0]?.confidence || 'medium',
  1569→      suggestions: suggestions,
  1570→      context: {
  1571→        leadName,
  1572→        company,
  1573→        stage,
  1574→        intent,
  1575→        buyingSignal
  1576→      }
  1577→    });
  1578→
  1579→  } catch (error) {
  1580→    console.error('AI suggest error:', error);
  1581→    res.status(500).json({ error: 'Failed to generate suggestion' });
  1582→  }
  1583→});
  1584→
  1585→/**
  1586→ * GET /api/sms/stats
  1587→ * Get SMS analytics and performance metrics
  1588→ */
  1589→router.get('/stats', async (req, res) => {
  1590→  try {
  1591→    if (!supabase) {
  1592→      return res.status(503).json({ error: 'Database not configured' });
  1593→    }
  1594→
  1595→    const { days = 30 } = req.query;
  1596→    const startDate = new Date();
  1597→    startDate.setDate(startDate.getDate() - parseInt(days));
  1598→
  1599→    // Get all SMS communications in the period
  1600→    const { data: communications, error: commsError } = await supabase
  1601→      .from('communications')
  1602→      .select('id, direction, created_at, delivery_status, status, read_at, metadata')
  1603→      .in('type', ['sms_inbound', 'sms_outbound'])
  1604→      .gte('created_at', startDate.toISOString())
  1605→      .order('created_at', { ascending: true });
  1606→
  1607→    if (commsError) {
  1608→      console.error('Stats fetch error:', commsError);
  1609→      return res.status(500).json({ error: 'Failed to fetch stats' });
  1610→    }
  1611→
  1612→    const msgs = communications || [];
  1613→
  1614→    // Calculate overall stats
  1615→    const outbound = msgs.filter(m => m.direction === 'outbound');
  1616→    const inbound = msgs.filter(m => m.direction === 'inbound');
  1617→    const delivered = outbound.filter(m => m.delivery_status === 'delivered' || m.delivery_status === 'read');
  1618→    const failed = outbound.filter(m => m.delivery_status === 'failed' || m.status === 'failed');
  1619→    const scheduled = outbound.filter(m => m.status === 'scheduled');
  1620→
  1621→    // Calculate response rate (inbound messages that came after an outbound)
  1622→    // Group by day for trend data
  1623→    const dailyStats = {};
  1624→    for (const msg of msgs) {
  1625→      const day = msg.created_at.split('T')[0];
  1626→      if (!dailyStats[day]) {
  1627→        dailyStats[day] = { sent: 0, received: 0, delivered: 0, failed: 0 };
  1628→      }
  1629→      if (msg.direction === 'outbound') {
  1630→        dailyStats[day].sent++;
  1631→        if (msg.delivery_status === 'delivered' || msg.delivery_status === 'read') {
  1632→          dailyStats[day].delivered++;
  1633→        }
  1634→        if (msg.delivery_status === 'failed' || msg.status === 'failed') {
  1635→          dailyStats[day].failed++;
  1636→        }
  1637→      } else {
  1638→        dailyStats[day].received++;
  1639→      }
  1640→    }
  1641→
  1642→    // Calculate delivery rate
  1643→    const deliveryRate = outbound.length > 0
  1644→      ? Math.round((delivered.length / outbound.length) * 100)
  1645→      : 0;
  1646→
  1647→    // Calculate response rate (replies received / messages sent)
  1648→    const responseRate = outbound.length > 0
  1649→      ? Math.round((inbound.length / outbound.length) * 100)
  1650→      : 0;
  1651→
  1652→    // Get template usage stats
  1653→    const templateUsage = {};
  1654→    for (const msg of outbound) {
  1655→      const slug = msg.metadata?.template_slug;
  1656→      if (slug) {
  1657→        if (!templateUsage[slug]) {
  1658→          templateUsage[slug] = { sent: 0, delivered: 0 };
  1659→        }
  1660→        templateUsage[slug].sent++;
  1661→        if (msg.delivery_status === 'delivered' || msg.delivery_status === 'read') {
  1662→          templateUsage[slug].delivered++;
  1663→        }
  1664→      }
  1665→    }
  1666→
  1667→    // Get hourly distribution for best times to send
  1668→    const hourlyDistribution = Array(24).fill(0);
  1669→    const hourlyResponses = Array(24).fill(0);
  1670→    for (const msg of inbound) {
  1671→      const hour = new Date(msg.created_at).getHours();
  1672→      hourlyResponses[hour]++;
  1673→    }
  1674→    for (const msg of outbound) {
  1675→      const hour = new Date(msg.created_at).getHours();
  1676→      hourlyDistribution[hour]++;
  1677→    }
  1678→
  1679→    // Find best hours (highest response rates)
  1680→    const hourlyResponseRates = hourlyDistribution.map((sent, hour) => ({
  1681→      hour,
  1682→      sent,
  1683→      responses: hourlyResponses[hour],
  1684→      rate: sent > 0 ? Math.round((hourlyResponses[hour] / sent) * 100) : 0
  1685→    }));
  1686→    const bestHours = [...hourlyResponseRates]
  1687→      .filter(h => h.sent >= 5) // Only consider hours with meaningful volume
  1688→      .sort((a, b) => b.rate - a.rate)
  1689→      .slice(0, 3)
  1690→      .map(h => ({ hour: h.hour, label: `${h.hour}:00`, responseRate: h.rate }));
  1691→
  1692→    // Convert daily stats to array sorted by date
  1693→    const trend = Object.entries(dailyStats)
  1694→      .map(([date, stats]) => ({ date, ...stats }))
  1695→      .sort((a, b) => a.date.localeCompare(b.date));
  1696→
  1697→    res.json({
  1698→      period: {
  1699→        days: parseInt(days),
  1700→        start: startDate.toISOString(),
  1701→        end: new Date().toISOString()
  1702→      },
  1703→      overview: {
  1704→        totalSent: outbound.length,
  1705→        totalReceived: inbound.length,
  1706→        delivered: delivered.length,
  1707→        failed: failed.length,
  1708→        scheduled: scheduled.length,
  1709→        deliveryRate,
  1710→        responseRate
  1711→      },
  1712→      trend,
  1713→      templatePerformance: Object.entries(templateUsage)
  1714→        .map(([slug, stats]) => ({
  1715→          slug,
  1716→          sent: stats.sent,
  1717→          delivered: stats.delivered,
  1718→          deliveryRate: stats.sent > 0 ? Math.round((stats.delivered / stats.sent) * 100) : 0
  1719→        }))
  1720→        .sort((a, b) => b.sent - a.sent),
  1721→      bestTimesToSend: bestHours,
  1722→      hourlyDistribution: hourlyResponseRates
  1723→    });
  1724→
  1725→  } catch (error) {
  1726→    console.error('SMS stats error:', error);
  1727→    res.status(500).json({ error: 'Failed to fetch SMS stats' });
  1728→  }
  1729→});
  1730→
  1731→/**
  1732→ * POST /api/sms/preview
  1733→ * Preview a template with variables (doesn't send)
  1734→ */
  1735→router.post('/preview', async (req, res) => {
  1736→  try {
  1737→    const { templateSlug, leadId, variables = {} } = req.body;
  1738→
  1739→    if (!templateSlug) {
  1740→      return res.status(400).json({ error: 'templateSlug is required' });
  1741→    }
  1742→
  1743→    const template = await getTemplate(templateSlug);
  1744→    if (!template) {
  1745→      return res.status(404).json({ error: `Template '${templateSlug}' not found` });
  1746→    }
  1747→
  1748→    let lead = null;
  1749→    if (leadId && supabase) {
  1750→      const { data } = await supabase
  1751→        .from('leads')
  1752→        .select('*')
  1753→        .eq('id', leadId)
  1754→        .single();
  1755→      lead = data;
  1756→    }
  1757→
  1758→    const leadVars = buildLeadVariables(lead);
  1759→    const allVars = { ...leadVars, ...variables };
  1760→    const rendered = renderTemplate(template.content, allVars);
  1761→
  1762→    res.json({
  1763→      template: template.name,
  1764→      original: template.content,
  1765→      rendered,
  1766→      variables: allVars,
  1767→      characterCount: rendered.length,
  1768→      segments: Math.ceil(rendered.length / 160)
  1769→    });
  1770→
  1771→  } catch (error) {
  1772→    console.error('Preview error:', error);
  1773→    res.status(500).json({ error: 'Failed to preview template' });
  1774→  }
  1775→});
  1776→
  1777→/**
  1778→ * POST /api/sms/flag/:id
  1779→ * Flag/unflag a message for follow-up
  1780→ * Body: { flagged: boolean, reason?: string }
  1781→ */
  1782→router.post('/flag/:id', async (req, res) => {
  1783→  try {
  1784→    if (!supabase) {
  1785→      return res.status(503).json({ error: 'Database not configured' });
  1786→    }
  1787→
  1788→    const { id } = req.params;
  1789→    const { flagged, reason } = req.body;
  1790→
  1791→    // Verify message exists
  1792→    const { data: existing, error: fetchError } = await supabase
  1793→      .from('communications')
  1794→      .select('id, flagged')
  1795→      .eq('id', id)
  1796→      .single();
  1797→
  1798→    if (fetchError || !existing) {
  1799→      return res.status(404).json({ error: 'Message not found' });
  1800→    }
  1801→
  1802→    // Update flag status
  1803→    const updates = {
  1804→      flagged: flagged !== undefined ? flagged : !existing.flagged,
  1805→      flagged_at: flagged ? new Date().toISOString() : null,
  1806→      flagged_reason: flagged ? (reason || null) : null
  1807→    };
  1808→
  1809→    const { data, error } = await supabase
  1810→      .from('communications')
  1811→      .update(updates)
  1812→      .eq('id', id)
  1813→      .select()
  1814→      .single();
  1815→
  1816→    if (error) {
  1817→      console.error('Flag message error:', error);
  1818→      return res.status(500).json({ error: 'Failed to flag message' });
  1819→    }
  1820→
  1821→    res.json({
  1822→      success: true,
  1823→      message: data,
  1824→      flagged: data.flagged
  1825→    });
  1826→
  1827→  } catch (error) {
  1828→    console.error('Flag message error:', error);
  1829→    res.status(500).json({ error: 'Failed to flag message' });
  1830→  }
  1831→});
  1832→
  1833→/**
  1834→ * GET /api/sms/response-times
  1835→ * Get response time statistics for gamification
  1836→ */
  1837→router.get('/response-times', async (req, res) => {
  1838→  try {
  1839→    if (!supabase) {
  1840→      return res.status(503).json({ error: 'Database not configured' });
  1841→    }
  1842→
  1843→    const { days = 7 } = req.query;
  1844→    const startDate = new Date();
  1845→    startDate.setDate(startDate.getDate() - parseInt(days));
  1846→
  1847→    // Get response times grouped by user
  1848→    const { data: responses, error } = await supabase
  1849→      .from('communications')
  1850→      .select('response_time_seconds, metadata')
  1851→      .eq('direction', 'outbound')
  1852→      .not('response_time_seconds', 'is', null)
  1853→      .gte('created_at', startDate.toISOString());
  1854→
  1855→    if (error) {
  1856→      console.error('Response times error:', error);
  1857→      return res.status(500).json({ error: 'Failed to fetch response times' });
  1858→    }
  1859→
  1860→    // Calculate stats
  1861→    const times = (responses || []).map(r => r.response_time_seconds);
  1862→    if (times.length === 0) {
  1863→      return res.json({
  1864→        count: 0,
  1865→        avgSeconds: 0,
  1866→        fastestSeconds: 0,
  1867→        avgFormatted: 'No data',
  1868→        fastestFormatted: 'No data'
  1869→      });
  1870→    }
  1871→
  1872→    const avg = Math.floor(times.reduce((a, b) => a + b, 0) / times.length);
  1873→    const fastest = Math.min(...times);
  1874→
  1875→    const formatDuration = (seconds) => {
  1876→      if (seconds < 60) return `${seconds}s`;
  1877→      if (seconds < 3600) return `${Math.floor(seconds / 60)}m`;
  1878→      return `${Math.floor(seconds / 3600)}h ${Math.floor((seconds % 3600) / 60)}m`;
  1879→    };
  1880→
  1881→    res.json({
  1882→      count: times.length,
  1883→      avgSeconds: avg,
  1884→      fastestSeconds: fastest,
  1885→      avgFormatted: formatDuration(avg),
  1886→      fastestFormatted: formatDuration(fastest),
  1887→      distribution: {
  1888→        under1min: times.filter(t => t < 60).length,
  1889→        under5min: times.filter(t => t < 300).length,
  1890→        under15min: times.filter(t => t < 900).length,
  1891→        under1hour: times.filter(t => t < 3600).length,
  1892→        over1hour: times.filter(t => t >= 3600).length
  1893→      }
  1894→    });
  1895→
  1896→  } catch (error) {
  1897→    console.error('Response times error:', error);
  1898→    res.status(500).json({ error: 'Failed to fetch response times' });
  1899→  }
  1900→});
  1901→
  1902→module.exports = router;
  1903→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


     1→/**
     2→ * Sales Rebuttals API routes
     3→ * CRUD for managing objections and rebuttal scripts
     4→ */
     5→
     6→const express = require('express');
     7→const router = express.Router();
     8→const { supabase, supabaseAdmin } = require('../config/database');
     9→const { isValidUUID, sanitizeString, errorResponse, successResponse } = require('../utils/validation');
    10→
    11→// Use admin client for RLS tables
    12→const db = supabaseAdmin || supabase;
    13→
    14→/**
    15→ * GET /api/objections
    16→ * List all objections with optional filtering
    17→ * Query params: category, search, tag
    18→ */
    19→router.get('/', async (req, res) => {
    20→  try {
    21→    if (!db) {
    22→      return res.status(503).json(errorResponse('Database not configured'));
    23→    }
    24→
    25→    const { category, search, tag } = req.query;
    26→    let query = db
    27→      .from('sales_rebuttals')
    28→      .select('*')
    29→      .order('created_at', { ascending: false });
    30→
    31→    if (category) {
    32→      query = query.eq('category', category);
    33→    }
    34→    if (search) {
    35→      query = query.ilike('title', `%${sanitizeString(search, 200)}%`);
    36→    }
    37→    if (tag) {
    38→      query = query.contains('tags', [tag]);
    39→    }
    40→
    41→    const { data, error } = await query;
    42→    if (error) {
    43→      console.error('Objections list error:', error.message);
    44→      throw error;
    45→    }
    46→
    47→    res.json(successResponse({ data: data || [] }));
    48→  } catch (error) {
    49→    console.error('Objections list error:', error);
    50→    res.status(500).json(errorResponse('Failed to fetch objections'));
    51→  }
    52→});
    53→
    54→/**
    55→ * POST /api/objections
    56→ * Create a new objection
    57→ */
    58→router.post('/', async (req, res) => {
    59→  try {
    60→    if (!db) {
    61→      return res.status(503).json(errorResponse('Database not configured'));
    62→    }
    63→
    64→    const { title, response, category, tags } = req.body;
    65→
    66→    // Validate required fields
    67→    if (!title?.trim()) {
    68→      return res.status(400).json(errorResponse('Title is required'));
    69→    }
    70→    if (!response?.trim()) {
    71→      return res.status(400).json(errorResponse('Response is required'));
    72→    }
    73→
    74→    const { data, error } = await db
    75→      .from('sales_rebuttals')
    76→      .insert({
    77→        title: sanitizeString(title, 500),
    78→        response: sanitizeString(response, 2000),
    79→        category: category ? sanitizeString(category, 100) : null,
    80→        tags: Array.isArray(tags) ? tags.map(t => sanitizeString(t, 50)).slice(0, 10) : []
    81→      })
    82→      .select()
    83→      .single();
    84→
    85→    if (error) {
    86→      console.error('Objection create error:', error.message);
    87→      throw error;
    88→    }
    89→
    90→    console.log(`Objection created: ${data.id}`);
    91→    res.status(201).json(successResponse({ data }));
    92→  } catch (error) {
    93→    console.error('Objection create error:', error);
    94→    res.status(500).json(errorResponse('Failed to create objection'));
    95→  }
    96→});
    97→
    98→/**
    99→ * PUT /api/objections/:id
   100→ * Update an objection
   101→ */
   102→router.put('/:id', async (req, res) => {
   103→  try {
   104→    if (!db) {
   105→      return res.status(503).json(errorResponse('Database not configured'));
   106→    }
   107→
   108→    const { id } = req.params;
   109→
   110→    // Validate UUID
   111→    if (!isValidUUID(id)) {
   112→      return res.status(400).json(errorResponse('Invalid ID format'));
   113→    }
   114→
   115→    const { title, response, category, tags } = req.body;
   116→    const updates = { updated_at: new Date().toISOString() };
   117→
   118→    // Build updates object (only include provided fields)
   119→    if (title !== undefined) {
   120→      if (!title?.trim()) {
   121→        return res.status(400).json(errorResponse('Title cannot be empty'));
   122→      }
   123→      updates.title = sanitizeString(title, 500);
   124→    }
   125→    if (response !== undefined) {
   126→      if (!response?.trim()) {
   127→        return res.status(400).json(errorResponse('Response cannot be empty'));
   128→      }
   129→      updates.response = sanitizeString(response, 2000);
   130→    }
   131→    if (category !== undefined) {
   132→      updates.category = category ? sanitizeString(category, 100) : null;
   133→    }
   134→    if (tags !== undefined) {
   135→      updates.tags = Array.isArray(tags) ? tags.map(t => sanitizeString(t, 50)).slice(0, 10) : [];
   136→    }
   137→
   138→    const { data, error } = await db
   139→      .from('sales_rebuttals')
   140→      .update(updates)
   141→      .eq('id', id)
   142→      .select()
   143→      .single();
   144→
   145→    if (error?.code === 'PGRST116') {
   146→      return res.status(404).json(errorResponse('Objection not found'));
   147→    }
   148→    if (error) {
   149→      console.error('Objection update error:', error.message);
   150→      throw error;
   151→    }
   152→
   153→    console.log(`Objection updated: ${id}`);
   154→    res.json(successResponse({ data }));
   155→  } catch (error) {
   156→    console.error('Objection update error:', error);
   157→    res.status(500).json(errorResponse('Failed to update objection'));
   158→  }
   159→});
   160→
   161→/**
   162→ * DELETE /api/objections/:id
   163→ * Delete an objection
   164→ */
   165→router.delete('/:id', async (req, res) => {
   166→  try {
   167→    if (!db) {
   168→      return res.status(503).json(errorResponse('Database not configured'));
   169→    }
   170→
   171→    const { id } = req.params;
   172→
   173→    // Validate UUID
   174→    if (!isValidUUID(id)) {
   175→      return res.status(400).json(errorResponse('Invalid ID format'));
   176→    }
   177→
   178→    const { error } = await db
   179→      .from('sales_rebuttals')
   180→      .delete()
   181→      .eq('id', id);
   182→
   183→    if (error) {
   184→      console.error('Objection delete error:', error.message);
   185→      throw error;
   186→    }
   187→
   188→    console.log(`Objection deleted: ${id}`);
   189→    res.status(204).send();
   190→  } catch (error) {
   191→    console.error('Objection delete error:', error);
   192→    res.status(500).json(errorResponse('Failed to delete objection'));
   193→  }
   194→});
   195→
   196→module.exports = router;
   197→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>