Files
bot_palmas/setup/migrate.php
T
Lizandro GuarnizoandClaude Sonnet 4.6 5506ebd2af feat: AI usage logging (AiLogger, ai_logs table, admin view)
- AiLogger.php: new service, inserts into ai_logs on every AI call
- ai_logs table: added to migrate.php (provider, model, call_type, tokens, duration_ms)
- AiBot: logs chat, media_response, nlu calls with timing and token counts
- MediaTranscriber: logs whisper, geminiAudio, openaiVision, geminiVision, claudeVision
- /admin/ai-logs: paginated table with provider/company filter
- Layout.php: added IA Logs nav link
- index.php: require AiLogger before AiBot; added GET /admin/ai-logs route

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-05 09:03:21 -05:00

299 lines
13 KiB
PHP

<?php
declare(strict_types=1);
require_once dirname(__DIR__) . '/config/env.php';
require_once dirname(__DIR__) . '/config/db.php';
$db = db();
// ─── Tablas ───────────────────────────────────────────────────────────────────
$db->exec("
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(150) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
");
$db->exec("
CREATE TABLE IF NOT EXISTS webhook_logs (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
event_field VARCHAR(50) NOT NULL DEFAULT 'messages',
from_number VARCHAR(30),
contact_name VARCHAR(150),
message_type VARCHAR(30),
message_preview TEXT,
raw_payload LONGTEXT,
received_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_received_at (received_at),
INDEX idx_from_number (from_number),
INDEX idx_event_field (event_field)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
");
$db->exec("
CREATE TABLE IF NOT EXISTS conversations (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
message_id VARCHAR(100) NOT NULL UNIQUE,
phone_number VARCHAR(30) NOT NULL,
contact_name VARCHAR(150),
direction ENUM('inbound','outbound') DEFAULT 'inbound',
message_type VARCHAR(30),
content TEXT,
media_id VARCHAR(150),
timestamp INT UNSIGNED,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_conv_phone (phone_number),
INDEX idx_conv_created (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
");
$db->exec("
CREATE TABLE IF NOT EXISTS notifications (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
type VARCHAR(50) NOT NULL DEFAULT 'new_message',
reference_id BIGINT,
phone_number VARCHAR(30),
message TEXT,
is_read TINYINT(1) DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_notif_read (is_read),
INDEX idx_notif_created (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
");
$db->exec("
CREATE TABLE IF NOT EXISTS media_queue (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
message_id VARCHAR(100) NOT NULL,
media_id VARCHAR(150) NOT NULL,
media_type VARCHAR(30),
attempts TINYINT DEFAULT 0,
processed TINYINT(1) DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_mq_processed (processed),
INDEX idx_mq_message_id (message_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
");
// ─── Tabla de empresas (multi-tenant) ─────────────────────────────────────────
$db->exec("
CREATE TABLE IF NOT EXISTS companies (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(150) NOT NULL,
display_name VARCHAR(150),
phone_number_id VARCHAR(50) NOT NULL DEFAULT '',
display_phone VARCHAR(30),
api_base_url VARCHAR(500) NOT NULL,
api_key VARCHAR(255),
bot_type ENUM('ai','normal','hybrid') DEFAULT 'normal',
requires_approval TINYINT(1) DEFAULT 0,
is_active TINYINT(1) DEFAULT 1,
config_json TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_phone_number_id (phone_number_id),
INDEX idx_is_active (is_active)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
");
// ─── Cola de mensajes salientes ──────────────────────────────────────────────
$db->exec("
CREATE TABLE IF NOT EXISTS outbound_queue (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
company_id INT NOT NULL,
to_number VARCHAR(30) NOT NULL,
message_type VARCHAR(30) NOT NULL DEFAULT 'text',
payload TEXT NOT NULL,
status ENUM('queued','sent','delivered','failed') DEFAULT 'queued',
attempts TINYINT DEFAULT 0,
max_attempts TINYINT DEFAULT 5,
last_error TEXT,
wam_id VARCHAR(100),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_company_status (company_id, status),
INDEX idx_status (status),
FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
");
// ─── Contexto de conversación del bot ─────────────────────────────────────────
$db->exec("
CREATE TABLE IF NOT EXISTS bot_context (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
company_id INT NOT NULL,
phone_number VARCHAR(30) NOT NULL,
bot_type ENUM('normal','ai') DEFAULT 'normal',
current_node VARCHAR(100),
ai_history LONGTEXT,
metadata TEXT,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY idx_bot_context (company_id, phone_number),
FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
");
// ─── Cola de aprobación — mensajes que requieren revisión humana ──────────────
$db->exec("
CREATE TABLE IF NOT EXISTS pending_approval (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
company_id INT NOT NULL,
phone_number VARCHAR(30) NOT NULL,
contact_name VARCHAR(150),
incoming_msg TEXT NOT NULL,
bot_response TEXT,
bot_type VARCHAR(30),
context_json TEXT,
status ENUM('pending','approved','rejected') DEFAULT 'pending',
reviewed_by VARCHAR(100),
review_note TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
reviewed_at DATETIME,
INDEX idx_company_status (company_id, status),
INDEX idx_status (status),
FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
");
// ─── Agregar company_id a tablas existentes ────────────────────────────────────
try {
$db->exec("ALTER TABLE conversations ADD COLUMN company_id INT DEFAULT NULL AFTER id, ADD INDEX idx_conv_company (company_id)");
} catch (\PDOException $e) {
// Columna ya existe
}
try {
$db->exec("ALTER TABLE webhook_logs ADD COLUMN company_id INT DEFAULT NULL AFTER id, ADD INDEX idx_wh_company (company_id)");
} catch (\PDOException $e) {}
try {
$db->exec("ALTER TABLE notifications ADD COLUMN company_id INT DEFAULT NULL AFTER id, ADD INDEX idx_notif_company (company_id)");
} catch (\PDOException $e) {}
// ─── Tabla de configuración general ────────────────────────────────────────────
$db->exec("
CREATE TABLE IF NOT EXISTS settings (
`key` VARCHAR(100) NOT NULL PRIMARY KEY,
`value` TEXT DEFAULT NULL,
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
");
// Seed defaults si están vacíos
$seedSettings = [
'whatsapp_access_token' => '',
'whatsapp_app_secret' => '',
'whatsapp_verify_token' => '',
'whatsapp_business_account_id' => '',
'whatsapp_default_phone_number_id' => '',
'ai_provider' => 'mock',
'openai_api_key' => '',
'openai_model' => 'gpt-4o-mini',
'gemini_api_key' => '',
'gemini_model' => 'gemini-2.5-flash',
'claude_api_key' => '',
'claude_model' => 'claude-haiku-4-5',
'ai_max_tokens' => '500',
'ai_default_prompt' => 'Eres un asistente virtual de Palmas360. Responde de forma amable y profesional.',
];
$insertStmt = $db->prepare("INSERT IGNORE INTO settings (`key`, `value`) VALUES (?, ?)");
foreach ($seedSettings as $k => $v) {
$insertStmt->execute([$k, $v]);
}
// ─── Números WhatsApp por empresa ─────────────────────────────────────────────
$db->exec("
CREATE TABLE IF NOT EXISTS company_phones (
id INT AUTO_INCREMENT PRIMARY KEY,
company_id INT NOT NULL,
wa_number VARCHAR(25) NOT NULL,
label VARCHAR(100) DEFAULT '',
permission_type TINYINT NOT NULL DEFAULT 1 COMMENT '1=solo reporta, 2=solo recibe, 3=ambos',
is_active TINYINT NOT NULL DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uq_cp (company_id, wa_number),
FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
");
// ─── Endpoints API configurables por empresa ───────────────────────────────────
$db->exec("
CREATE TABLE IF NOT EXISTS company_endpoints (
id INT AUTO_INCREMENT PRIMARY KEY,
company_id INT NOT NULL,
endpoint_key VARCHAR(50) NOT NULL,
direction ENUM('upload','download') NOT NULL,
url VARCHAR(500) DEFAULT '',
method VARCHAR(10) DEFAULT 'GET',
last_response TEXT,
last_called_at DATETIME,
is_active TINYINT DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uq_ce (company_id, endpoint_key),
FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
");
$db->exec("
CREATE TABLE IF NOT EXISTS multi_company_sessions (
wa_number VARCHAR(20) NOT NULL PRIMARY KEY,
companies_json TEXT NOT NULL,
selected_id INT DEFAULT NULL,
expires_at DATETIME NOT NULL,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_expires (expires_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
");
// ─── AI usage logs ───────────────────────────────────────────────────────────
$db->exec("
CREATE TABLE IF NOT EXISTS ai_logs (
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
company_id INT DEFAULT NULL,
phone_number VARCHAR(20) DEFAULT NULL,
provider VARCHAR(20) NOT NULL,
model VARCHAR(60) NOT NULL DEFAULT '',
call_type VARCHAR(30) NOT NULL DEFAULT 'chat',
input_preview VARCHAR(500) DEFAULT NULL,
output_preview VARCHAR(500) DEFAULT NULL,
tokens_in INT DEFAULT NULL,
tokens_out INT DEFAULT NULL,
duration_ms INT DEFAULT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_company (company_id),
INDEX idx_provider (provider),
INDEX idx_created (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
");
// ─── Campo erp_active_users en companies ──────────────────────────────────────
try {
$db->exec("ALTER TABLE companies ADD COLUMN erp_active_users INT DEFAULT 0 AFTER is_active");
} catch (\PDOException $e) {}
// ─── Usuario admin por defecto ────────────────────────────────────────────────
$email = 'admin@palmas360.com';
$password = 'Palmas360@Admin26';
$hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);
$stmt = $db->prepare("INSERT IGNORE INTO users (name, email, password) VALUES (?, ?, ?)");
$stmt->execute(['Lizandro', $email, $hash]);
echo "\n✅ Tablas creadas correctamente.\n";
echo "👤 Usuario: {$email}\n";
echo "🔑 Contraseña: {$password}\n";
echo "⚠️ Cambia la contraseña después del primer login.\n\n";