diff --git a/assets/css/style.css b/assets/css/style.css index fed82d28..f89014a8 100644 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -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; diff --git a/call_center_pro.php b/call_center_pro.php index 744c492e..78f149db 100644 --- a/call_center_pro.php +++ b/call_center_pro.php @@ -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 "
Acceso denegado.
"; @@ -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'; } -
+
@@ -691,7 +753,7 @@ require_once 'layout_header.php'; Drive detectado: filas Agregados: : distribución desde fila · último procesado Extracción: desde fila - Auto actualización: cada 10 min + Auto actualización: activa Ver vista previa Drive
@@ -2184,6 +2246,61 @@ if (airDroidCopyButton) { }); } +const ccPanelLiveEnabled = ; +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); diff --git a/pedidos_en_transito.php b/pedidos_en_transito.php index 56f47899..3de7bf27 100644 --- a/pedidos_en_transito.php +++ b/pedidos_en_transito.php @@ -298,11 +298,15 @@ include 'layout_header.php'; - +
@@ -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' ); })(); diff --git a/shalom_api.php b/shalom_api.php index df48726a..e37e51d6 100644 --- a/shalom_api.php +++ b/shalom_api.php @@ -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);