34849-vm/shalom_api.php
2026-07-21 17:29:06 +00:00

859 lines
28 KiB
PHP

<?php
header('Content-Type: application/json');
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
header('Pragma: no-cache');
header('Expires: 0');
// Make sure we ALWAYS return valid JSON from this endpoint, even when
// upstream error bodies include invalid UTF-8 bytes.
$jsonOptions = JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE | JSON_PARTIAL_OUTPUT_ON_ERROR;
function respond_json(int $statusCode, array $payload, int $jsonOptions): void {
http_response_code($statusCode);
$json = json_encode($payload, $jsonOptions);
if ($json === false) {
$json = '{"error":"Error serializando la respuesta"}';
}
echo $json;
exit;
}
function build_public_tracking_url(string $orderNumber, string $orderCode): string {
return 'https://shalom.com.pe/rastrea/' . rawurlencode($orderNumber) . '/' . rawurlencode($orderCode);
}
function parse_shalom_tracking_date(?string $rawText): array {
$rawText = trim((string) $rawText);
if ($rawText === '') {
return [null, null];
}
$cleanText = preg_replace('/^Desde el\s*/iu', '', $rawText);
$cleanText = is_string($cleanText) ? trim($cleanText) : $rawText;
$timezone = new DateTimeZone('America/Lima');
$formats = [
'd/m/y \a \l\a\s H:i',
'd/m/Y \a \l\a\s H:i',
];
foreach ($formats as $format) {
$dt = DateTimeImmutable::createFromFormat($format, $cleanText, $timezone);
if ($dt instanceof DateTimeImmutable) {
return [$dt->format(DATE_ATOM), $cleanText];
}
}
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')) {
return 'Entregado';
}
if (str_contains($upper, 'REPARTO')) {
return 'En reparto';
}
if (str_contains($upper, 'DESTINO')) {
return 'En destino';
}
if (str_contains($upper, 'TRANSITO') || str_contains($upper, 'TRÁNSITO')) {
return 'En tránsito';
}
if (str_contains($upper, 'ORIGEN') || str_contains($upper, 'REGISTRADO')) {
return 'En origen';
}
return $normalized;
}
function infer_status_message_from_payload(array $statusPayload): string {
$statusData = isset($statusPayload['data']) && is_array($statusPayload['data'])
? $statusPayload['data']
: [];
$priority = [
['key' => 'entregado', 'label' => 'Entregado'],
['key' => 'reparto', 'label' => 'En reparto'],
['key' => 'destino', 'label' => 'En destino'],
['key' => 'transito', 'label' => 'En tránsito'],
['key' => 'origen', 'label' => 'En origen'],
['key' => 'registrado', 'label' => 'En origen'],
['key' => 'demora', 'label' => 'En demora'],
];
foreach ($priority as $candidate) {
$key = $candidate['key'];
if (!empty($statusData[$key])) {
return $candidate['label'];
}
}
$fallbackMessage = trim((string) ($statusPayload['message'] ?? $statusPayload['error'] ?? ''));
$normalizedFallback = normalize_shalom_status_label($fallbackMessage);
return $normalizedFallback !== '' ? $normalizedFallback : 'No disponible';
}
// Load env from executor/.env (helps when Apache/PHP doesn't export .env vars)
function load_dotenv_if_needed(array $keys): void {
$missing = array_filter($keys, fn($k) => getenv($k) === false || getenv($k) === '');
if (empty($missing)) {
return;
}
static $loaded = false;
if ($loaded) return;
$envPath = realpath(__DIR__ . '/../.env'); // executor/.env
if ($envPath && is_readable($envPath)) {
$lines = @file($envPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
foreach ($lines as $line) {
$trimmed = trim($line);
if ($trimmed === '' || $trimmed[0] === '#') {
continue;
}
if (!str_contains($trimmed, '=')) continue;
[$k, $v] = array_map('trim', explode('=', $trimmed, 2));
if ($k === '') continue;
// Strip potential surrounding quotes
$v = trim($v, "\"' ");
// Do not override existing process env
if (getenv($k) === false || getenv($k) === '') {
putenv("{$k}={$v}");
}
}
$loaded = true;
}
}
function resolve_binary(array $candidates, string $fallback): string {
foreach ($candidates as $candidate) {
if (is_executable($candidate)) {
return $candidate;
}
}
return $fallback;
}
function generate_uuid_v4(): string {
$bytes = random_bytes(16);
$bytes[6] = chr((ord($bytes[6]) & 0x0f) | 0x40);
$bytes[8] = chr((ord($bytes[8]) & 0x3f) | 0x80);
$hex = bin2hex($bytes);
return sprintf(
'%s-%s-%s-%s-%s',
substr($hex, 0, 8),
substr($hex, 8, 4),
substr($hex, 12, 4),
substr($hex, 16, 4),
substr($hex, 20, 12)
);
}
function build_shalom_web_authorization(): string {
$token = 'web-' . generate_uuid_v4();
$expiresAt = time() + 300;
$payload = $token . '@' . $expiresAt;
$signature = hash_hmac('sha256', $payload, '.Ov3rsku112024l4r43l.');
return 'Bearer ' . $payload . '@' . $signature;
}
function decrypt_shalom_public_payload(array $decoded): array {
if (empty($decoded['encrypted'])) {
return [
'success' => true,
'data' => $decoded,
];
}
$encodedPayload = trim((string) ($decoded['data'] ?? ''));
if ($encodedPayload === '') {
return [
'success' => false,
'error' => 'Shalom devolvió una respuesta cifrada vacía.',
];
}
$key = base64_decode('uQn/bQ94PXBEfId70zjN+VE1hSU7kh9VBXTOUd68Ssc=', true);
if ($key === false || strlen($key) !== 32) {
return [
'success' => false,
'error' => 'No se pudo preparar la clave de descifrado de Shalom.',
];
}
$binaryPayload = base64_decode($encodedPayload, true);
if ($binaryPayload === false || strlen($binaryPayload) <= 16) {
return [
'success' => false,
'error' => 'La respuesta cifrada de Shalom es inválida.',
];
}
$iv = substr($binaryPayload, 0, 16);
$ciphertext = substr($binaryPayload, 16);
$plainText = openssl_decrypt($ciphertext, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv);
if (!is_string($plainText) || $plainText === '') {
return [
'success' => false,
'error' => 'No se pudo descifrar la respuesta del estado público de Shalom.',
];
}
$decodedPlain = json_decode($plainText, true);
if (json_last_error() !== JSON_ERROR_NONE || !is_array($decodedPlain)) {
return [
'success' => false,
'error' => 'Shalom devolvió un estado público imposible de interpretar.',
'details' => $plainText,
];
}
return [
'success' => true,
'data' => $decodedPlain,
];
}
function call_public_shalom_status_api(string $orderNumber): array {
$authorization = build_shalom_web_authorization();
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => 'https://serviceswebapi.shalomcontrol.com/api/v1/web/rastrea/estados',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => [
'ose_id' => $orderNumber,
],
CURLOPT_HTTPHEADER => [
'Accept: application/json',
'Authorization: ' . $authorization,
'Origin: https://shalom.com.pe',
'Referer: https://shalom.com.pe/',
'X-Requested-With: XMLHttpRequest',
],
CURLOPT_USERAGENT => 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36',
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 20,
CURLOPT_FOLLOWLOCATION => true,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);
if ($curlError) {
return [
'success' => false,
'error' => 'Error consultando el estado público de Shalom.',
'details' => $curlError,
];
}
if ($response === false || $response === null || $response === '') {
return [
'success' => false,
'error' => 'Shalom no devolvió respuesta para el estado público.',
'details' => 'HTTP ' . ($httpCode ?: 'desconocido'),
];
}
$decodedTransport = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE || !is_array($decodedTransport)) {
return [
'success' => false,
'error' => 'La respuesta del estado público de Shalom no es JSON válido.',
'details' => $response,
];
}
$decrypted = decrypt_shalom_public_payload($decodedTransport);
if (empty($decrypted['success'])) {
return $decrypted;
}
$payload = $decrypted['data'];
$success = !empty($payload['success']);
$message = is_array($payload) ? trim((string) ($payload['message'] ?? $payload['error'] ?? '')) : '';
if ($httpCode >= 400 || !$success) {
return [
'success' => false,
'error' => $message !== '' ? 'Estado público Shalom: ' . $message : 'Shalom no pudo devolver el estado real del envío.',
'details' => $payload,
];
}
return [
'success' => true,
'data' => $payload,
];
}
function infer_status_detail_text(string $statusMessage): string {
$upper = mb_strtoupper(trim($statusMessage), 'UTF-8');
if ($upper === '') {
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.';
}
if (str_contains($upper, 'REPARTO')) {
return 'El pedido está en reparto final.';
}
if (str_contains($upper, 'DESTINO')) {
return 'El pedido llegó a la ciudad de destino y está listo para el siguiente paso.';
}
if (str_contains($upper, 'TRANSITO') || str_contains($upper, 'TRÁNSITO')) {
return 'Rumbo a su destino.';
}
if (str_contains($upper, 'ORIGEN') || str_contains($upper, 'REGISTRADO')) {
return 'El pedido fue registrado en la agencia de origen.';
}
return 'Estado actualizado en Shalom.';
}
function build_status_api_response(string $orderNumber, string $orderCode, array $statusPayload, ?array $scraped = null): array {
$statusMessage = infer_status_message_from_payload($statusPayload);
$statusData = isset($statusPayload['data']) && is_array($statusPayload['data'])
? $statusPayload['data']
: [];
if ($scraped !== null) {
[$dateIso, $dateText] = parse_shalom_tracking_date($scraped['date_text'] ?? '');
if (!isset($statusData['registrado']) || !is_array($statusData['registrado'])) {
$statusData['registrado'] = [
'fecha' => $dateIso,
'fecha_text' => $dateText,
];
} else {
if ($dateIso !== null && empty($statusData['registrado']['fecha'])) {
$statusData['registrado']['fecha'] = $dateIso;
}
if ($dateText !== null && empty($statusData['registrado']['fecha_text'])) {
$statusData['registrado']['fecha_text'] = $dateText;
}
}
}
$detailText = $scraped['description'] ?? '';
if (trim((string) $detailText) === '') {
$detailText = infer_status_detail_text($statusMessage);
}
return [
'source' => $scraped !== null ? 'shalom_public_status+web' : 'shalom_public_status',
'search' => [
'success' => true,
'data' => [
'origen' => ['nombre' => 'N/A'],
'destino' => ['nombre' => 'N/A', 'direccion' => ''],
'tracking_url' => $scraped['tracking_url'] ?? build_public_tracking_url($orderNumber, $orderCode),
'internal_order_number' => $scraped['internal_order_number'] ?? null,
'external_order_number' => $orderNumber,
'order_code' => $orderCode,
'detail_text' => $detailText,
'requires_login' => !empty($scraped['requires_login']),
],
],
'statuses' => [
'message' => $statusMessage,
'raw_message' => isset($statusPayload['message']) ? trim((string) $statusPayload['message']) : null,
'data' => $statusData,
],
];
}
function run_shalom_web_scraper(string $orderNumber, string $orderCode): array {
if (!function_exists('exec')) {
return [
'success' => false,
'error' => 'La función exec está deshabilitada en el servidor.',
];
}
$scriptPath = __DIR__ . '/includes/shalom_scraper.js';
if (!is_readable($scriptPath)) {
return [
'success' => false,
'error' => 'No se encontró el scraper local de Shalom.',
];
}
$nodeBinary = resolve_binary(['/usr/bin/node', '/usr/local/bin/node'], 'node');
$timeoutBinary = resolve_binary(['/usr/bin/timeout', '/bin/timeout'], 'timeout');
$commandParts = [
escapeshellarg($timeoutBinary),
'50s',
escapeshellarg($nodeBinary),
escapeshellarg($scriptPath),
escapeshellarg($orderNumber),
escapeshellarg($orderCode),
];
$command = implode(' ', $commandParts) . ' 2>&1';
$output = [];
$exitCode = 0;
exec($command, $output, $exitCode);
$rawOutput = trim(implode("\n", $output));
if ($rawOutput === '') {
return [
'success' => false,
'error' => 'El rastreo público de Shalom no devolvió respuesta.',
'details' => 'Proceso sin salida útil.',
];
}
$decoded = json_decode($rawOutput, true);
if (!is_array($decoded)) {
return [
'success' => false,
'error' => 'Respuesta inválida del rastreo público de Shalom.',
'details' => $rawOutput,
];
}
if ($exitCode !== 0 && empty($decoded['success'])) {
if (empty($decoded['details'])) {
$decoded['details'] = $rawOutput;
}
return $decoded;
}
return $decoded;
}
function run_shalom_search_api_lookup(string $orderNumber, string $orderCode): array {
if (!function_exists('exec')) {
return [
'success' => false,
'error' => 'La función exec está deshabilitada en el servidor.',
];
}
$scriptPath = __DIR__ . '/includes/shalom_api_lookup.js';
if (!is_readable($scriptPath)) {
return [
'success' => false,
'error' => 'No se encontró el helper local de búsqueda de Shalom.',
];
}
$nodeBinary = resolve_binary(['/usr/bin/node', '/usr/local/bin/node'], 'node');
$timeoutBinary = resolve_binary(['/usr/bin/timeout', '/bin/timeout'], 'timeout');
$commandParts = [
escapeshellarg($timeoutBinary),
'80s',
escapeshellarg($nodeBinary),
escapeshellarg($scriptPath),
escapeshellarg($orderNumber),
escapeshellarg($orderCode),
];
$command = implode(' ', $commandParts) . ' 2>&1';
$output = [];
$exitCode = 0;
exec($command, $output, $exitCode);
$rawOutput = trim(implode("\n", $output));
if ($rawOutput === '') {
return [
'success' => false,
'error' => 'La búsqueda oficial de Shalom no devolvió respuesta.',
'details' => 'Proceso sin salida útil.',
];
}
$decoded = json_decode($rawOutput, true);
if (!is_array($decoded)) {
return [
'success' => false,
'error' => 'Respuesta inválida del helper oficial de Shalom.',
'details' => $rawOutput,
];
}
if ($exitCode !== 0 && empty($decoded['success'])) {
if (empty($decoded['details'])) {
$decoded['details'] = $rawOutput;
}
return $decoded;
}
return $decoded;
}
function build_scraper_response(string $orderNumber, string $orderCode, array $scraped): array {
[$dateIso, $dateText] = parse_shalom_tracking_date($scraped['date_text'] ?? '');
return [
'source' => 'shalom_web',
'search' => [
'success' => true,
'data' => [
'origen' => ['nombre' => 'N/A'],
'destino' => ['nombre' => 'N/A', 'direccion' => ''],
'tracking_url' => $scraped['tracking_url'] ?? build_public_tracking_url($orderNumber, $orderCode),
'internal_order_number' => $scraped['internal_order_number'] ?? null,
'external_order_number' => $orderNumber,
'order_code' => $orderCode,
'detail_text' => $scraped['description'] ?? '',
'requires_login' => !empty($scraped['requires_login']),
],
],
'statuses' => [
'message' => normalize_shalom_status_label((string) ($scraped['status'] ?? 'No disponible')),
'data' => [
'registrado' => [
'fecha' => $dateIso,
'fecha_text' => $dateText,
],
],
],
];
}
function call_official_shalom_api(string $orderNumber, string $orderCode, ?string $apiKey): array {
if (!$apiKey) {
return [
'success' => false,
'error' => 'Falta SHALOM_API_KEY.',
];
}
$url = 'https://shalom-api.lat/api/track';
$postData = [
'orderNumber' => $orderNumber,
'orderCode' => $orderCode,
];
$jsonData = json_encode($postData, JSON_UNESCAPED_UNICODE);
if ($jsonData === false) {
return [
'success' => false,
'error' => 'Error serializando la solicitud a la API de Shalom.',
];
}
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $jsonData,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Accept: application/json',
"Authorization: Bearer {$apiKey}",
],
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 25,
CURLOPT_FOLLOWLOCATION => true,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);
if ($curlError) {
return [
'success' => false,
'error' => 'Error en la comunicación con la API de Shalom.',
'details' => $curlError,
];
}
if ($response === false || $response === null || $response === '') {
return [
'success' => false,
'error' => 'Shalom no devolvió respuesta por la API.',
'details' => 'HTTP ' . ($httpCode ?: 'desconocido'),
];
}
$decoded = json_decode($response, true);
if ($httpCode >= 400) {
$message = null;
if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
$message = $decoded['message'] ?? $decoded['error'] ?? null;
}
return [
'success' => false,
'error' => $message ? 'Error API Shalom: ' . $message : 'Error API Shalom HTTP ' . $httpCode . '.',
'details' => is_array($decoded) ? $decoded : $response,
];
}
if (json_last_error() !== JSON_ERROR_NONE || !is_array($decoded)) {
return [
'success' => false,
'error' => 'La API de Shalom devolvió una respuesta inválida.',
'details' => $response,
];
}
return [
'success' => true,
'data' => $decoded,
];
}
// Inputs
$orderNumber = trim($_GET['orderNumber'] ?? '');
$orderCode = trim($_GET['orderCode'] ?? '');
$includeDetailsRaw = trim((string) ($_GET['includeDetails'] ?? ''));
$includeDetails = in_array(strtolower($includeDetailsRaw), ['1', 'true', 'yes', 'si', 'sí'], true);
if ($orderNumber === '' || $orderCode === '') {
respond_json(400, ['error' => 'Número de orden y código de orden son requeridos.'], $jsonOptions);
}
$attemptErrors = [];
// 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;
$resolvedOrderNumber = is_scalar($resolvedOrderNumber) ? (string) $resolvedOrderNumber : null;
$trackingUrl = $searchApiAttempt['resolved']['tracking_url'] ?? build_public_tracking_url(
$resolvedOrderNumber !== null ? $resolvedOrderNumber : $orderNumber,
$orderCode
);
$apiResponse = build_status_api_response($orderNumber, $orderCode, $searchApiAttempt['status'], [
'internal_order_number' => $resolvedOrderNumber,
'tracking_url' => $trackingUrl,
'description' => '',
'requires_login' => false,
]);
$apiResponse['source'] = 'shalom_search_api';
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'];
}
if (!empty($searchApiAttempt['details'])) {
$details = is_string($searchApiAttempt['details']) ? $searchApiAttempt['details'] : json_encode($searchApiAttempt['details'], $jsonOptions);
if ($details) {
$attemptErrors[] = 'Detalle búsqueda API: ' . $details;
}
}
// 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'];
}
if (!empty($scraperAttempt['details'])) {
$details = is_string($scraperAttempt['details']) ? $scraperAttempt['details'] : json_encode($scraperAttempt['details'], $jsonOptions);
if ($details) {
$attemptErrors[] = 'Detalle web: ' . $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'];
}
if (!empty($officialAttempt['details'])) {
$details = is_string($officialAttempt['details']) ? $officialAttempt['details'] : json_encode($officialAttempt['details'], $jsonOptions);
if ($details) {
$attemptErrors[] = 'Detalle API: ' . $details;
}
}
respond_json(502, [
'error' => 'No se pudo consultar el estado en Shalom.',
'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);