Files
bot_palmas/public/index.php
T
2026-06-12 12:18:21 -05:00

328 lines
16 KiB
PHP

<?php
declare(strict_types=1);
require_once __DIR__ . '/../config/env.php';
require_once __DIR__ . '/../config/db.php';
require_once __DIR__ . '/../services/Settings.php';
// Merge DB settings over .env defaults (so env() always returns the latest)
(function () {
try {
$rows = db()->query("SELECT `key`, `value` FROM settings")->fetchAll();
foreach ($rows as $r) {
if ($r['value'] !== null && $r['value'] !== '') {
$_ENV[$r['key']] = $r['value'];
putenv("{$r['key']}={$r['value']}");
}
}
} catch (\Throwable $e) {
// DB not ready yet — fallback to .env only
}
})();
require_once __DIR__ . '/../admin/v1/WpWebhook.php';
require_once __DIR__ . '/../middleware/SessionAuth.php';
require_once __DIR__ . '/../admin/LoginController.php';
require_once __DIR__ . '/../admin/DashboardController.php';
require_once __DIR__ . '/../services/CompanyRepository.php';
require_once __DIR__ . '/../services/CompanyApiClient.php';
require_once __DIR__ . '/../services/WhatsAppSender.php';
require_once __DIR__ . '/../services/OutboundWorker.php';
require_once __DIR__ . '/../services/ErpSync.php';
require_once __DIR__ . '/../services/ConversationContext.php';
require_once __DIR__ . '/../services/NormalBot.php';
require_once __DIR__ . '/../services/AiBot.php';
require_once __DIR__ . '/../services/BotRouter.php';
require_once __DIR__ . '/../services/PendingApproval.php';
require_once __DIR__ . '/../services/ErpMonitor.php';
// ─── Helper de respuesta JSON ────────────────────────────────────────────────
function jsonResponse(int $status, array $body): void
{
http_response_code($status);
header('Content-Type: application/json; charset=utf-8');
echo json_encode($body, JSON_UNESCAPED_UNICODE);
exit;
}
function serveHtml(string $filename): void
{
$file = __DIR__ . '/' . $filename;
if (!file_exists($file)) {
jsonResponse(404, ['error' => 'Página no encontrada']);
}
http_response_code(200);
header('Content-Type: text/html; charset=utf-8');
readfile($file);
exit;
}
// ─── Router ──────────────────────────────────────────────────────────────────
$method = $_SERVER['REQUEST_METHOD'];
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
// Normalizar: quitar trailing slash
$path = rtrim($path, '/') ?: '/';
// ─── Tabla de rutas ──────────────────────────────────────────────────────────
$routes = [
['GET', '/', fn() => (header('Location: /login') ?: exit())],
['GET', '/health', fn() => jsonResponse(200, ['status' => 'ok', 'service' => 'bot-palmas360'])],
// ─── WhatsApp webhook ────────────────────────────────────────────────────
// Seguridad: HMAC-SHA256 (X-Hub-Signature-256) verificado dentro del controlador
['GET', '/admin/v1/wp-webhook', fn() => WpWebhook::verify()],
['POST', '/admin/v1/wp-webhook', fn() => WpWebhook::receive()],
// ─── Login / Logout ──────────────────────────────────────────────────────
['GET', '/login', fn() => LoginController::showForm()],
['POST', '/login', fn() => LoginController::authenticate()],
['GET', '/logout', fn() => LoginController::logout()],
// ─── Dashboard protegido ─────────────────────────────────────────────────
['GET', '/admin/dashboard', fn() => DashboardController::index()],
['GET', '/admin/live', fn() => DashboardController::live()],
['GET', '/admin/webhook/stream', fn() => DashboardController::stream()],
['GET', '/admin/webhook/raw', fn() => DashboardController::getRaw()],
['GET', '/admin/chat', fn() => DashboardController::chat()],
['GET', '/admin/chat/conversations', fn() => DashboardController::chatConversations()],
['GET', '/admin/chat/messages', fn() => DashboardController::chatMessages()],
['POST', '/admin/chat/send', fn() => DashboardController::chatSend()],
// ─── Páginas legales (requeridas por Meta/WhatsApp Business) ────────────
['GET', '/politicas', fn() => serveHtml('politicas.html')],
['GET', '/eliminacion-datos-usuario', fn() => serveHtml('eliminacion-datos-usuario.html')],
['GET', '/condiciones-servicio', fn() => serveHtml('condiciones-servicio.html')],
// ─── API de envío (outbound) — autenticada por API Key de la empresa ────
['POST', '/api/send', fn() => (function () {
$apiKey = $_SERVER['HTTP_X_API_KEY'] ?? '';
if ($apiKey === '') {
jsonResponse(401, ['error' => 'X-API-Key header requerido']);
}
$company = CompanyRepository::findByApiKey($apiKey);
if ($company === null) {
jsonResponse(403, ['error' => 'API Key inválida']);
}
$input = json_decode(file_get_contents('php://input'), true);
if (!is_array($input)) {
jsonResponse(400, ['error' => 'JSON inválido']);
}
$to = trim($input['to'] ?? '');
$type = $input['type'] ?? 'text';
if ($to === '') {
jsonResponse(400, ['error' => 'Campo "to" requerido']);
}
$payload = match ($type) {
'text' => ['text' => $input['text'] ?? ''],
'image' => ['media_id' => $input['media_id'] ?? '', 'url' => $input['url'] ?? '', 'caption' => $input['caption'] ?? ''],
'template' => ['template_name' => $input['template_name'] ?? '', 'components' => $input['components'] ?? []],
'interactive' => ['interactive' => $input['interactive'] ?? []],
default => jsonResponse(400, ['error' => "Tipo no soportado: {$type}"]),
};
$db = db();
$stmt = $db->prepare("INSERT INTO outbound_queue (company_id, to_number, message_type, payload) VALUES (?, ?, ?, ?)");
$stmt->execute([$company['id'], $to, $type, json_encode($payload)]);
$queueId = (int)$db->lastInsertId();
jsonResponse(200, ['status' => 'queued', 'id' => $queueId]);
})()],
// ─── ERP: listar mensajes pendientes de aprobación ──────────────────────
['GET', '/api/pending', fn() => (function () {
$apiKey = $_SERVER['HTTP_X_API_KEY'] ?? '';
if ($apiKey === '') {
jsonResponse(401, ['error' => 'X-API-Key header requerido']);
}
$company = CompanyRepository::findByApiKey($apiKey);
if ($company === null) {
jsonResponse(403, ['error' => 'API Key inválida']);
}
$items = PendingApproval::findByCompany((int)$company['id'], 'pending');
jsonResponse(200, ['pending' => $items, 'total' => count($items)]);
})()],
// ─── ERP: aprobar o rechazar un mensaje pendiente ───────────────────────
['POST', '/api/approval', fn() => (function () {
$apiKey = $_SERVER['HTTP_X_API_KEY'] ?? '';
if ($apiKey === '') {
jsonResponse(401, ['error' => 'X-API-Key header requerido']);
}
$company = CompanyRepository::findByApiKey($apiKey);
if ($company === null) {
jsonResponse(403, ['error' => 'API Key inválida']);
}
$input = json_decode(file_get_contents('php://input'), true);
if (!is_array($input)) {
jsonResponse(400, ['error' => 'JSON inválido']);
}
$pendingId = (int)($input['pending_id'] ?? 0);
$action = $input['action'] ?? ''; // approve | reject
$note = $input['note'] ?? null;
if ($pendingId === 0) {
jsonResponse(400, ['error' => 'pending_id requerido']);
}
if ($action === 'approve') {
$item = PendingApproval::approve($pendingId, 'ERP: ' . ($company['name'] ?? ''), $note);
if ($item === null) {
jsonResponse(404, ['error' => 'Item no encontrado o ya procesado']);
}
jsonResponse(200, ['status' => 'approved', 'pending_id' => $pendingId, 'message' => 'Respuesta aprobada y encolada para envío']);
} elseif ($action === 'reject') {
$item = PendingApproval::reject($pendingId, 'ERP: ' . ($company['name'] ?? ''), $note);
if ($item === null) {
jsonResponse(404, ['error' => 'Item no encontrado o ya procesado']);
}
jsonResponse(200, ['status' => 'rejected', 'pending_id' => $pendingId, 'message' => 'Mensaje rechazado']);
} else {
jsonResponse(400, ['error' => "Acción no soportada: {$action}. Usa 'approve' o 'reject'"]);
}
})()],
// ─── ERP: enviar mensaje directo (outbound legacy) ──────────────────────
['POST', '/api/send-direct', fn() => (function () {
$apiKey = $_SERVER['HTTP_X_API_KEY'] ?? '';
if ($apiKey === '') {
jsonResponse(401, ['error' => 'X-API-Key header requerido']);
}
$company = CompanyRepository::findByApiKey($apiKey);
if ($company === null) {
jsonResponse(403, ['error' => 'API Key inválida']);
}
$input = json_decode(file_get_contents('php://input'), true);
if (!is_array($input)) {
jsonResponse(400, ['error' => 'JSON inválido']);
}
$to = trim($input['to'] ?? '');
$type = $input['type'] ?? 'text';
if ($to === '') {
jsonResponse(400, ['error' => 'Campo "to" requerido']);
}
$payload = match ($type) {
'text' => json_encode(['text' => $input['text'] ?? '']),
'image' => json_encode(['media_id' => $input['media_id'] ?? '', 'url' => $input['url'] ?? '', 'caption' => $input['caption'] ?? '']),
'template' => json_encode(['template_name' => $input['template_name'] ?? '', 'components' => $input['components'] ?? []]),
'interactive' => json_encode(['interactive' => $input['interactive'] ?? []]),
default => jsonResponse(400, ['error' => "Tipo no soportado: {$type}"]),
};
$db = db();
$stmt = $db->prepare("INSERT INTO outbound_queue (company_id, to_number, message_type, payload) VALUES (?, ?, ?, ?)");
$stmt->execute([$company['id'], $to, $type, $payload]);
jsonResponse(200, ['status' => 'queued', 'id' => (int)$db->lastInsertId()]);
})()],
// ─── Sincronizar empresas desde ERP (protegido) ─────────────────────────
['GET', '/admin/sync-companies', fn() => DashboardController::syncCompanies()],
// ─── Listar empresas (protegido) ────────────────────────────────────────
['GET', '/admin/companies', fn() => DashboardController::companies()],
// ─── Editar empresa (formulario) ───────────────────────────────────────
['GET', '/admin/company/edit', fn() => DashboardController::companyEdit()],
// ─── Guardar empresa (crear/actualizar) ─────────────────────────────────
['POST', '/admin/company/save', fn() => (function () {
SessionAuth::require();
$data = $_POST;
$id = CompanyRepository::save($data);
if ($id > 0) {
header('Location: /admin/companies?msg=' . ($data['id'] ?? 0 > 0 ? 'updated' : 'created'));
} else {
header('Location: /admin/companies?msg=error');
}
exit;
})()],
// ─── Eliminar empresa ──────────────────────────────────────────────────
['GET', '/admin/company/delete', fn() => (function () {
SessionAuth::require();
$id = (int)($_GET['id'] ?? 0);
if ($id > 0 && CompanyRepository::delete($id)) {
header('Location: /admin/companies?msg=deleted');
} else {
header('Location: /admin/companies?msg=error');
}
exit;
})()],
// ─── Procesar cola outbound (protegido o cron) ─────────────────────────
['GET', '/admin/process-queue', fn() => DashboardController::processQueue()],
// ─── Admin: listar pendientes de aprobación ─────────────────────────────
['GET', '/admin/pending', fn() => DashboardController::pending()],
// ─── Admin: aprobar pendiente ───────────────────────────────────────────
['POST', '/admin/pending-approve', fn() => (function () {
SessionAuth::require();
$user = SessionAuth::user();
$input = json_decode(file_get_contents('php://input'), true);
$id = (int)($input['id'] ?? 0);
if ($id === 0) jsonResponse(400, ['error' => 'ID requerido']);
$item = PendingApproval::approve($id, $user['name'] ?? 'Admin', $input['note'] ?? null);
if ($item === null) jsonResponse(404, ['error' => 'No encontrado o ya procesado']);
jsonResponse(200, ['status' => 'approved', 'message' => 'Respuesta aprobada y encolada']);
})()],
// ─── Admin: rechazar pendiente ──────────────────────────────────────────
['POST', '/admin/pending-reject', fn() => (function () {
SessionAuth::require();
$user = SessionAuth::user();
$input = json_decode(file_get_contents('php://input'), true);
$id = (int)($input['id'] ?? 0);
if ($id === 0) jsonResponse(400, ['error' => 'ID requerido']);
$item = PendingApproval::reject($id, $user['name'] ?? 'Admin', $input['note'] ?? null);
if ($item === null) jsonResponse(404, ['error' => 'No encontrado o ya procesado']);
jsonResponse(200, ['status' => 'rejected', 'message' => 'Mensaje rechazado']);
})()],
// ─── Admin: configuración general ──────────────────────────────────────
['GET', '/admin/settings', fn() => DashboardController::settings()],
['POST', '/admin/settings/save', fn() => (function () {
SessionAuth::require();
$allowed = [
'whatsapp_access_token', 'whatsapp_app_secret', 'whatsapp_verify_token',
'whatsapp_business_account_id', 'whatsapp_default_phone_number_id',
'ai_provider', 'openai_api_key', 'openai_model', 'ai_max_tokens', 'ai_default_prompt',
];
$pairs = [];
foreach ($allowed as $k) {
if (isset($_POST[$k])) {
$pairs[$k] = trim($_POST[$k]);
}
}
Settings::setMany($pairs);
// Force re-read into $_ENV
foreach ($pairs as $k => $v) {
$_ENV[$k] = $v;
putenv("{$k}={$v}");
}
header('Location: /admin/settings?msg=saved');
exit;
})()],
// ─── Admin: estado de salud de los ERP ──────────────────────────────────
['GET', '/admin/erp-health', fn() => (function () {
SessionAuth::require();
$result = ErpMonitor::summary();
jsonResponse(200, $result);
})()],
];
// ─── Despacho ────────────────────────────────────────────────────────────────
foreach ($routes as [$routeMethod, $routePath, $handler]) {
if ($method === $routeMethod && $path === $routePath) {
$handler();
exit;
}
}
jsonResponse(404, ['error' => 'Ruta no encontrada', 'path' => $path]);