'pending', 'pending_id' => $pendingId, 'company_id' => (int)$company['id'], 'message' => 'Nuevo mensaje pendiente de aprobación', ]); $ch = curl_init($webhookUrl); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => $payload, CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', 'X-API-Key: ' . ($company['api_key'] ?? ''), ], CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10, ]); curl_exec($ch); curl_close($ch); } private static function classifyAndProcess(array $company, array $context, string $input, string $inputType): ?array { $botType = $company['bot_type'] ?? 'normal'; return match ($botType) { 'normal' => self::runNormalBot($company, $context, $input, $inputType), 'ai' => self::runAiBot($company, $context, $input), 'hybrid' => self::runHybridBot($company, $context, $input, $inputType), default => null, }; } private static function runNormalBot(array $company, array $context, string $input, string $inputType): ?array { if ($inputType === 'interactive' || $inputType === 'button') { return NormalBot::processInteractive($company, $context, $input); } return NormalBot::process($company, $context, $input); } private static function runAiBot(array $company, array $context, string $input): ?array { return AiBot::process($company, $context, $input); } private static function runHybridBot(array $company, array $context, string $input, string $inputType): ?array { $response = self::runNormalBot($company, $context, $input, $inputType); if ($response !== null) { return $response; } self::log("NormalBot no manejó '{$input}', escalando a IA"); return self::runAiBot($company, $context, $input); } private static function shouldSkipBot(array $company, array $context): bool { if (!(int)($company['is_active'] ?? 0)) { return true; } $config = self::getConfig($company); $ignorePrefixes = $config['ignore_prefixes'] ?? []; foreach ($ignorePrefixes as $prefix) { if (str_starts_with($context['from'] ?? '', $prefix)) { return true; } } return false; } private static function enqueueResponse(array $response, array $company): void { $to = $response['to'] ?? ''; $type = $response['type'] ?? 'text'; $payload = $response['payload'] ?? ''; if ($to === '' || $payload === '') { return; } try { $stmt = db()->prepare("INSERT INTO outbound_queue (company_id, to_number, message_type, payload) VALUES (?, ?, ?, ?)"); $stmt->execute([$company['id'], $to, $type, $payload]); $queueId = (int)db()->lastInsertId(); self::log("Bot encoló respuesta {$type} para {$to} [q#{$queueId}]"); $content = $payload; if ($type !== 'text') { $decoded = json_decode($payload, true); $content = $decoded['caption'] ?? $decoded['body'] ?? $decoded['text'] ?? $payload; } $messageId = 'bot_out_' . $company['id'] . '_' . time() . '_' . bin2hex(random_bytes(4)); $stmt2 = db()->prepare(" INSERT IGNORE INTO conversations (company_id, message_id, phone_number, direction, message_type, content, timestamp) VALUES (?, ?, ?, 'outbound', ?, ?, ?) "); $stmt2->execute([ $company['id'], $messageId, $to, $type, mb_substr($content, 0, 1000), time(), ]); $phoneNumberId = $company['phone_number_id'] ?: env('WHATSAPP_PHONE_NUMBER_ID', ''); if ($phoneNumberId === '') { self::log("ERROR: phone_number_id no configurado para company {$company['id']}"); return; } $decodedPayload = json_decode($payload, true); if (!is_array($decodedPayload)) { self::log("ERROR: payload inválido para q#{$queueId}"); return; } $sendResult = match ($type) { 'text' => WhatsAppSender::sendText($to, $decodedPayload['text'] ?? '', $phoneNumberId), 'interactive' => WhatsAppSender::sendInteractive($to, $decodedPayload['interactive'] ?? [], $phoneNumberId), 'image' => WhatsAppSender::sendImage($to, $decodedPayload['media_id'] ?? $decodedPayload['url'] ?? '', $phoneNumberId, $decodedPayload['caption'] ?? null), default => ['success' => false, 'error' => "Tipo no soportado: {$type}"], }; if ($sendResult['success']) { db()->prepare("UPDATE outbound_queue SET status = 'sent', wam_id = ?, updated_at = NOW() WHERE id = ?") ->execute([$sendResult['wam_id'], $queueId]); self::log("Bot envió {$type} a {$to} (wam:{$sendResult['wam_id']})"); } else { db()->prepare("UPDATE outbound_queue SET status = 'queued', last_error = ?, updated_at = NOW() WHERE id = ?") ->execute([$sendResult['error'] ?? 'error', $queueId]); self::log("Bot no pudo enviar {$type} a {$to}: {$sendResult['error']} — queda en cola"); } } catch (\Throwable $e) { self::log('ERROR en enqueueResponse: ' . $e->getMessage()); } } private static function getConfig(array $company): array { $json = $company['config_json'] ?? ''; if ($json === '') return []; $config = json_decode($json, true); return is_array($config) ? $config : []; } private static function log(string $message): void { $dir = dirname(__DIR__) . '/storage/logs'; if (!is_dir($dir)) { mkdir($dir, 0755, true); } $file = $dir . '/bot-' . date('Y-m-d') . '.log'; $line = '[' . date('Y-m-d H:i:s') . '] [BOT] ' . $message . PHP_EOL; file_put_contents($file, $line, FILE_APPEND | LOCK_EX); if (env('APP_ENV', 'production') === 'local') { error_log($line); } } }