- El número WA Business es global (configuración general), no por empresa - OutboundWorker usa fallback a WHATSAPP_DEFAULT_PHONE_NUMBER_ID si la empresa no tiene - CompanyRepository::save() ya no requiere phone_number_id - phone_number_id permite DEFAULT '' en schema Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
263 lines
11 KiB
PHP
263 lines
11 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',
|
|
'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;
|
|
");
|
|
|
|
// ─── 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";
|