Autosave: 20260721-173027

This commit is contained in:
Flatlogic Bot 2026-07-21 17:29:06 +00:00
parent 730f9cb10b
commit 33ea21ca77
4 changed files with 362 additions and 66 deletions

View File

@ -280,9 +280,7 @@ h1, .h1 {
}
.cc-pedidos-listos-container,
.cc-pedidos-completados-container {
overflow-x: auto !important;
overflow-y: scroll !important;
scrollbar-gutter: stable both-edges;
overflow: auto !important;
}
.excel-container th {
position: sticky;

View File

@ -5,15 +5,15 @@ require_once 'includes/callcenter_test_helpers.php';
require_once 'includes/drive_test_orders.php';
require_once 'includes/contraentrega_cobertura.php';
$pdo = db();
$provinciasPorDepartamentoContraentrega = contraentregaProvinciasPorDepartamento();
$distritosPorProvinciaContraentrega = contraentregaDistritosPorProvincia();
$departamentosContraentrega = array_keys($provinciasPorDepartamentoContraentrega);
$sedesShalom = cc_test_fetch_shalom_sedes(db());
$pageTitle = 'Pedidos Asignados | Bandeja de la asesora';
$pageDescription = 'Bandeja para que cada asesora gestione sus pedidos asignados con la misma interfaz, estados, historial y acciones.';
if (!cc_test_current_user_can_access_module(db())) {
if (!cc_test_current_user_can_access_module($pdo)) {
http_response_code(403);
require_once 'layout_header.php';
echo "<div class='container py-5'><div class='alert alert-danger mb-0'>Acceso denegado.</div></div>";
@ -21,6 +21,43 @@ if (!cc_test_current_user_can_access_module(db())) {
exit();
}
if (isset($_GET['ajax']) && $_GET['ajax'] === 'panel_status') {
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
$currentUserId = (int) ($_SESSION['user_id'] ?? 0);
if ($currentUserId <= 0) {
http_response_code(401);
echo json_encode([
'success' => false,
'message' => 'Sesión no válida.',
]);
exit();
}
try {
$stmtPanel = $pdo->prepare("\n SELECT source_key, user_id, estado, assigned_at, updated_at, eliminado_at\n FROM callcenter_test_tracking\n WHERE user_id = ? AND eliminado_at IS NULL\n ORDER BY source_key\n ");
$stmtPanel->execute([$currentUserId]);
$panelOrders = $stmtPanel->fetchAll(PDO::FETCH_ASSOC);
echo json_encode([
'success' => true,
'signature' => cc_test_panel_orders_signature($panelOrders),
'total' => count($panelOrders),
]);
} catch (Throwable $exception) {
error_log('Panel status check failed: ' . $exception->getMessage());
http_response_code(500);
echo json_encode([
'success' => false,
'message' => 'No se pudo verificar el panel.',
]);
}
exit();
}
$sedesShalom = cc_test_fetch_shalom_sedes($pdo);
function cc_test_parse_datetime(?string $value): ?DateTimeImmutable
{
@ -175,6 +212,28 @@ function cc_test_followup_semaforo(array $order): ?array
return cc_test_account_followup_semaforo($order['numero_cuenta_enviado_at'] ?? null);
}
function cc_test_panel_orders_signature(array $orders): string
{
$payload = [];
foreach ($orders as $order) {
$payload[] = [
'source_key' => (string) ($order['source_key'] ?? ''),
'user_id' => (int) ($order['user_id'] ?? 0),
'estado' => cc_test_normalize_state((string) ($order['estado'] ?? '')),
'assigned_at' => trim((string) ($order['assigned_at'] ?? '')),
'updated_at' => trim((string) ($order['updated_at'] ?? ($order['seguimiento_actualizado'] ?? ''))),
'eliminado_at' => trim((string) ($order['eliminado_at'] ?? '')),
];
}
usort($payload, static function (array $a, array $b): int {
return strcmp($a['source_key'], $b['source_key']);
});
return sha1(json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
}
$view = $_GET['view'] ?? 'pendientes_hoy';
$allowedViews = [
'pendientes_hoy' => 'Bandeja principal',
@ -225,8 +284,9 @@ $stats = [
'cerrados' => 0,
];
$panelSignature = '';
try {
$pdo = db();
cc_test_ensure_tracking_table($pdo);
cc_test_ensure_historial_llamadas_table($pdo);
$assessors = cc_test_fetch_assessors($pdo);
@ -402,6 +462,8 @@ try {
}
unset($order);
$panelSignature = cc_test_panel_orders_signature($orders);
$visibleOrders = array_values(array_filter($orders, static function (array $order) use ($view, $selectedAssessorFilter): bool {
if ($selectedAssessorFilter !== '' && !($order['matches_assessor_filter'] ?? false)) {
return false;
@ -667,7 +729,7 @@ require_once 'layout_header.php';
}
</style>
<main class="container-fluid py-4">
<main class="container-fluid py-4" data-cc-panel-root="1" data-panel-signature="<?php echo htmlspecialchars($panelSignature); ?>" data-panel-poll-url="<?php echo htmlspecialchars(cc_test_build_url('call_center_pro.php', array_merge($callCenterParams, ['ajax' => 'panel_status']))); ?>" data-panel-poll-interval="6000">
<section class="mb-4">
<div class="d-flex flex-column flex-xl-row justify-content-between align-items-xl-center gap-3">
<div>
@ -691,7 +753,7 @@ require_once 'layout_header.php';
<span class="badge rounded-pill text-bg-light border px-3 py-2">Drive detectado: <?php echo (int) $totalRows; ?> filas</span>
<span class="badge rounded-pill text-bg-light border px-3 py-2">Agregados: <?php echo (int) $agregadosCount; ?></span>
<span class="badge rounded-pill text-bg-light border px-3 py-2"><?php if ($usesIncrementalSync && $lastProcessedRow !== null): ?><?php echo htmlspecialchars(mb_strtoupper($storeLabel)); ?>: distribución desde fila <?php echo (int) ($storeConfig['startRow'] ?? $startRow); ?> · último procesado <?php echo (int) $lastProcessedRow; ?><?php else: ?>Extracción: desde fila <?php echo (int) $startRow; ?><?php endif; ?></span>
<span class="badge rounded-pill text-bg-light border px-3 py-2">Auto actualización: cada 10 min</span>
<span class="badge rounded-pill text-bg-light border px-3 py-2">Auto actualización: activa</span>
<a href="test_importar_drive.php?store=<?php echo htmlspecialchars($storeKey); ?>" class="btn btn-outline-primary btn-sm">Ver vista previa Drive</a>
</div>
</div>
@ -2184,6 +2246,61 @@ if (airDroidCopyButton) {
});
}
const ccPanelLiveEnabled = <?php echo $isAdmin ? 'false' : 'true'; ?>;
if (ccPanelLiveEnabled) {
const ccPanelRoot = document.querySelector('[data-cc-panel-root="1"]');
if (ccPanelRoot) {
const ccPanelPollUrl = ccPanelRoot.dataset.panelPollUrl || '';
const ccPanelInterval = parseInt(ccPanelRoot.dataset.panelPollInterval || '6000', 10) || 6000;
let ccPanelSignature = ccPanelRoot.dataset.panelSignature || '';
let ccPanelPolling = false;
const ccPanelCheck = async () => {
if (ccPanelPolling || !ccPanelPollUrl) {
return;
}
ccPanelPolling = true;
try {
const url = new URL(ccPanelPollUrl, window.location.href);
url.searchParams.set('_ts', Date.now().toString());
const response = await fetch(url.toString(), {
cache: 'no-store',
credentials: 'same-origin'
});
if (!response.ok) {
return;
}
const data = await response.json();
if (!data || !data.success) {
return;
}
const nextSignature = String(data.signature || '');
if (nextSignature && nextSignature !== ccPanelSignature) {
ccPanelSignature = nextSignature;
window.location.reload();
}
} catch (error) {
console.warn('Panel update check failed:', error);
} finally {
ccPanelPolling = false;
}
};
window.setTimeout(ccPanelCheck, 2500);
window.setInterval(ccPanelCheck, ccPanelInterval);
document.addEventListener('visibilitychange', function () {
if (!document.hidden) {
ccPanelCheck();
}
});
}
}
window.setInterval(function () {
window.location.reload();
}, 600000);

View File

@ -298,11 +298,15 @@ include 'layout_header.php';
</td>
<?php endif; ?>
<td><span class="badge" style="<?php echo getStatusStyle($pedido['estado']); ?>"><?php echo ($pedido['estado'] == 'Gestion') ? 'GESTIONES ⚙️' : htmlspecialchars($pedido['estado']); ?></span></td>
<td id="live-status-<?php echo $pedido['id']; ?>">
<?php
$estadoShalomCache = trim((string) ($pedido['estado_shalom'] ?? ''));
$estadoShalomActualizadoAt = trim((string) ($pedido['estado_shalom_actualizado_at'] ?? ''));
?>
<td
id="live-status-<?php echo $pedido['id']; ?>"
data-shalom-status="<?php echo htmlspecialchars($estadoShalomCache); ?>"
data-shalom-updated-at="<?php echo htmlspecialchars($estadoShalomActualizadoAt); ?>"
>
<?php if ($estadoShalomCache !== ''): ?>
<div class="d-flex flex-column align-items-start gap-1">
<span class="badge <?php echo htmlspecialchars(getShalomStatusBadgeClass($estadoShalomCache)); ?>"><?php echo htmlspecialchars($estadoShalomCache); ?></span>
@ -368,6 +372,8 @@ document.addEventListener('DOMContentLoaded', function() {
const SHALOM_API_ENABLED = true;
const verificationNotice = document.getElementById('shalom-auto-status');
let shalomVerificationPromise = null;
const SHALOM_AUTO_SKIP_RECENT_MINUTES = 3;
const SHALOM_REQUEST_GAP_MS = 250;
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
@ -382,7 +388,9 @@ document.addEventListener('DOMContentLoaded', function() {
let badgeClass = 'bg-secondary';
let whatsappClass = 'btn-secondary';
if (upper.includes('ENTREGADO') || upper.includes('COMPLETADO') || upper.includes('EN DESTINO') || upper.includes('DESTINO')) {
if (upper.includes('REVISAR') || upper.includes('VERIFICAR')) {
badgeClass = 'bg-warning text-dark';
} else if (upper.includes('ENTREGADO') || upper.includes('COMPLETADO') || upper.includes('EN DESTINO') || upper.includes('DESTINO')) {
badgeClass = 'bg-success';
whatsappClass = 'btn-success';
} else if (upper.includes('EN TRANSITO') || upper.includes('EN TRÁNSITO') || upper.includes('TRANSITO') || upper.includes('TRÁNSITO')) {
@ -394,7 +402,33 @@ document.addEventListener('DOMContentLoaded', function() {
return { badgeClass, whatsappClass };
};
const getStatusTextFromCell = (cell) => {
const badge = cell ? cell.querySelector('.badge') : null;
return badge ? (badge.textContent || '').trim() : '';
};
const isLockedShalomStatus = (statusMessage) => {
const upper = (statusMessage || '').toUpperCase();
return upper.includes('ENTREGADO') || upper.includes('COMPLETADO') || upper.includes('EN DESTINO') || upper.includes('DESTINO');
};
const parseShalomUpdatedAt = (rawValue) => {
if (!rawValue) return null;
const normalized = String(rawValue).trim().replace(' ', 'T');
const parsed = new Date(normalized);
return Number.isNaN(parsed.getTime()) ? null : parsed;
};
const wasShalomRecentlyChecked = (cell) => {
const parsed = parseShalomUpdatedAt(cell?.dataset?.shalomUpdatedAt || '');
if (!parsed) return false;
const ageMs = Date.now() - parsed.getTime();
return ageMs >= 0 && ageMs < (SHALOM_AUTO_SKIP_RECENT_MINUTES * 60 * 1000);
};
const setStatusCell = (cell, badgeClass, text, title = '', updatedAtText = '') => {
cell.dataset.shalomStatus = text || '';
cell.innerHTML = '';
const wrapper = document.createElement('div');
wrapper.className = 'd-flex flex-column align-items-start gap-1';
@ -508,7 +542,7 @@ document.addEventListener('DOMContentLoaded', function() {
};
const persistShalomStatus = async (pedidoId, statusMessage) => {
if (!pedidoId || !statusMessage) return '';
if (!pedidoId || !statusMessage) return null;
try {
const response = await fetch('update_shalom_status.php', {
@ -524,13 +558,16 @@ document.addEventListener('DOMContentLoaded', function() {
const payload = await response.json().catch(() => ({}));
if (!response.ok || !payload.success) {
return '';
return null;
}
return formatDateTimeLabel(payload.checked_at);
return {
checkedAtRaw: payload.checked_at || '',
checkedAtLabel: formatDateTimeLabel(payload.checked_at)
};
} catch (error) {
console.warn('No se pudo guardar el estado Shalom en caché.', error);
return '';
return null;
}
};
@ -543,11 +580,12 @@ document.addEventListener('DOMContentLoaded', function() {
formatDateTimeLabel(data?.statuses?.data?.registrado?.fecha) ||
formatDateTimeLabel(data?.statuses?.data?.registrado?.fecha_text);
setStatusCell(statusCell, badgeClass, statusMessage, '', updatedFromProvider || 'Actualizando caché...');
setStatusCell(statusCell, badgeClass, statusMessage, '', updatedFromProvider || 'Actualizando...');
const cachedUpdatedAt = await persistShalomStatus(pedidoId, statusMessage);
if (cachedUpdatedAt) {
setStatusCell(statusCell, badgeClass, statusMessage, '', cachedUpdatedAt);
const cachedUpdate = await persistShalomStatus(pedidoId, statusMessage);
if (cachedUpdate && cachedUpdate.checkedAtRaw) {
statusCell.dataset.shalomUpdatedAt = cachedUpdate.checkedAtRaw;
setStatusCell(statusCell, badgeClass, statusMessage, '', cachedUpdate.checkedAtLabel || updatedFromProvider);
} else if (updatedFromProvider) {
setStatusCell(statusCell, badgeClass, statusMessage, '', updatedFromProvider);
}
@ -802,6 +840,22 @@ document.addEventListener('DOMContentLoaded', function() {
let processed = 0;
let updated = 0;
let skippedLocked = 0;
let skippedRecent = 0;
const buildVerificationProgressLabel = () => {
let label = `Actualizando Estado Shalom: ${processed}/${rows.length} pedidos revisados`;
if (updated) {
label += ` · ${updated} actualizados`;
}
if (skippedLocked) {
label += ` · ${skippedLocked} ya en destino omitidos`;
}
if (skippedRecent) {
label += ` · ${skippedRecent} recientes reutilizados`;
}
return `${label}.`;
};
for (const row of rows) {
const orderNumber = row.dataset.orderNumber;
@ -818,14 +872,29 @@ document.addEventListener('DOMContentLoaded', function() {
if (orderCode === '26') {
setStatusCell(statusCell, 'bg-info', 'OLVA COURIER');
processed++;
updateVerificationNotice(`Actualizando Estado Shalom: ${processed}/${rows.length} pedidos revisados.`, 'info');
updateVerificationNotice(buildVerificationProgressLabel(), 'info');
continue;
}
if (!orderNumber || !orderCode || orderNumber === 'N/A' || orderCode === 'N/A') {
setStatusCell(statusCell, 'bg-light text-dark', 'Sin datos');
processed++;
updateVerificationNotice(`Actualizando Estado Shalom: ${processed}/${rows.length} pedidos revisados.`, 'info');
updateVerificationNotice(buildVerificationProgressLabel(), 'info');
continue;
}
const currentStatusText = getStatusTextFromCell(statusCell);
if (automatic && isLockedShalomStatus(currentStatusText)) {
skippedLocked++;
processed++;
updateVerificationNotice(buildVerificationProgressLabel(), 'info');
continue;
}
if (automatic && currentStatusText && currentStatusText !== 'VERIFICAR' && wasShalomRecentlyChecked(statusCell)) {
skippedRecent++;
processed++;
updateVerificationNotice(buildVerificationProgressLabel(), 'info');
continue;
}
@ -853,10 +922,10 @@ document.addEventListener('DOMContentLoaded', function() {
}
processed++;
updateVerificationNotice(`Actualizando Estado Shalom: ${processed}/${rows.length} pedidos revisados${updated ? ` · ${updated} actualizados` : ''}.`, 'info');
updateVerificationNotice(buildVerificationProgressLabel(), 'info');
if (processed < rows.length) {
await sleep(700);
await sleep(SHALOM_REQUEST_GAP_MS);
}
}
@ -864,11 +933,13 @@ document.addEventListener('DOMContentLoaded', function() {
hour: '2-digit',
minute: '2-digit'
});
const skippedLockedSummary = skippedLocked ? ` Se omitieron ${skippedLocked} pedido(s) que ya estaban en destino.` : '';
const skippedRecentSummary = skippedRecent ? ` Se reutilizaron ${skippedRecent} pedido(s) consultados hace menos de ${SHALOM_AUTO_SKIP_RECENT_MINUTES} minuto(s).` : '';
updateVerificationNotice(
automatic
? `Estados Shalom actualizados automáticamente a las ${finishedAt}.`
: `Actualización manual de Estados Shalom completada a las ${finishedAt}.`,
? `Estados Shalom actualizados automáticamente a las ${finishedAt}.${skippedLockedSummary}${skippedRecentSummary}`
: `Actualización manual de Estados Shalom completada a las ${finishedAt}.${skippedLockedSummary}${skippedRecentSummary}`,
'success'
);
})();

View File

@ -47,12 +47,120 @@ function parse_shalom_tracking_date(?string $rawText): array {
return [null, $cleanText];
}
function normalize_shalom_lookup_text(string $text): string {
$normalized = trim((string) preg_replace('/\s+/u', ' ', $text));
if ($normalized === '') {
return '';
}
$upper = mb_strtoupper($normalized, 'UTF-8');
return strtr($upper, [
'Á' => 'A',
'É' => 'E',
'Í' => 'I',
'Ó' => 'O',
'Ú' => 'U',
'Ü' => 'U',
]);
}
function is_shalom_missing_order_message(?string $text): bool {
$normalized = normalize_shalom_lookup_text((string) $text);
if ($normalized === '') {
return false;
}
if (
str_contains($normalized, 'NO SE ENCONTRO LA ORDEN')
|| str_contains($normalized, 'NO SE ENCONTRO EL PEDIDO')
|| str_contains($normalized, 'NO SE ENCONTRO LA GUIA')
|| str_contains($normalized, 'NO SE PUDO ENCONTRAR LA GUIA')
|| str_contains($normalized, 'ORDEN NO ENCONTRADA')
|| str_contains($normalized, 'GUIA NO ENCONTRADA')
|| str_contains($normalized, 'NO EXISTE LA ORDEN')
|| str_contains($normalized, 'NO EXISTE LA GUIA')
|| (str_contains($normalized, 'NO DEVOLVIO UN N') && str_contains($normalized, 'INTERNO VALIDO'))
) {
return true;
}
return false;
}
function detect_shalom_missing_order_reason(array $payload): ?string {
$candidates = [];
foreach (['error', 'message'] as $key) {
if (isset($payload[$key]) && is_scalar($payload[$key])) {
$candidate = trim((string) $payload[$key]);
if ($candidate !== '') {
$candidates[] = $candidate;
}
}
}
if (isset($payload['details'])) {
if (is_scalar($payload['details'])) {
$candidate = trim((string) $payload['details']);
if ($candidate !== '') {
$candidates[] = $candidate;
}
} elseif (is_array($payload['details'])) {
foreach (['error', 'message', 'details'] as $key) {
if (isset($payload['details'][$key]) && is_scalar($payload['details'][$key])) {
$candidate = trim((string) $payload['details'][$key]);
if ($candidate !== '') {
$candidates[] = $candidate;
}
}
}
}
}
foreach ($candidates as $candidate) {
if (is_shalom_missing_order_message($candidate)) {
return $candidate;
}
}
return null;
}
function build_invalid_guide_response(string $orderNumber, string $orderCode, string $rawMessage = ''): array {
return [
'source' => 'shalom_invalid_guide',
'search' => [
'success' => false,
'data' => [
'origen' => ['nombre' => 'N/A'],
'destino' => ['nombre' => 'N/A', 'direccion' => ''],
'tracking_url' => build_public_tracking_url($orderNumber, $orderCode),
'internal_order_number' => null,
'external_order_number' => $orderNumber,
'order_code' => $orderCode,
'detail_text' => 'Shalom no encontró la orden. Revisa el N° de orden y el código de orden para corregir la guía.',
'requires_login' => false,
],
],
'statuses' => [
'message' => 'REVISAR GUIA',
'raw_message' => trim($rawMessage),
'data' => [],
],
];
}
function normalize_shalom_status_label(string $statusMessage): string {
$normalized = trim(preg_replace('/\s+/u', ' ', $statusMessage));
if ($normalized === '') {
return '';
}
if (is_shalom_missing_order_message($normalized)) {
return 'REVISAR GUIA';
}
$upper = mb_strtoupper($normalized, 'UTF-8');
if (str_contains($upper, 'ENTREGA EXITOSA') || str_contains($upper, 'ENTREGAD') || str_contains($upper, 'COMPLET')) {
@ -316,6 +424,10 @@ function infer_status_detail_text(string $statusMessage): string {
return 'Estado actualizado en Shalom.';
}
if (str_contains($upper, 'REVISAR GUIA') || str_contains($upper, 'VERIFICAR')) {
return 'Shalom no encontró la orden. Revisa el N° de orden y el código de orden.';
}
if (str_contains($upper, 'ENTREGADO') || str_contains($upper, 'COMPLETADO')) {
return 'El pedido ha sido entregado satisfactoriamente.';
}
@ -641,26 +753,12 @@ if ($orderNumber === '' || $orderCode === '') {
respond_json(400, ['error' => 'Número de orden y código de orden son requeridos.'], $jsonOptions);
}
load_dotenv_if_needed(['SHALOM_API_KEY']);
// API key fallback from DB (so the app can work even if you can't edit .env)
$apiKey = getenv('SHALOM_API_KEY');
if (!$apiKey) {
try {
require_once __DIR__ . '/db/config.php';
$pdo = db();
$stmt = $pdo->prepare('SELECT valor FROM configuracion WHERE clave = ? LIMIT 1');
$stmt->execute(['SHALOM_API_KEY']);
$apiKey = $stmt->fetchColumn();
$apiKey = is_string($apiKey) ? trim($apiKey) : null;
} catch (Throwable $e) {
$apiKey = null;
}
}
$attemptErrors = [];
// Fallback 0: resolver el pedido con la búsqueda oficial de Shalom y luego pedir su estado real.
// Primero resolvemos el N° interno real con la búsqueda oficial de Shalom.
// El endpoint público de estados usa ese identificador interno (ose_id);
// si se le pasa el número externo del pedido puede devolver otra guía y
// marcar "Entregado" por error.
$searchApiAttempt = run_shalom_search_api_lookup($orderNumber, $orderCode);
if (!empty($searchApiAttempt['success']) && !empty($searchApiAttempt['search']) && !empty($searchApiAttempt['status']) && is_array($searchApiAttempt['search']) && is_array($searchApiAttempt['status'])) {
$resolvedOrderNumber = $searchApiAttempt['search']['data']['ose_id'] ?? null;
@ -680,6 +778,11 @@ if (!empty($searchApiAttempt['success']) && !empty($searchApiAttempt['search'])
respond_json(200, $apiResponse, $jsonOptions);
}
$missingGuideReason = detect_shalom_missing_order_reason($searchApiAttempt);
if ($missingGuideReason !== null) {
respond_json(200, build_invalid_guide_response($orderNumber, $orderCode, $missingGuideReason), $jsonOptions);
}
if (!empty($searchApiAttempt['error'])) {
$attemptErrors[] = 'Búsqueda API Shalom: ' . $searchApiAttempt['error'];
}
@ -690,32 +793,16 @@ if (!empty($searchApiAttempt['details'])) {
}
}
$statusAttempt = call_public_shalom_status_api($orderNumber);
if (!empty($statusAttempt['success']) && !empty($statusAttempt['data']) && is_array($statusAttempt['data'])) {
if ($includeDetails) {
$scraperAttempt = run_shalom_web_scraper($orderNumber, $orderCode);
if (!empty($scraperAttempt['success'])) {
respond_json(200, build_status_api_response($orderNumber, $orderCode, $statusAttempt['data'], $scraperAttempt), $jsonOptions);
}
}
respond_json(200, build_status_api_response($orderNumber, $orderCode, $statusAttempt['data']), $jsonOptions);
}
if (!empty($statusAttempt['error'])) {
$attemptErrors[] = 'Estado público: ' . $statusAttempt['error'];
}
if (!empty($statusAttempt['details'])) {
$details = is_string($statusAttempt['details']) ? $statusAttempt['details'] : json_encode($statusAttempt['details'], $jsonOptions);
if ($details) {
$attemptErrors[] = 'Detalle estado público: ' . $details;
}
}
// Fallback 1: rastreo web visual (más pesado, pero útil si el estado público falla).
// Fallback 1: rastreo web visual (más pesado, pero usa el número + código visibles al usuario).
$scraperAttempt = run_shalom_web_scraper($orderNumber, $orderCode);
if (!empty($scraperAttempt['success'])) {
respond_json(200, build_scraper_response($orderNumber, $orderCode, $scraperAttempt), $jsonOptions);
}
$missingGuideReason = detect_shalom_missing_order_reason($scraperAttempt);
if ($missingGuideReason !== null) {
respond_json(200, build_invalid_guide_response($orderNumber, $orderCode, $missingGuideReason), $jsonOptions);
}
if (!empty($scraperAttempt['error'])) {
$attemptErrors[] = 'Rastreo web: ' . $scraperAttempt['error'];
}
@ -726,11 +813,34 @@ if (!empty($scraperAttempt['details'])) {
}
}
load_dotenv_if_needed(['SHALOM_API_KEY']);
// API key fallback from DB (so the app can work even if you can't edit .env)
$apiKey = getenv('SHALOM_API_KEY');
if (!$apiKey) {
try {
require_once __DIR__ . '/db/config.php';
$pdo = db();
$stmt = $pdo->prepare('SELECT valor FROM configuracion WHERE clave = ? LIMIT 1');
$stmt->execute(['SHALOM_API_KEY']);
$apiKey = $stmt->fetchColumn();
$apiKey = is_string($apiKey) ? trim($apiKey) : null;
} catch (Throwable $e) {
$apiKey = null;
}
}
// Fallback 2: API oficial antigua.
$officialAttempt = call_official_shalom_api($orderNumber, $orderCode, $apiKey ?: null);
if (!empty($officialAttempt['success']) && !empty($officialAttempt['data']) && is_array($officialAttempt['data'])) {
respond_json(200, $officialAttempt['data'], $jsonOptions);
}
$missingGuideReason = detect_shalom_missing_order_reason($officialAttempt);
if ($missingGuideReason !== null) {
respond_json(200, build_invalid_guide_response($orderNumber, $orderCode, $missingGuideReason), $jsonOptions);
}
if (!empty($officialAttempt['error'])) {
$attemptErrors[] = 'API oficial: ' . $officialAttempt['error'];
}
@ -743,6 +853,6 @@ if (!empty($officialAttempt['details'])) {
respond_json(502, [
'error' => 'No se pudo consultar el estado en Shalom.',
'details' => !empty($attemptErrors) ? implode(' | ', $attemptErrors) : 'No hubo respuesta útil del estado público, del rastreo web ni de la API.',
'details' => !empty($attemptErrors) ? implode(' | ', $attemptErrors) : 'No hubo respuesta útil de la búsqueda oficial, del rastreo web ni de la API.',
'tracking_url' => build_public_tracking_url($orderNumber, $orderCode),
], $jsonOptions);