     1→/**
     2→ * Duels Service - XP Duels for Battleground V2
     3→ * Head-to-head challenges with AI-calculated odds
     4→ */
     5→
     6→const { supabase } = require('../config/database');
     7→
     8→// Duel types and their configurations
     9→const DUEL_TYPES = {
    10→  calls_hour: {
    11→    name: 'Call Blitz',
    12→    metric: 'calls',
    13→    duration: 60 * 60 * 1000, // 1 hour
    14→    description: 'Most calls in 1 hour'
    15→  },
    16→  calls_day: {
    17→    name: 'Daily Grind',
    18→    metric: 'calls',
    19→    duration: 24 * 60 * 60 * 1000, // 24 hours
    20→    description: 'Most calls today'
    21→  },
    22→  conversions_day: {
    23→    name: 'Closer Duel',
    24→    metric: 'conversions',
    25→    duration: 24 * 60 * 60 * 1000,
    26→    description: 'Most conversions today'
    27→  },
    28→  conversions_week: {
    29→    name: 'Weekly Showdown',
    30→    metric: 'conversions',
    31→    duration: 7 * 24 * 60 * 60 * 1000,
    32→    description: 'Most conversions this week'
    33→  }
    34→};
    35→
    36→// Stake limits
    37→const MIN_STAKE = 50;
    38→const MAX_STAKE = 500;
    39→const MAX_DAILY_LOSS_PERCENT = 0.5; // Can't lose more than 50% of daily XP
    40→
    41→/**
    42→ * Get user's average performance for a metric (last 30 days)
    43→ */
    44→async function getUserMetricAverage(userId, metric) {
    45→  try {
    46→    if (metric === 'calls') {
    47→      // Get average daily calls
    48→      const { data, error } = await supabase.rpc('get_user_metric_average', {
    49→        p_user_id: userId,
    50→        p_metric: 'calls'
    51→      });
    52→
    53→      if (error) {
    54→        console.error('Error getting call average:', error);
    55→        // Fallback: query directly
    56→        const { data: calls } = await supabase
    57→          .from('communications')
    58→          .select('created_at')
    59→          .eq('user_id', userId)
    60→          .eq('type', 'call_outbound')
    61→          .gte('created_at', new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString());
    62→
    63→        if (!calls || calls.length === 0) return 10; // Default for new users
    64→
    65→        // Group by day and average
    66→        const dayMap = {};
    67→        calls.forEach(c => {
    68→          const day = c.created_at.split('T')[0];
    69→          dayMap[day] = (dayMap[day] || 0) + 1;
    70→        });
    71→        const days = Object.values(dayMap);
    72→        return days.length > 0 ? days.reduce((a, b) => a + b, 0) / days.length : 10;
    73→      }
    74→
    75→      return data || 10;
    76→    }
    77→
    78→    if (metric === 'conversions') {
    79→      const { data: conversions } = await supabase
    80→        .from('leads')
    81→        .select('created_at')
    82→        .eq('assigned_to', userId)
    83→        .eq('status', 'converted')
    84→        .gte('created_at', new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString());
    85→
    86→      if (!conversions || conversions.length === 0) return 1; // Default
    87→
    88→      const dayMap = {};
    89→      conversions.forEach(c => {
    90→        const day = c.created_at.split('T')[0];
    91→        dayMap[day] = (dayMap[day] || 0) + 1;
    92→      });
    93→      const days = Object.values(dayMap);
    94→      return days.length > 0 ? days.reduce((a, b) => a + b, 0) / days.length : 1;
    95→    }
    96→
    97→    return 10; // Default
    98→  } catch (err) {
    99→    console.error('Error in getUserMetricAverage:', err);
   100→    return 10;
   101→  }
   102→}
   103→
   104→/**
   105→ * Calculate odds for a duel
   106→ * Returns odds from challenger's perspective (>1 = challenger favored)
   107→ */
   108→async function calculateOdds(challengerId, targetId, duelType) {
   109→  const metric = DUEL_TYPES[duelType]?.metric || 'calls';
   110→
   111→  const [challengerAvg, targetAvg] = await Promise.all([
   112→    getUserMetricAverage(challengerId, metric),
   113→    getUserMetricAverage(targetId, metric)
   114→  ]);
   115→
   116→  // Base odds = challenger avg / target avg
   117→  let odds = challengerAvg / Math.max(targetAvg, 0.1);
   118→
   119→  // Get duel history for adjustments
   120→  const { data: challengerHistory } = await supabase
   121→    .from('duel_history')
   122→    .select('*')
   123→    .eq('user_id', challengerId)
   124→    .single();
   125→
   126→  const { data: targetHistory } = await supabase
   127→    .from('duel_history')
   128→    .select('*')
   129→    .eq('user_id', targetId)
   130→    .single();
   131→
   132→  // Adjust for streak (hot hand / cold streak)
   133→  if (challengerHistory?.current_duel_streak > 2) {
   134→    odds *= 1.1; // Challenger on a streak, slight boost
   135→  }
   136→  if (targetHistory?.current_duel_streak > 2) {
   137→    odds *= 0.9; // Target on a streak, reduce challenger odds
   138→  }
   139→
   140→  // Cap odds between 0.25 and 4.0 (4:1 max either way)
   141→  odds = Math.max(0.25, Math.min(4.0, odds));
   142→
   143→  // Round to 2 decimal places
   144→  return Math.round(odds * 100) / 100;
   145→}
   146→
   147→/**
   148→ * Create a new duel challenge
   149→ */
   150→async function createDuel(challengerId, targetId, duelType, stakeAmount) {
   151→  // Validate duel type
   152→  if (!DUEL_TYPES[duelType]) {
   153→    throw new Error(`Invalid duel type: ${duelType}`);
   154→  }
   155→
   156→  // Validate stake
   157→  if (stakeAmount < MIN_STAKE || stakeAmount > MAX_STAKE) {
   158→    throw new Error(`Stake must be between ${MIN_STAKE} and ${MAX_STAKE} XP`);
   159→  }
   160→
   161→  // Check challenger isn't already in an active duel
   162→  const { data: existingDuel } = await supabase
   163→    .from('duels')
   164→    .select('id')
   165→    .or(`challenger_id.eq.${challengerId},target_id.eq.${challengerId}`)
   166→    .in('status', ['pending', 'active'])
   167→    .limit(1);
   168→
   169→  if (existingDuel && existingDuel.length > 0) {
   170→    throw new Error('You already have an active or pending duel');
   171→  }
   172→
   173→  // Check target isn't already in an active duel
   174→  const { data: targetDuel } = await supabase
   175→    .from('duels')
   176→    .select('id')
   177→    .or(`challenger_id.eq.${targetId},target_id.eq.${targetId}`)
   178→    .in('status', ['pending', 'active'])
   179→    .limit(1);
   180→
   181→  if (targetDuel && targetDuel.length > 0) {
   182→    throw new Error('Target is already in an active or pending duel');
   183→  }
   184→
   185→  // Calculate odds
   186→  const odds = await calculateOdds(challengerId, targetId, duelType);
   187→
   188→  // Create the duel
   189→  const { data: duel, error } = await supabase
   190→    .from('duels')
   191→    .insert({
   192→      challenger_id: challengerId,
   193→      target_id: targetId,
   194→      duel_type: duelType,
   195→      stake_amount: stakeAmount,
   196→      challenger_odds: odds,
   197→      status: 'pending'
   198→    })
   199→    .select()
   200→    .single();
   201→
   202→  if (error) throw error;
   203→
   204→  return {
   205→    ...duel,
   206→    duel_config: DUEL_TYPES[duelType],
   207→    odds_display: odds > 1
   208→      ? `You're ${odds}:1 favorite`
   209→      : `You're ${Math.round(1/odds * 100) / 100}:1 underdog`
   210→  };
   211→}
   212→
   213→/**
   214→ * Accept a duel challenge
   215→ */
   216→async function acceptDuel(duelId, userId) {
   217→  // Get the duel
   218→  const { data: duel, error: fetchError } = await supabase
   219→    .from('duels')
   220→    .select('*')
   221→    .eq('id', duelId)
   222→    .single();
   223→
   224→  if (fetchError || !duel) {
   225→    throw new Error('Duel not found');
   226→  }
   227→
   228→  if (duel.target_id !== userId) {
   229→    throw new Error('You are not the target of this duel');
   230→  }
   231→
   232→  if (duel.status !== 'pending') {
   233→    throw new Error('Duel is no longer pending');
   234→  }
   235→
   236→  // Calculate end time based on duel type
   237→  const duration = DUEL_TYPES[duel.duel_type]?.duration || 24 * 60 * 60 * 1000;
   238→  const now = new Date();
   239→  const endsAt = new Date(now.getTime() + duration);
   240→
   241→  // Update duel to active
   242→  const { data: updatedDuel, error: updateError } = await supabase
   243→    .from('duels')
   244→    .update({
   245→      status: 'active',
   246→      starts_at: now.toISOString(),
   247→      ends_at: endsAt.toISOString()
   248→    })
   249→    .eq('id', duelId)
   250→    .select()
   251→    .single();
   252→
   253→  if (updateError) throw updateError;
   254→
   255→  return {
   256→    ...updatedDuel,
   257→    duel_config: DUEL_TYPES[updatedDuel.duel_type]
   258→  };
   259→}
   260→
   261→/**
   262→ * Decline a duel challenge
   263→ */
   264→async function declineDuel(duelId, userId) {
   265→  const { data: duel, error: fetchError } = await supabase
   266→    .from('duels')
   267→    .select('*')
   268→    .eq('id', duelId)
   269→    .single();
   270→
   271→  if (fetchError || !duel) {
   272→    throw new Error('Duel not found');
   273→  }
   274→
   275→  if (duel.target_id !== userId) {
   276→    throw new Error('You are not the target of this duel');
   277→  }
   278→
   279→  if (duel.status !== 'pending') {
   280→    throw new Error('Duel is no longer pending');
   281→  }
   282→
   283→  const { error: updateError } = await supabase
   284→    .from('duels')
   285→    .update({ status: 'declined' })
   286→    .eq('id', duelId);
   287→
   288→  if (updateError) throw updateError;
   289→
   290→  return { success: true };
   291→}
   292→
   293→/**
   294→ * Get user's active and pending duels
   295→ */
   296→async function getUserDuels(userId) {
   297→  const { data: duels, error } = await supabase
   298→    .from('duels')
   299→    .select('*')
   300→    .or(`challenger_id.eq.${userId},target_id.eq.${userId}`)
   301→    .in('status', ['pending', 'active'])
   302→    .order('created_at', { ascending: false });
   303→
   304→  if (error) throw error;
   305→
   306→  return duels.map(duel => ({
   307→    ...duel,
   308→    duel_config: DUEL_TYPES[duel.duel_type],
   309→    is_challenger: duel.challenger_id === userId
   310→  }));
   311→}
   312→
   313→/**
   314→ * Get recent completed duels for a user
   315→ */
   316→async function getUserDuelHistory(userId, limit = 10) {
   317→  const { data: duels, error } = await supabase
   318→    .from('duels')
   319→    .select('*')
   320→    .or(`challenger_id.eq.${userId},target_id.eq.${userId}`)
   321→    .eq('status', 'completed')
   322→    .order('completed_at', { ascending: false })
   323→    .limit(limit);
   324→
   325→  if (error) throw error;
   326→
   327→  return duels.map(duel => ({
   328→    ...duel,
   329→    duel_config: DUEL_TYPES[duel.duel_type],
   330→    is_challenger: duel.challenger_id === userId,
   331→    won: duel.winner_id === userId
   332→  }));
   333→}
   334→
   335→/**
   336→ * Update duel scores based on current activity
   337→ */
   338→async function updateDuelScores(duelId) {
   339→  const { data: duel, error: fetchError } = await supabase
   340→    .from('duels')
   341→    .select('*')
   342→    .eq('id', duelId)
   343→    .eq('status', 'active')
   344→    .single();
   345→
   346→  if (fetchError || !duel) return null;
   347→
   348→  const metric = DUEL_TYPES[duel.duel_type]?.metric || 'calls';
   349→  const startTime = duel.starts_at;
   350→
   351→  let challengerScore = 0;
   352→  let targetScore = 0;
   353→
   354→  if (metric === 'calls') {
   355→    // Count calls since duel started
   356→    const { data: challengerCalls } = await supabase
   357→      .from('communications')
   358→      .select('id')
   359→      .eq('user_id', duel.challenger_id)
   360→      .eq('type', 'call_outbound')
   361→      .gte('created_at', startTime);
   362→
   363→    const { data: targetCalls } = await supabase
   364→      .from('communications')
   365→      .select('id')
   366→      .eq('user_id', duel.target_id)
   367→      .eq('type', 'call_outbound')
   368→      .gte('created_at', startTime);
   369→
   370→    challengerScore = challengerCalls?.length || 0;
   371→    targetScore = targetCalls?.length || 0;
   372→  } else if (metric === 'conversions') {
   373→    // Count conversions since duel started
   374→    const { data: challengerConversions } = await supabase
   375→      .from('leads')
   376→      .select('id')
   377→      .eq('assigned_to', duel.challenger_id)
   378→      .eq('status', 'converted')
   379→      .gte('updated_at', startTime);
   380→
   381→    const { data: targetConversions } = await supabase
   382→      .from('leads')
   383→      .select('id')
   384→      .eq('assigned_to', duel.target_id)
   385→      .eq('status', 'converted')
   386→      .gte('updated_at', startTime);
   387→
   388→    challengerScore = challengerConversions?.length || 0;
   389→    targetScore = targetConversions?.length || 0;
   390→  }
   391→
   392→  // Update scores
   393→  const { data: updatedDuel, error: updateError } = await supabase
   394→    .from('duels')
   395→    .update({
   396→      challenger_score: challengerScore,
   397→      target_score: targetScore
   398→    })
   399→    .eq('id', duelId)
   400→    .select()
   401→    .single();
   402→
   403→  if (updateError) throw updateError;
   404→
   405→  return updatedDuel;
   406→}
   407→
   408→/**
   409→ * Complete a duel and distribute XP
   410→ */
   411→async function completeDuel(duelId) {
   412→  const { data: duel, error: fetchError } = await supabase
   413→    .from('duels')
   414→    .select('*')
   415→    .eq('id', duelId)
   416→    .single();
   417→
   418→  if (fetchError || !duel) {
   419→    throw new Error('Duel not found');
   420→  }
   421→
   422→  if (duel.status !== 'active') {
   423→    throw new Error('Duel is not active');
   424→  }
   425→
   426→  // Update scores one final time
   427→  await updateDuelScores(duelId);
   428→
   429→  // Re-fetch with updated scores
   430→  const { data: finalDuel } = await supabase
   431→    .from('duels')
   432→    .select('*')
   433→    .eq('id', duelId)
   434→    .single();
   435→
   436→  // Determine winner
   437→  let winnerId = null;
   438→  let xpWon = 0;
   439→
   440→  if (finalDuel.challenger_score > finalDuel.target_score) {
   441→    winnerId = finalDuel.challenger_id;
   442→    xpWon = Math.round(finalDuel.stake_amount * finalDuel.challenger_odds);
   443→  } else if (finalDuel.target_score > finalDuel.challenger_score) {
   444→    winnerId = finalDuel.target_id;
   445→    xpWon = Math.round(finalDuel.stake_amount / Math.max(finalDuel.challenger_odds, 0.25));
   446→  }
   447→  // If tie, winnerId stays null, no XP transferred
   448→
   449→  // Update duel as completed
   450→  const { data: completedDuel, error: completeError } = await supabase
   451→    .from('duels')
   452→    .update({
   453→      status: 'completed',
   454→      completed_at: new Date().toISOString(),
   455→      winner_id: winnerId,
   456→      xp_won: xpWon
   457→    })
   458→    .eq('id', duelId)
   459→    .select()
   460→    .single();
   461→
   462→  if (completeError) throw completeError;
   463→
   464→  // Update XP for winner and loser
   465→  if (winnerId) {
   466→    const loserId = winnerId === finalDuel.challenger_id
   467→      ? finalDuel.target_id
   468→      : finalDuel.challenger_id;
   469→
   470→    // Add XP to winner (atomic function prevents race conditions)
   471→    const { error: xpError } = await supabase.rpc('add_user_xp', {
   472→      p_user_id: winnerId,
   473→      p_xp_amount: xpWon
   474→    });
   475→    if (xpError) {
   476→      console.error('Failed to award duel XP:', xpError.message);
   477→    }
   478→
   479→    // Update duel history
   480→    await updateDuelHistory(winnerId, true, xpWon);
   481→    await updateDuelHistory(loserId, false, finalDuel.stake_amount);
   482→
   483→    // Create celebration event
   484→    await createCelebrationEvent('duel_win', winnerId, xpWon, completedDuel);
   485→  }
   486→
   487→  return completedDuel;
   488→}
   489→
   490→/**
   491→ * Update user's duel history
   492→ */
   493→async function updateDuelHistory(userId, won, xpAmount) {
   494→  const { data: existing } = await supabase
   495→    .from('duel_history')
   496→    .select('*')
   497→    .eq('user_id', userId)
   498→    .single();
   499→
   500→  if (existing) {
   501→    await supabase
   502→      .from('duel_history')
   503→      .update({
   504→        duels_won: won ? existing.duels_won + 1 : existing.duels_won,
   505→        duels_lost: won ? existing.duels_lost : existing.duels_lost + 1,
   506→        total_xp_won: won ? existing.total_xp_won + xpAmount : existing.total_xp_won,
   507→        total_xp_lost: won ? existing.total_xp_lost : existing.total_xp_lost + xpAmount,
   508→        current_duel_streak: won ? existing.current_duel_streak + 1 : 0,
   509→        best_duel_streak: won
   510→          ? Math.max(existing.best_duel_streak, existing.current_duel_streak + 1)
   511→          : existing.best_duel_streak,
   512→        last_duel_at: new Date().toISOString(),
   513→        updated_at: new Date().toISOString()
   514→      })
   515→      .eq('user_id', userId);
   516→  } else {
   517→    await supabase
   518→      .from('duel_history')
   519→      .insert({
   520→        user_id: userId,
   521→        duels_won: won ? 1 : 0,
   522→        duels_lost: won ? 0 : 1,
   523→        total_xp_won: won ? xpAmount : 0,
   524→        total_xp_lost: won ? 0 : xpAmount,
   525→        current_duel_streak: won ? 1 : 0,
   526→        best_duel_streak: won ? 1 : 0,
   527→        last_duel_at: new Date().toISOString()
   528→      });
   529→  }
   530→}
   531→
   532→/**
   533→ * Create a celebration event for team-wide notification
   534→ */
   535→async function createCelebrationEvent(eventType, userId, xpAmount, metadata = {}) {
   536→  const titles = {
   537→    duel_win: 'DUEL VICTORY!',
   538→    conversion: 'JUST CLOSED!',
   539→    streak_milestone: 'STREAK MILESTONE!',
   540→    challenge_win: 'CHALLENGE WON!'
   541→  };
   542→
   543→  await supabase
   544→    .from('celebration_events')
   545→    .insert({
   546→      event_type: eventType,
   547→      user_id: userId,
   548→      title: titles[eventType] || 'CELEBRATION!',
   549→      xp_amount: xpAmount,
   550→      metadata
   551→    });
   552→}
   553→
   554→/**
   555→ * Get recent celebration events (for team pulse)
   556→ */
   557→async function getRecentCelebrations(limit = 10) {
   558→  const { data, error } = await supabase
   559→    .from('celebration_events')
   560→    .select('*')
   561→    .order('created_at', { ascending: false })
   562→    .limit(limit);
   563→
   564→  if (error) throw error;
   565→  return data;
   566→}
   567→
   568→/**
   569→ * Process expired duels (run periodically)
   570→ */
   571→async function processExpiredDuels() {
   572→  // Expire pending duels older than 24 hours
   573→  await supabase
   574→    .from('duels')
   575→    .update({ status: 'expired' })
   576→    .eq('status', 'pending')
   577→    .lt('created_at', new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString());
   578→
   579→  // Complete active duels that have ended
   580→  const { data: endedDuels } = await supabase
   581→    .from('duels')
   582→    .select('id')
   583→    .eq('status', 'active')
   584→    .lt('ends_at', new Date().toISOString());
   585→
   586→  if (endedDuels && endedDuels.length > 0) {
   587→    for (const duel of endedDuels) {
   588→      try {
   589→        await completeDuel(duel.id);
   590→      } catch (err) {
   591→        console.error(`Error completing duel ${duel.id}:`, err);
   592→      }
   593→    }
   594→  }
   595→}
   596→
   597→module.exports = {
   598→  DUEL_TYPES,
   599→  calculateOdds,
   600→  createDuel,
   601→  acceptDuel,
   602→  declineDuel,
   603→  getUserDuels,
   604→  getUserDuelHistory,
   605→  updateDuelScores,
   606→  completeDuel,
   607→  createCelebrationEvent,
   608→  getRecentCelebrations,
   609→  processExpiredDuels
   610→};
   611→

<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→ * Deal Intelligence Service
     3→ * Analyzes objection outcomes, deal velocity, and stall prediction
     4→ */
     5→
     6→const { supabase, supabaseAdmin } = require('../config/database');
     7→
     8→// Use admin client for tables with RLS
     9→const db = supabaseAdmin || supabase;
    10→
    11→/**
    12→ * Record a deal stage transition
    13→ */
    14→async function recordStageTransition(leadId, stage, notes = null, metadata = {}) {
    15→  try {
    16→    // Close previous stage if exists (specific columns for performance)
    17→    const { data: prevStage } = await db
    18→      .from('deal_stages')
    19→      .select('id, entered_at')
    20→      .eq('lead_id', leadId)
    21→      .is('exited_at', null)
    22→      .order('entered_at', { ascending: false })
    23→      .limit(1)
    24→      .single();
    25→
    26→    if (prevStage) {
    27→      const enteredAt = new Date(prevStage.entered_at);
    28→      const now = new Date();
    29→      const durationHours = Math.round((now - enteredAt) / (1000 * 60 * 60));
    30→
    31→      await db
    32→        .from('deal_stages')
    33→        .update({
    34→          exited_at: now.toISOString(),
    35→          duration_hours: durationHours
    36→        })
    37→        .eq('id', prevStage.id);
    38→    }
    39→
    40→    // Create new stage
    41→    const { data, error } = await db
    42→      .from('deal_stages')
    43→      .insert({
    44→        lead_id: leadId,
    45→        stage,
    46→        notes,
    47→        metadata
    48→      })
    49→      .select()
    50→      .single();
    51→
    52→    if (error) {
    53→      console.error('Error recording stage transition:', error);
    54→      return { success: false, error: error.message };
    55→    }
    56→
    57→    return { success: true, stage: data };
    58→  } catch (error) {
    59→    console.error('Error in recordStageTransition:', error);
    60→    return { success: false, error: error.message };
    61→  }
    62→}
    63→
    64→/**
    65→ * Record deal outcome (win/loss)
    66→ */
    67→async function recordDealOutcome(data) {
    68→  const {
    69→    leadId,
    70→    outcome, // 'won', 'lost', 'stalled'
    71→    primaryReason,
    72→    secondaryReasons = [],
    73→    competitorLostTo = null,
    74→    dealValue = null,
    75→    winningResponse = null,
    76→    repNotes = null,
    77→    recordedBy = null
    78→  } = data;
    79→
    80→  try {
    81→    // Get lead info for touchpoints calculation
    82→    const { data: lead } = await db
    83→      .from('leads')
    84→      .select('created_at')
    85→      .eq('id', leadId)
    86→      .single();
    87→
    88→    // Count touchpoints
    89→    const { count: touchpointsCount } = await db
    90→      .from('communications')
    91→      .select('*', { count: 'exact', head: true })
    92→      .eq('lead_id', leadId);
    93→
    94→    // Get objections encountered
    95→    const { data: objectionData } = await db
    96→      .from('objection_responses')
    97→      .select('objection_type')
    98→      .eq('lead_id', leadId);
    99→
   100→    const objectionsEncountered = [...new Set(
   101→      (objectionData || []).map(o => o.objection_type)
   102→    )];
   103→
   104→    // Calculate time to close
   105→    let timeToCloseDays = null;
   106→    if (lead?.created_at) {
   107→      const createdAt = new Date(lead.created_at);
   108→      const now = new Date();
   109→      timeToCloseDays = Math.ceil((now - createdAt) / (1000 * 60 * 60 * 24));
   110→    }
   111→
   112→    // Upsert outcome
   113→    const { data: outcomeData, error } = await db
   114→      .from('deal_outcomes')
   115→      .upsert({
   116→        lead_id: leadId,
   117→        outcome,
   118→        primary_reason: primaryReason,
   119→        secondary_reasons: secondaryReasons,
   120→        competitor_lost_to: competitorLostTo,
   121→        deal_value: dealValue,
   122→        time_to_close_days: timeToCloseDays,
   123→        touchpoints_count: touchpointsCount || 0,
   124→        objections_encountered: objectionsEncountered,
   125→        winning_response: winningResponse,
   126→        rep_notes: repNotes,
   127→        recorded_by: recordedBy,
   128→        recorded_at: new Date().toISOString()
   129→      }, {
   130→        onConflict: 'lead_id'
   131→      })
   132→      .select()
   133→      .single();
   134→
   135→    if (error) {
   136→      console.error('Error recording deal outcome:', error);
   137→      return { success: false, error: error.message };
   138→    }
   139→
   140→    // Update objection outcomes for analytics
   141→    if (outcome === 'won' || outcome === 'lost') {
   142→      await updateObjectionOutcomes(leadId, outcome === 'won', timeToCloseDays);
   143→    }
   144→
   145→    // Record final stage
   146→    await recordStageTransition(leadId, outcome);
   147→
   148→    return { success: true, outcome: outcomeData };
   149→  } catch (error) {
   150→    console.error('Error in recordDealOutcome:', error);
   151→    return { success: false, error: error.message };
   152→  }
   153→}
   154→
   155→/**
   156→ * Update objection_responses with outcome data
   157→ */
   158→async function updateObjectionOutcomes(leadId, converted, daysToOutcome) {
   159→  try {
   160→    // Find objection responses linked to this lead's communications
   161→    const { data: comms } = await db
   162→      .from('communications')
   163→      .select('metadata')
   164→      .eq('lead_id', leadId)
   165→      .not('metadata', 'is', null);
   166→
   167→    // Extract unique objection types
   168→    const objectionTypes = new Set();
   169→    (comms || []).forEach(c => {
   170→      if (c.metadata?.objection_type) {
   171→        objectionTypes.add(c.metadata.objection_type);
   172→      }
   173→      if (c.metadata?.objections) {
   174→        c.metadata.objections.forEach(o => {
   175→          if (typeof o === 'string') objectionTypes.add(o);
   176→          else if (o.type) objectionTypes.add(o.type);
   177→        });
   178→      }
   179→    });
   180→
   181→    // Update objection_responses for matching types
   182→    for (const objType of objectionTypes) {
   183→      // Get responses used during this lead's journey
   184→      const { data: responses } = await db
   185→        .from('objection_responses')
   186→        .select('id, led_to_conversion_bool')
   187→        .eq('objection_type', objType);
   188→
   189→      for (const response of (responses || [])) {
   190→        await db
   191→          .from('objection_responses')
   192→          .update({
   193→            led_to_conversion_bool: converted,
   194→            lead_final_outcome: converted ? 'won' : 'lost',
   195→            days_to_outcome: daysToOutcome,
   196→            updated_at: new Date().toISOString()
   197→          })
   198→          .eq('id', response.id);
   199→      }
   200→    }
   201→  } catch (error) {
   202→    console.error('Error updating objection outcomes:', error);
   203→  }
   204→}
   205→
   206→/**
   207→ * Analyze objection impact across all deals
   208→ */
   209→async function analyzeObjectionImpact() {
   210→  try {
   211→    const { data, error } = await db
   212→      .from('objection_impact_stats')
   213→      .select('*')
   214→      .order('conversion_rate', { ascending: false });
   215→
   216→    if (error) {
   217→      console.error('Error fetching objection impact:', error);
   218→      return { success: false, error: error.message };
   219→    }
   220→
   221→    return {
   222→      success: true,
   223→      objections: data || []
   224→    };
   225→  } catch (error) {
   226→    console.error('Error in analyzeObjectionImpact:', error);
   227→    return { success: false, error: error.message };
   228→  }
   229→}
   230→
   231→/**
   232→ * Find winning responses for a specific objection type
   233→ */
   234→async function findWinningResponses(objectionType) {
   235→  try {
   236→    const { data, error } = await db
   237→      .from('objection_responses')
   238→      .select('response_text, times_shown, effectiveness_score, led_to_conversion')
   239→      .eq('objection_type', objectionType)
   240→      .eq('led_to_conversion_bool', true)
   241→      .order('effectiveness_score', { ascending: false })
   242→      .limit(5);
   243→
   244→    if (error) {
   245→      console.error('Error finding winning responses:', error);
   246→      return { success: false, error: error.message };
   247→    }
   248→
   249→    return {
   250→      success: true,
   251→      responses: (data || []).map(r => ({
   252→        response: r.response_text,
   253→        timesUsed: r.times_shown,
   254→        effectivenessScore: r.effectiveness_score,
   255→        conversions: r.led_to_conversion
   256→      }))
   257→    };
   258→  } catch (error) {
   259→    console.error('Error in findWinningResponses:', error);
   260→    return { success: false, error: error.message };
   261→  }
   262→}
   263→
   264→/**
   265→ * Calculate deal velocity statistics
   266→ */
   267→async function getDealVelocity(options = {}) {
   268→  const { startDate, endDate, userId } = options;
   269→
   270→  try {
   271→    let query = db
   272→      .from('deal_outcomes')
   273→      .select('time_to_close_days, touchpoints_count, deal_value')
   274→      .eq('outcome', 'won');
   275→
   276→    if (startDate) {
   277→      query = query.gte('recorded_at', startDate);
   278→    }
   279→    if (endDate) {
   280→      query = query.lte('recorded_at', endDate);
   281→    }
   282→    if (userId) {
   283→      query = query.eq('recorded_by', userId);
   284→    }
   285→
   286→    const { data, error } = await query;
   287→
   288→    if (error) {
   289→      console.error('Error fetching deal velocity:', error);
   290→      return { success: false, error: error.message };
   291→    }
   292→
   293→    const deals = data || [];
   294→
   295→    if (deals.length === 0) {
   296→      return {
   297→        success: true,
   298→        velocity: {
   299→          avgDaysToClose: 0,
   300→          avgTouchpoints: 0,
   301→          avgDealValue: 0,
   302→          totalDeals: 0,
   303→          fastDeals: 0, // <2 days
   304→          normalDeals: 0, // 2-7 days
   305→          slowDeals: 0 // >7 days
   306→        }
   307→      };
   308→    }
   309→
   310→    const avgDaysToClose = deals.reduce((sum, d) => sum + (d.time_to_close_days || 0), 0) / deals.length;
   311→    const avgTouchpoints = deals.reduce((sum, d) => sum + (d.touchpoints_count || 0), 0) / deals.length;
   312→    const avgDealValue = deals.reduce((sum, d) => sum + (parseFloat(d.deal_value) || 0), 0) / deals.length;
   313→
   314→    const fastDeals = deals.filter(d => d.time_to_close_days && d.time_to_close_days < 2).length;
   315→    const normalDeals = deals.filter(d => d.time_to_close_days && d.time_to_close_days >= 2 && d.time_to_close_days <= 7).length;
   316→    const slowDeals = deals.filter(d => d.time_to_close_days && d.time_to_close_days > 7).length;
   317→
   318→    return {
   319→      success: true,
   320→      velocity: {
   321→        avgDaysToClose: Math.round(avgDaysToClose * 10) / 10,
   322→        avgTouchpoints: Math.round(avgTouchpoints * 10) / 10,
   323→        avgDealValue: Math.round(avgDealValue * 100) / 100,
   324→        totalDeals: deals.length,
   325→        fastDeals,
   326→        normalDeals,
   327→        slowDeals
   328→      }
   329→    };
   330→  } catch (error) {
   331→    console.error('Error in getDealVelocity:', error);
   332→    return { success: false, error: error.message };
   333→  }
   334→}
   335→
   336→/**
   337→ * Predict if a lead is at risk of stalling
   338→ */
   339→async function predictStall(leadId) {
   340→  try {
   341→    // Get lead data (specific columns for performance)
   342→    const { data: lead } = await db
   343→      .from('leads')
   344→      .select('id, score, status, updated_at')
   345→      .eq('id', leadId)
   346→      .single();
   347→
   348→    if (!lead) {
   349→      return { success: false, error: 'Lead not found' };
   350→    }
   351→
   352→    // Get communications (specific columns for performance)
   353→    const { data: comms } = await db
   354→      .from('communications')
   355→      .select('direction, created_at')
   356→      .eq('lead_id', leadId)
   357→      .order('created_at', { ascending: false });
   358→
   359→    // Get callbacks (just check existence)
   360→    const { data: callbacks } = await db
   361→      .from('callbacks')
   362→      .select('id')
   363→      .eq('lead_id', leadId)
   364→      .eq('status', 'pending')
   365→      .gte('callback_time', new Date().toISOString());
   366→
   367→    const signals = [];
   368→    const now = new Date();
   369→
   370→    // Signal 1: No response in 3+ days
   371→    const lastInbound = (comms || []).find(c => c.direction === 'inbound');
   372→    if (lastInbound) {
   373→      const daysSinceResponse = Math.ceil((now - new Date(lastInbound.created_at)) / (1000 * 60 * 60 * 24));
   374→      if (daysSinceResponse > 3) {
   375→        signals.push({
   376→          risk: 'high',
   377→          reason: `No response in ${daysSinceResponse} days`,
   378→          weight: 30
   379→        });
   380→      }
   381→    } else if (comms && comms.length > 0) {
   382→      // No inbound ever
   383→      signals.push({
   384→        risk: 'medium',
   385→        reason: 'Never received a response',
   386→        weight: 20
   387→      });
   388→    }
   389→
   390→    // Signal 2: No callback scheduled
   391→    if (!callbacks || callbacks.length === 0) {
   392→      signals.push({
   393→        risk: 'medium',
   394→        reason: 'No callback scheduled',
   395→        weight: 15
   396→      });
   397→    }
   398→
   399→    // Signal 3: Multiple unanswered outreach
   400→    const outbound = (comms || []).filter(c => c.direction === 'outbound');
   401→    const unansweredCount = outbound.filter((c, i) => {
   402→      // Check if next message is inbound (response)
   403→      const nextComm = (comms || [])[i + 1];
   404→      return !nextComm || nextComm.direction !== 'inbound';
   405→    }).length;
   406→
   407→    if (unansweredCount >= 3) {
   408→      signals.push({
   409→        risk: 'high',
   410→        reason: `${unansweredCount} unanswered messages`,
   411→        weight: 25
   412→      });
   413→    }
   414→
   415→    // Signal 4: Score declining
   416→    if (lead.score && lead.score < 30) {
   417→      signals.push({
   418→        risk: 'medium',
   419→        reason: `Low lead score (${lead.score})`,
   420→        weight: 15
   421→      });
   422→    }
   423→
   424→    // Signal 5: Status is stale
   425→    if (lead.status === 'contacted' || lead.status === 'cold') {
   426→      const lastActivity = comms?.[0]?.created_at || lead.updated_at;
   427→      const daysSinceActivity = Math.ceil((now - new Date(lastActivity)) / (1000 * 60 * 60 * 24));
   428→      if (daysSinceActivity > 5) {
   429→        signals.push({
   430→          risk: 'medium',
   431→          reason: `No activity in ${daysSinceActivity} days`,
   432→          weight: 20
   433→        });
   434→      }
   435→    }
   436→
   437→    // Calculate stall probability
   438→    const totalWeight = signals.reduce((sum, s) => sum + s.weight, 0);
   439→    const stallProbability = Math.min(100, totalWeight);
   440→
   441→    // Generate recommendation
   442→    let recommendation = null;
   443→    if (stallProbability >= 70) {
   444→      recommendation = 'Urgent: Call immediately or try a different approach';
   445→    } else if (stallProbability >= 50) {
   446→      recommendation = 'Schedule a callback and try a personalized message';
   447→    } else if (stallProbability >= 30) {
   448→      recommendation = 'Send a follow-up message with a new value proposition';
   449→    }
   450→
   451→    return {
   452→      success: true,
   453→      prediction: {
   454→        stallProbability,
   455→        riskLevel: stallProbability >= 70 ? 'high' : stallProbability >= 40 ? 'medium' : 'low',
   456→        signals,
   457→        recommendation
   458→      }
   459→    };
   460→  } catch (error) {
   461→    console.error('Error in predictStall:', error);
   462→    return { success: false, error: error.message };
   463→  }
   464→}
   465→
   466→/**
   467→ * Get loss reason breakdown
   468→ */
   469→async function getLossReasons(options = {}) {
   470→  const { startDate, endDate } = options;
   471→
   472→  try {
   473→    let query = db
   474→      .from('deal_outcomes')
   475→      .select('primary_reason')
   476→      .eq('outcome', 'lost')
   477→      .not('primary_reason', 'is', null);
   478→
   479→    if (startDate) {
   480→      query = query.gte('recorded_at', startDate);
   481→    }
   482→    if (endDate) {
   483→      query = query.lte('recorded_at', endDate);
   484→    }
   485→
   486→    const { data, error } = await query;
   487→
   488→    if (error) {
   489→      console.error('Error fetching loss reasons:', error);
   490→      return { success: false, error: error.message };
   491→    }
   492→
   493→    // Count by reason
   494→    const counts = {};
   495→    (data || []).forEach(d => {
   496→      counts[d.primary_reason] = (counts[d.primary_reason] || 0) + 1;
   497→    });
   498→
   499→    const total = data?.length || 0;
   500→    const reasons = Object.entries(counts)
   501→      .map(([reason, count]) => ({
   502→        reason,
   503→        count,
   504→        percentage: total > 0 ? Math.round((count / total) * 100) : 0
   505→      }))
   506→      .sort((a, b) => b.count - a.count);
   507→
   508→    return {
   509→      success: true,
   510→      total,
   511→      reasons
   512→    };
   513→  } catch (error) {
   514→    console.error('Error in getLossReasons:', error);
   515→    return { success: false, error: error.message };
   516→  }
   517→}
   518→
   519→/**
   520→ * Get list of configured loss reasons for UI
   521→ */
   522→async function getLossReasonOptions() {
   523→  try {
   524→    const { data, error } = await db
   525→      .from('loss_reasons')
   526→      .select('reason, category')
   527→      .eq('is_active', true)
   528→      .order('display_order', { ascending: true });
   529→
   530→    if (error) {
   531→      console.error('Error fetching loss reason options:', error);
   532→      return { success: false, error: error.message };
   533→    }
   534→
   535→    return {
   536→      success: true,
   537→      reasons: data || []
   538→    };
   539→  } catch (error) {
   540→    console.error('Error in getLossReasonOptions:', error);
   541→    return { success: false, error: error.message };
   542→  }
   543→}
   544→
   545→/**
   546→ * Get deals at risk of stalling
   547→ */
   548→async function getDealsAtRisk(limit = 10) {
   549→  try {
   550→    // Get active leads
   551→    const { data: leads, error } = await db
   552→      .from('leads')
   553→      .select('id, first_name, last_name, company, score, status, updated_at')
   554→      .in('status', ['new', 'contacted', 'warm', 'cold'])
   555→      .order('score', { ascending: false })
   556→      .limit(50);
   557→
   558→    if (error) {
   559→      console.error('Error fetching leads:', error);
   560→      return { success: false, error: error.message };
   561→    }
   562→
   563→    // Predict stall for each lead
   564→    const atRisk = [];
   565→    for (const lead of (leads || [])) {
   566→      const prediction = await predictStall(lead.id);
   567→      if (prediction.success && prediction.prediction.stallProbability >= 40) {
   568→        atRisk.push({
   569→          leadId: lead.id,
   570→          name: `${lead.first_name || ''} ${lead.last_name || ''}`.trim() || lead.company,
   571→          company: lead.company,
   572→          score: lead.score,
   573→          status: lead.status,
   574→          ...prediction.prediction
   575→        });
   576→      }
   577→    }
   578→
   579→    // Sort by risk and return top N
   580→    return {
   581→      success: true,
   582→      dealsAtRisk: atRisk
   583→        .sort((a, b) => b.stallProbability - a.stallProbability)
   584→        .slice(0, limit)
   585→    };
   586→  } catch (error) {
   587→    console.error('Error in getDealsAtRisk:', error);
   588→    return { success: false, error: error.message };
   589→  }
   590→}
   591→
   592→module.exports = {
   593→  recordStageTransition,
   594→  recordDealOutcome,
   595→  analyzeObjectionImpact,
   596→  findWinningResponses,
   597→  getDealVelocity,
   598→  predictStall,
   599→  getLossReasons,
   600→  getLossReasonOptions,
   601→  getDealsAtRisk
   602→};
   603→

<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→ * Self-Improvement Service
     3→ * Automatically adjusts thresholds and learns from system performance
     4→ *
     5→ * Part of the 0.1% Intelligence System
     6→ */
     7→
     8→const { supabase, supabaseAdmin } = require('../config/database');
     9→const { sendSlackMessage } = require('./slack');
    10→
    11→const db = supabaseAdmin || supabase;
    12→
    13→// Default threshold configuration (stored in database for persistence)
    14→const DEFAULT_THRESHOLDS = {
    15→  // Friction detection
    16→  rage_clicks_per_minute: 3,
    17→  errors_per_5min: 10,
    18→  abandonment_rate_spike: 50,
    19→
    20→  // API monitoring
    21→  api_failures_per_5min: 5,
    22→  sms_failure_rate: 20,
    23→  slow_api_threshold_ms: 5000,
    24→
    25→  // AI quality
    26→  min_suggestion_confidence: 0.6,
    27→  ai_quality_target: 70, // Target 70% success rate
    28→};
    29→
    30→/**
    31→ * Get current thresholds from database (or defaults)
    32→ */
    33→async function getThresholds() {
    34→  const { data, error } = await db
    35→    .from('system_settings')
    36→    .select('value')
    37→    .eq('key', 'intelligence_thresholds')
    38→    .single();
    39→
    40→  if (error) {
    41→    // PGRST116 is "no rows" which is expected on first run
    42→    if (error.code !== 'PGRST116') {
    43→      console.error('[SelfImprovement] Error fetching thresholds:', error);
    44→    }
    45→    return DEFAULT_THRESHOLDS;
    46→  }
    47→
    48→  return data?.value ? { ...DEFAULT_THRESHOLDS, ...data.value } : DEFAULT_THRESHOLDS;
    49→}
    50→
    51→/**
    52→ * Save updated thresholds to database
    53→ */
    54→async function saveThresholds(thresholds, changes, reason) {
    55→  try {
    56→    // Upsert thresholds
    57→    const { error: upsertError } = await db
    58→      .from('system_settings')
    59→      .upsert({
    60→        key: 'intelligence_thresholds',
    61→        value: thresholds,
    62→        updated_at: new Date().toISOString()
    63→      }, { onConflict: 'key' });
    64→
    65→    if (upsertError) {
    66→      console.error('[SelfImprovement] Failed to upsert thresholds:', upsertError);
    67→      return false;
    68→    }
    69→
    70→    // Log the adjustment
    71→    const { error: insertError } = await db
    72→      .from('intelligence_adjustments')
    73→      .insert({
    74→        adjustment_type: 'threshold',
    75→        changes: changes,
    76→        reason: reason,
    77→        created_at: new Date().toISOString()
    78→      });
    79→
    80→    if (insertError) {
    81→      console.error('[SelfImprovement] Failed to log adjustment:', insertError);
    82→      // Continue anyway - threshold was saved, just logging failed
    83→    }
    84→
    85→    console.log('[SelfImprovement] Thresholds updated:', changes);
    86→    return true;
    87→  } catch (error) {
    88→    console.error('[SelfImprovement] Failed to save thresholds:', error);
    89→    return false;
    90→  }
    91→}
    92→
    93→/**
    94→ * Analyze recent suggestion performance
    95→ */
    96→async function analyzeSuggestionPerformance() {
    97→  const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString();
    98→
    99→  const { data: suggestions, error } = await db
   100→    .from('intelligence_suggestions')
   101→    .select('*')
   102→    .gte('created_at', thirtyDaysAgo);
   103→
   104→  if (error) {
   105→    console.error('[SelfImprovement] Error fetching suggestions:', error);
   106→    return { hasEnoughData: false };
   107→  }
   108→
   109→  if (!suggestions || suggestions.length < 10) {
   110→    return { hasEnoughData: false };
   111→  }
   112→
   113→  const implemented = suggestions.filter(s => s.status === 'implemented');
   114→  const successful = implemented.filter(s => s.success_verdict === 'success');
   115→  const failed = implemented.filter(s => s.success_verdict === 'failed');
   116→
   117→  const successRate = implemented.length > 0
   118→    ? (successful.length / implemented.length) * 100
   119→    : 0;
   120→
   121→  const implementationRate = (implemented.length / suggestions.length) * 100;
   122→
   123→  // Analyze by category
   124→  const byCategory = {};
   125→  suggestions.forEach(s => {
   126→    const cat = s.category || 'unknown';
   127→    if (!byCategory[cat]) {
   128→      byCategory[cat] = { total: 0, implemented: 0, successful: 0 };
   129→    }
   130→    byCategory[cat].total++;
   131→    if (s.status === 'implemented') {
   132→      byCategory[cat].implemented++;
   133→      if (s.success_verdict === 'success') {
   134→        byCategory[cat].successful++;
   135→      }
   136→    }
   137→  });
   138→
   139→  // Find best and worst performing categories
   140→  const categoryStats = Object.entries(byCategory).map(([cat, stats]) => ({
   141→    category: cat,
   142→    ...stats,
   143→    successRate: stats.implemented > 0 ? (stats.successful / stats.implemented) * 100 : 0
   144→  })).sort((a, b) => b.successRate - a.successRate);
   145→
   146→  return {
   147→    hasEnoughData: true,
   148→    total: suggestions.length,
   149→    implemented: implemented.length,
   150→    successful: successful.length,
   151→    failed: failed.length,
   152→    successRate: Math.round(successRate),
   153→    implementationRate: Math.round(implementationRate),
   154→    byCategory: categoryStats,
   155→    bestCategory: categoryStats[0],
   156→    worstCategory: categoryStats[categoryStats.length - 1]
   157→  };
   158→}
   159→
   160→/**
   161→ * Analyze friction detection accuracy
   162→ */
   163→async function analyzeFrictionAccuracy() {
   164→  const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString();
   165→
   166→  const { data: frictions, error } = await db
   167→    .from('detected_friction')
   168→    .select('*')
   169→    .gte('created_at', thirtyDaysAgo);
   170→
   171→  if (error) {
   172→    console.error('[SelfImprovement] Error fetching frictions:', error);
   173→    return { hasEnoughData: false };
   174→  }
   175→
   176→  if (!frictions || frictions.length < 5) {
   177→    return { hasEnoughData: false };
   178→  }
   179→
   180→  const resolved = frictions.filter(f => f.status === 'resolved');
   181→  const falsePositives = frictions.filter(f => f.status === 'rejected');
   182→
   183→  const accuracyRate = frictions.length > 0
   184→    ? ((frictions.length - falsePositives.length) / frictions.length) * 100
   185→    : 100;
   186→
   187→  // Analyze by type
   188→  const byType = {};
   189→  frictions.forEach(f => {
   190→    const type = f.friction_type || 'unknown';
   191→    if (!byType[type]) {
   192→      byType[type] = { total: 0, resolved: 0, rejected: 0 };
   193→    }
   194→    byType[type].total++;
   195→    if (f.status === 'resolved') byType[type].resolved++;
   196→    if (f.status === 'rejected') byType[type].rejected++;
   197→  });
   198→
   199→  return {
   200→    hasEnoughData: true,
   201→    total: frictions.length,
   202→    resolved: resolved.length,
   203→    falsePositives: falsePositives.length,
   204→    accuracyRate: Math.round(accuracyRate),
   205→    byType
   206→  };
   207→}
   208→
   209→/**
   210→ * Auto-adjust thresholds based on performance data
   211→ */
   212→async function autoAdjustThresholds() {
   213→  console.log('\n[SelfImprovement] Running auto-adjustment analysis...');
   214→
   215→  const currentThresholds = await getThresholds();
   216→  const suggestionPerf = await analyzeSuggestionPerformance();
   217→  const frictionPerf = await analyzeFrictionAccuracy();
   218→
   219→  const adjustments = [];
   220→  const newThresholds = { ...currentThresholds };
   221→
   222→  // Check suggestion performance
   223→  if (suggestionPerf.hasEnoughData) {
   224→    // If success rate is below target, increase confidence threshold
   225→    if (suggestionPerf.successRate < currentThresholds.ai_quality_target) {
   226→      const increase = Math.min(0.1, (currentThresholds.ai_quality_target - suggestionPerf.successRate) / 100);
   227→      newThresholds.min_suggestion_confidence = Math.min(0.9, currentThresholds.min_suggestion_confidence + increase);
   228→
   229→      adjustments.push({
   230→        threshold: 'min_suggestion_confidence',
   231→        old: currentThresholds.min_suggestion_confidence,
   232→        new: newThresholds.min_suggestion_confidence,
   233→        reason: `Success rate (${suggestionPerf.successRate}%) below target (${currentThresholds.ai_quality_target}%)`
   234→      });
   235→    }
   236→
   237→    // If success rate is significantly above target, can slightly decrease confidence threshold
   238→    if (suggestionPerf.successRate > currentThresholds.ai_quality_target + 15) {
   239→      const decrease = 0.05;
   240→      newThresholds.min_suggestion_confidence = Math.max(0.5, currentThresholds.min_suggestion_confidence - decrease);
   241→
   242→      adjustments.push({
   243→        threshold: 'min_suggestion_confidence',
   244→        old: currentThresholds.min_suggestion_confidence,
   245→        new: newThresholds.min_suggestion_confidence,
   246→        reason: `Success rate (${suggestionPerf.successRate}%) significantly above target`
   247→      });
   248→    }
   249→  }
   250→
   251→  // Check friction detection accuracy
   252→  if (frictionPerf.hasEnoughData) {
   253→    // If too many false positives for rage clicks, increase threshold
   254→    const rageClickFP = frictionPerf.byType?.rage_clicks?.rejected || 0;
   255→    const rageClickTotal = frictionPerf.byType?.rage_clicks?.total || 1;
   256→    const rageFPRate = (rageClickFP / rageClickTotal) * 100;
   257→
   258→    if (rageFPRate > 30) {
   259→      newThresholds.rage_clicks_per_minute = Math.min(10, currentThresholds.rage_clicks_per_minute + 1);
   260→
   261→      adjustments.push({
   262→        threshold: 'rage_clicks_per_minute',
   263→        old: currentThresholds.rage_clicks_per_minute,
   264→        new: newThresholds.rage_clicks_per_minute,
   265→        reason: `High false positive rate (${Math.round(rageFPRate)}%) for rage clicks`
   266→      });
   267→    }
   268→
   269→    // If too many false positives for errors, increase threshold
   270→    const errorFP = frictionPerf.byType?.error_prone?.rejected || 0;
   271→    const errorTotal = frictionPerf.byType?.error_prone?.total || 1;
   272→    const errorFPRate = (errorFP / errorTotal) * 100;
   273→
   274→    if (errorFPRate > 30) {
   275→      newThresholds.errors_per_5min = Math.min(25, currentThresholds.errors_per_5min + 2);
   276→
   277→      adjustments.push({
   278→        threshold: 'errors_per_5min',
   279→        old: currentThresholds.errors_per_5min,
   280→        new: newThresholds.errors_per_5min,
   281→        reason: `High false positive rate (${Math.round(errorFPRate)}%) for error detection`
   282→      });
   283→    }
   284→  }
   285→
   286→  // Apply adjustments if any
   287→  if (adjustments.length > 0) {
   288→    await saveThresholds(newThresholds, adjustments, 'auto_adjustment');
   289→
   290→    // Notify via Slack
   291→    const adjustmentList = adjustments.map(a =>
   292→      `• ${a.threshold}: ${a.old} → ${a.new} (${a.reason})`
   293→    ).join('\n');
   294→
   295→    await sendSlackMessage({
   296→      text: `:gear: *Self-Improvement: Threshold Adjustments*\n\n${adjustmentList}\n\n_Auto-adjusted based on 30-day performance data_`
   297→    });
   298→  }
   299→
   300→  return {
   301→    adjustmentsMade: adjustments.length,
   302→    adjustments,
   303→    suggestionPerformance: suggestionPerf,
   304→    frictionAccuracy: frictionPerf
   305→  };
   306→}
   307→
   308→/**
   309→ * Learn from successful suggestions to improve future recommendations
   310→ */
   311→async function learnFromSuccess() {
   312→  console.log('[SelfImprovement] Analyzing successful suggestions...');
   313→
   314→  // Get successful suggestions from last 90 days
   315→  const ninetyDaysAgo = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000).toISOString();
   316→
   317→  const { data: successfulSuggestions, error } = await db
   318→    .from('intelligence_suggestions')
   319→    .select('*')
   320→    .eq('status', 'implemented')
   321→    .eq('success_verdict', 'success')
   322→    .gte('created_at', ninetyDaysAgo);
   323→
   324→  if (error) {
   325→    console.error('[SelfImprovement] Error fetching successful suggestions:', error);
   326→    return { patternsFound: 0 };
   327→  }
   328→
   329→  if (!successfulSuggestions || successfulSuggestions.length === 0) {
   330→    return { patternsFound: 0 };
   331→  }
   332→
   333→  // Extract patterns from successful suggestions
   334→  const patterns = {
   335→    categories: {},
   336→    impactTypes: {},
   337→    avgImpact: 0,
   338→    commonKeywords: {}
   339→  };
   340→
   341→  let totalImpact = 0;
   342→
   343→  successfulSuggestions.forEach(s => {
   344→    // Track categories
   345→    const cat = s.category || 'unknown';
   346→    patterns.categories[cat] = (patterns.categories[cat] || 0) + 1;
   347→
   348→    // Track impact
   349→    if (s.measured_impact) {
   350→      totalImpact += s.measured_impact;
   351→    }
   352→
   353→    // Extract keywords from title/description
   354→    const text = `${s.title || ''} ${s.description || ''}`.toLowerCase();
   355→    const keywords = text.match(/\b\w{4,}\b/g) || [];
   356→    keywords.forEach(kw => {
   357→      patterns.commonKeywords[kw] = (patterns.commonKeywords[kw] || 0) + 1;
   358→    });
   359→  });
   360→
   361→  patterns.avgImpact = successfulSuggestions.length > 0
   362→    ? Math.round(totalImpact / successfulSuggestions.length)
   363→    : 0;
   364→
   365→  // Store learned patterns
   366→  const { error: upsertError } = await db
   367→    .from('system_settings')
   368→    .upsert({
   369→      key: 'learned_success_patterns',
   370→      value: {
   371→        patterns,
   372→        updatedAt: new Date().toISOString(),
   373→        basedOn: successfulSuggestions.length
   374→      }
   375→    }, { onConflict: 'key' });
   376→
   377→  if (upsertError) {
   378→    console.error('[SelfImprovement] Failed to save patterns:', upsertError);
   379→  }
   380→
   381→  return {
   382→    patternsFound: Object.keys(patterns.categories).length,
   383→    topCategories: Object.entries(patterns.categories)
   384→      .sort((a, b) => b[1] - a[1])
   385→      .slice(0, 5),
   386→    avgImpact: patterns.avgImpact,
   387→    suggestionCount: successfulSuggestions.length
   388→  };
   389→}
   390→
   391→/**
   392→ * Generate self-improvement report
   393→ */
   394→async function generateSelfImprovementReport() {
   395→  const adjustmentResult = await autoAdjustThresholds();
   396→  const learningResult = await learnFromSuccess();
   397→
   398→  const report = {
   399→    timestamp: new Date().toISOString(),
   400→    thresholdAdjustments: adjustmentResult,
   401→    patternLearning: learningResult,
   402→    summary: []
   403→  };
   404→
   405→  // Generate summary
   406→  if (adjustmentResult.adjustmentsMade > 0) {
   407→    report.summary.push(`Made ${adjustmentResult.adjustmentsMade} threshold adjustment(s)`);
   408→  }
   409→
   410→  if (adjustmentResult.suggestionPerformance?.hasEnoughData) {
   411→    report.summary.push(`AI suggestion success rate: ${adjustmentResult.suggestionPerformance.successRate}%`);
   412→  }
   413→
   414→  if (adjustmentResult.frictionAccuracy?.hasEnoughData) {
   415→    report.summary.push(`Friction detection accuracy: ${adjustmentResult.frictionAccuracy.accuracyRate}%`);
   416→  }
   417→
   418→  if (learningResult.patternsFound > 0) {
   419→    report.summary.push(`Learned from ${learningResult.suggestionCount} successful suggestions`);
   420→  }
   421→
   422→  return report;
   423→}
   424→
   425→module.exports = {
   426→  getThresholds,
   427→  saveThresholds,
   428→  analyzeSuggestionPerformance,
   429→  analyzeFrictionAccuracy,
   430→  autoAdjustThresholds,
   431→  learnFromSuccess,
   432→  generateSelfImprovementReport,
   433→  DEFAULT_THRESHOLDS
   434→};
   435→

<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→ * RateRight Platform Sync Service
     3→ * Syncs users from RateRight Admin API to Growth Engine CRM
     4→ */
     5→
     6→const { supabase } = require('../config/database');
     7→const { fetchWithTimeout, isTimeoutError } = require('../utils/fetchWithTimeout');
     8→const { normalizeAustralianPhone } = require('../utils/phone');
     9→const logger = require('../utils/logger');
    10→const {
    11→  calculateWorkerStage,
    12→  calculateContractorStage,
    13→  calculateHealthScore,
    14→  logStageTransition,
    15→} = require('./stageCalculator');
    16→
    17→// Configuration from environment
    18→const PLATFORM_API_URL = process.env.RATERIGHT_API_URL || 'https://rateright.com.au/api';
    19→const PLATFORM_API_KEY = process.env.RATERIGHT_ADMIN_API_KEY;
    20→
    21→// Score boost for re-engaged users
    22→const RE_ENGAGEMENT_SCORE_BOOST = 50;
    23→
    24→/**
    25→ * Make authenticated request to Platform API
    26→ */
    27→async function platformRequest(endpoint, options = {}) {
    28→  if (!PLATFORM_API_KEY) {
    29→    throw new Error('RATERIGHT_ADMIN_API_KEY not configured');
    30→  }
    31→
    32→  const url = `${PLATFORM_API_URL}${endpoint}`;
    33→
    34→  try {
    35→    const response = await fetchWithTimeout(url, {
    36→      ...options,
    37→      headers: {
    38→        'X-API-Key': PLATFORM_API_KEY,
    39→        'Content-Type': 'application/json',
    40→        ...options.headers,
    41→      },
    42→    }, 30000); // 30 second timeout for database queries
    43→
    44→    if (!response.ok) {
    45→      const errorText = await response.text();
    46→      throw new Error(`Platform API error ${response.status}: ${errorText}`);
    47→    }
    48→
    49→    return response.json();
    50→  } catch (error) {
    51→    if (isTimeoutError(error)) {
    52→      throw new Error('Platform API request timed out after 30 seconds');
    53→    }
    54→    throw error;
    55→  }
    56→}
    57→
    58→/**
    59→ * Fetch all users from Platform Admin API
    60→ */
    61→async function fetchPlatformUsers() {
    62→  try {
    63→    const data = await platformRequest('/admin/users');
    64→    // Handle both array response and { users: [] } response
    65→    return Array.isArray(data) ? data : (data.users || []);
    66→  } catch (error) {
    67→    logger.error('Failed to fetch platform users', { error: error.message });
    68→    throw error;
    69→  }
    70→}
    71→
    72→/**
    73→ * Fetch platform stats
    74→ */
    75→async function fetchPlatformStats() {
    76→  try {
    77→    return await platformRequest('/admin/stats');
    78→  } catch (error) {
    79→    logger.error('Failed to fetch platform stats', { error: error.message });
    80→    throw error;
    81→  }
    82→}
    83→
    84→/**
    85→ * Fetch jobs from Platform Admin API
    86→ */
    87→async function fetchPlatformJobs() {
    88→  try {
    89→    const data = await platformRequest('/admin/jobs');
    90→    // Handle paginated response
    91→    return data.jobs || (Array.isArray(data) ? data : []);
    92→  } catch (error) {
    93→    logger.warn('Failed to fetch platform jobs', { error: error.message });
    94→    return [];
    95→  }
    96→}
    97→
    98→/**
    99→ * Fetch contracts from Platform Admin API
   100→ */
   101→async function fetchPlatformContracts() {
   102→  try {
   103→    const data = await platformRequest('/admin/contracts');
   104→    // Handle paginated response
   105→    return data.contracts || (Array.isArray(data) ? data : []);
   106→  } catch (error) {
   107→    logger.warn('Failed to fetch platform contracts', { error: error.message });
   108→    return [];
   109→  }
   110→}
   111→
   112→/**
   113→ * Get jobs for a specific user
   114→ */
   115→function getUserJobs(allJobs, platformUserId) {
   116→  return allJobs.filter(j => j.contractor_id === platformUserId);
   117→}
   118→
   119→/**
   120→ * Get contracts for a specific user
   121→ */
   122→function getUserContracts(allContracts, platformUserId, role) {
   123→  if (role === 'worker') {
   124→    return allContracts.filter(c => c.worker_id === platformUserId);
   125→  }
   126→  return allContracts.filter(c => c.contractor_id === platformUserId);
   127→}
   128→
   129→/**
   130→ * Find existing lead by email or phone
   131→ */
   132→async function findExistingLead(email, phone) {
   133→  if (!supabase) return null;
   134→
   135→  // Try email first
   136→  if (email) {
   137→    const { data } = await supabase
   138→      .from('leads')
   139→      .select('*')
   140→      .eq('email', email.toLowerCase())
   141→      .is('deleted_at', null)
   142→      .single();
   143→    if (data) return data;
   144→  }
   145→
   146→  // Try phone
   147→  if (phone) {
   148→    const normalizedPhone = normalizeAustralianPhone(phone);
   149→    if (normalizedPhone) {
   150→      const { data } = await supabase
   151→        .from('leads')
   152→        .select('*')
   153→        .eq('phone', normalizedPhone)
   154→        .is('deleted_at', null)
   155→        .single();
   156→      if (data) return data;
   157→    }
   158→  }
   159→
   160→  return null;
   161→}
   162→
   163→/**
   164→ * Determine lead type from platform user role
   165→ */
   166→function getLeadType(platformUser) {
   167→  const role = (platformUser.role || platformUser.type || '').toLowerCase();
   168→
   169→  if (role.includes('worker') || role === 'worker') {
   170→    return 'worker';
   171→  }
   172→  if (role.includes('contractor') || role.includes('employer') || role === 'contractor') {
   173→    return 'contractor';
   174→  }
   175→  // Default to contractor for business accounts
   176→  if (platformUser.company || platformUser.business_name || platformUser.abn) {
   177→    return 'contractor';
   178→  }
   179→  return 'unknown';
   180→}
   181→
   182→/**
   183→ * Sync a single platform user to CRM lead
   184→ * Enhanced to store all platform data and calculate lifecycle stage
   185→ */
   186→async function syncPlatformUser(platformUser, allJobs = [], allContracts = []) {
   187→  if (!supabase) {
   188→    throw new Error('Database not configured');
   189→  }
   190→
   191→  const email = platformUser.email?.toLowerCase();
   192→  const phone = platformUser.phone_number
   193→    ? normalizeAustralianPhone(platformUser.phone_number)
   194→    : null;
   195→
   196→  // Must have at least email or phone
   197→  if (!email && !phone) {
   198→    return { success: false, reason: 'No email or phone' };
   199→  }
   200→
   201→  const leadType = getLeadType(platformUser);
   202→
   203→  // Get user's jobs and contracts
   204→  const userJobs = getUserJobs(allJobs, platformUser.id);
   205→  const userContracts = getUserContracts(allContracts, platformUser.id, leadType);
   206→
   207→  // Calculate stage based on lead type
   208→  const stage = leadType === 'worker'
   209→    ? calculateWorkerStage(platformUser, userContracts)
   210→    : calculateContractorStage(platformUser, userJobs, userContracts);
   211→
   212→  // Calculate health score
   213→  const healthScore = calculateHealthScore(platformUser);
   214→
   215→  // Store all 35 platform fields
   216→  const platformData = {
   217→    id: platformUser.id,
   218→    abn_number: platformUser.abn_number,
   219→    account_status: platformUser.account_status,
   220→    average_rating: platformUser.average_rating,
   221→    business_name: platformUser.business_name,
   222→    current_level: platformUser.current_level,
   223→    date_created: platformUser.date_created,
   224→    email: platformUser.email,
   225→    first_name: platformUser.first_name,
   226→    gst_registered: platformUser.gst_registered,
   227→    insurance_expiry_date: platformUser.insurance_expiry_date,
   228→    is_active: platformUser.is_active,
   229→    jobs_completed: platformUser.jobs_completed,
   230→    last_login: platformUser.last_login,
   231→    last_name: platformUser.last_name,
   232→    location: platformUser.location,
   233→    phone_number: platformUser.phone_number,
   234→    preferences: platformUser.preferences,
   235→    primary_trade: platformUser.primary_trade,
   236→    profile_picture: platformUser.profile_picture,
   237→    public_liability_amount: platformUser.public_liability_amount,
   238→    public_liability_insurance: platformUser.public_liability_insurance,
   239→    response_rate: platformUser.response_rate,
   240→    role: platformUser.role,
   241→    seasonal_league: platformUser.seasonal_league,
   242→    stripe_charges_enabled: platformUser.stripe_charges_enabled,
   243→    stripe_onboarding_complete: platformUser.stripe_onboarding_complete,
   244→    stripe_payouts_enabled: platformUser.stripe_payouts_enabled,
   245→    total_points: platformUser.total_points,
   246→    total_reviews: platformUser.total_reviews,
   247→    username: platformUser.username,
   248→    white_card_expiry: platformUser.white_card_expiry,
   249→    white_card_number: platformUser.white_card_number,
   250→    worker_control_level: platformUser.worker_control_level,
   251→    workers_comp_insurance: platformUser.workers_comp_insurance,
   252→    // Add computed data
   253→    _jobs_count: userJobs.length,
   254→    _contracts_count: userContracts.length,
   255→    _synced_at: new Date().toISOString(),
   256→  };
   257→
   258→  // Check for existing lead
   259→  const existingLead = await findExistingLead(email, phone);
   260→
   261→  if (existingLead) {
   262→    // Check if stage has changed
   263→    const previousStage = existingLead.platform_stage;
   264→    const stageChanged = previousStage && previousStage !== stage;
   265→
   266→    // Re-engagement! Update existing lead
   267→    const updates = {
   268→      // Store full platform data
   269→      platform_data: platformData,
   270→      platform_stage: stage,
   271→      platform_health_score: healthScore,
   272→      // Update metadata
   273→      metadata: {
   274→        ...(existingLead.metadata || {}),
   275→        platform_user_id: platformUser.id,
   276→        platform_role: platformUser.role,
   277→        platform_synced_at: new Date().toISOString(),
   278→        re_engaged: true,
   279→        re_engaged_at: new Date().toISOString(),
   280→        lead_type: leadType,
   281→      },
   282→      // Boost score for re-engagement
   283→      score: Math.min(100, (existingLead.score || 0) + RE_ENGAGEMENT_SCORE_BOOST),
   284→      updated_at: new Date().toISOString(),
   285→    };
   286→
   287→    // Update stage_changed_at if stage changed
   288→    if (stageChanged) {
   289→      updates.stage_changed_at = new Date().toISOString();
   290→    }
   291→
   292→    // If they have a company now and we didn't have one
   293→    if ((platformUser.business_name || platformUser.company) && !existingLead.company) {
   294→      updates.company = platformUser.business_name || platformUser.company;
   295→    }
   296→
   297→    const { error } = await supabase
   298→      .from('leads')
   299→      .update(updates)
   300→      .eq('id', existingLead.id);
   301→
   302→    if (error) {
   303→      logger.error('Failed to update lead for re-engagement', { error: error.message, leadId: existingLead.id });
   304→      return { success: false, reason: error.message };
   305→    }
   306→
   307→    // Log stage transition if changed
   308→    if (stageChanged) {
   309→      await logStageTransition(existingLead.id, previousStage, stage, 'platform_sync', {
   310→        healthScore,
   311→        platformUserId: platformUser.id,
   312→      });
   313→    }
   314→
   315→    logger.info('Lead re-engaged from platform', {
   316→      leadId: existingLead.id,
   317→      leadType,
   318→      stage,
   319→      healthScore,
   320→      stageChanged,
   321→    });
   322→
   323→    return {
   324→      success: true,
   325→      action: 're_engaged',
   326→      leadId: existingLead.id,
   327→      leadType,
   328→      stage,
   329→      healthScore,
   330→      previousScore: existingLead.score,
   331→      newScore: updates.score,
   332→    };
   333→  }
   334→
   335→  // Create new lead
   336→  const newLead = {
   337→    first_name: platformUser.first_name || null,
   338→    last_name: platformUser.last_name || null,
   339→    email: email,
   340→    phone: phone,
   341→    company: platformUser.business_name || null,
   342→    source: 'platform_signup',
   343→    status: 'new',
   344→    score: leadType === 'contractor' ? 30 : 20,
   345→    // New platform fields
   346→    platform_data: platformData,
   347→    platform_stage: stage,
   348→    platform_health_score: healthScore,
   349→    stage_changed_at: new Date().toISOString(),
   350→    metadata: {
   351→      platform_user_id: platformUser.id,
   352→      platform_role: platformUser.role,
   353→      platform_created_at: platformUser.date_created,
   354→      platform_synced_at: new Date().toISOString(),
   355→      lead_type: leadType,
   356→      abn: platformUser.abn_number || null,
   357→    },
   358→  };
   359→
   360→  const { data, error } = await supabase
   361→    .from('leads')
   362→    .insert(newLead)
   363→    .select('id')
   364→    .single();
   365→
   366→  if (error) {
   367→    logger.error('Failed to create lead from platform', { error: error.message, email });
   368→    return { success: false, reason: error.message };
   369→  }
   370→
   371→  // Log initial stage
   372→  await logStageTransition(data.id, null, stage, 'platform_sync', {
   373→    healthScore,
   374→    platformUserId: platformUser.id,
   375→    isNew: true,
   376→  });
   377→
   378→  logger.info('New lead created from platform', {
   379→    leadId: data.id,
   380→    leadType,
   381→    stage,
   382→    healthScore,
   383→  });
   384→
   385→  return {
   386→    success: true,
   387→    action: 'created',
   388→    leadId: data.id,
   389→    leadType,
   390→    stage,
   391→    healthScore,
   392→  };
   393→}
   394→
   395→/**
   396→ * Sync all platform users to CRM
   397→ * Returns summary of sync results
   398→ */
   399→async function syncAllPlatformUsers(options = {}) {
   400→  const { sinceDate, limit } = options;
   401→
   402→  logger.info('Starting platform user sync', { sinceDate, limit });
   403→  const startTime = Date.now();
   404→
   405→  // Fetch users, jobs, and contracts from platform
   406→  let users;
   407→  let allJobs = [];
   408→  let allContracts = [];
   409→
   410→  try {
   411→    users = await fetchPlatformUsers();
   412→  } catch (error) {
   413→    return {
   414→      success: false,
   415→      error: error.message,
   416→      duration: Date.now() - startTime,
   417→    };
   418→  }
   419→
   420→  // Fetch jobs and contracts for stage calculation (non-blocking)
   421→  try {
   422→    allJobs = await fetchPlatformJobs();
   423→    logger.info('Fetched platform jobs', { count: allJobs.length });
   424→  } catch (error) {
   425→    logger.warn('Could not fetch jobs for stage calculation', { error: error.message });
   426→  }
   427→
   428→  try {
   429→    allContracts = await fetchPlatformContracts();
   430→    logger.info('Fetched platform contracts', { count: allContracts.length });
   431→  } catch (error) {
   432→    logger.warn('Could not fetch contracts for stage calculation', { error: error.message });
   433→  }
   434→
   435→  // Filter by date if specified
   436→  if (sinceDate) {
   437→    const since = new Date(sinceDate);
   438→    users = users.filter(u => {
   439→      const createdAt = new Date(u.created_at || u.createdAt);
   440→      return createdAt >= since;
   441→    });
   442→  }
   443→
   444→  // Limit if specified
   445→  if (limit && users.length > limit) {
   446→    users = users.slice(0, limit);
   447→  }
   448→
   449→  // Sync each user
   450→  const results = {
   451→    total: users.length,
   452→    created: 0,
   453→    re_engaged: 0,
   454→    skipped: 0,
   455→    errors: 0,
   456→    workers: 0,
   457→    contractors: 0,
   458→    details: [],
   459→  };
   460→
   461→  for (const user of users) {
   462→    const result = await syncPlatformUser(user, allJobs, allContracts);
   463→
   464→    if (result.success) {
   465→      if (result.action === 'created') {
   466→        results.created++;
   467→      } else if (result.action === 're_engaged') {
   468→        results.re_engaged++;
   469→      }
   470→
   471→      if (result.leadType === 'worker') results.workers++;
   472→      if (result.leadType === 'contractor') results.contractors++;
   473→
   474→      results.details.push({
   475→        platformUserId: user.id,
   476→        email: user.email,
   477→        action: result.action,
   478→        leadId: result.leadId,
   479→        leadType: result.leadType,
   480→        stage: result.stage,
   481→        healthScore: result.healthScore,
   482→      });
   483→    } else {
   484→      if (result.reason === 'No email or phone') {
   485→        results.skipped++;
   486→      } else {
   487→        results.errors++;
   488→      }
   489→    }
   490→  }
   491→
   492→  const duration = Date.now() - startTime;
   493→
   494→  logger.info('Platform sync completed', {
   495→    total: results.total,
   496→    created: results.created,
   497→    re_engaged: results.re_engaged,
   498→    workers: results.workers,
   499→    contractors: results.contractors,
   500→    duration,
   501→  });
   502→
   503→  return {
   504→    success: true,
   505→    ...results,
   506→    duration,
   507→  };
   508→}
   509→
   510→/**
   511→ * Check if platform API is configured and accessible
   512→ */
   513→async function checkPlatformConnection() {
   514→  if (!PLATFORM_API_KEY) {
   515→    return {
   516→      configured: false,
   517→      error: 'RATERIGHT_ADMIN_API_KEY not set'
   518→    };
   519→  }
   520→
   521→  try {
   522→    const stats = await fetchPlatformStats();
   523→    return {
   524→      configured: true,
   525→      connected: true,
   526→      stats,
   527→    };
   528→  } catch (error) {
   529→    return {
   530→      configured: true,
   531→      connected: false,
   532→      error: error.message
   533→    };
   534→  }
   535→}
   536→
   537→module.exports = {
   538→  platformRequest,
   539→  fetchPlatformUsers,
   540→  fetchPlatformStats,
   541→  fetchPlatformJobs,
   542→  fetchPlatformContracts,
   543→  syncPlatformUser,
   544→  syncAllPlatformUsers,
   545→  checkPlatformConnection,
   546→  findExistingLead,
   547→  getLeadType,
   548→  getUserJobs,
   549→  getUserContracts,
   550→};
   551→

<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>