859 lines
28 KiB
PHP
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);
|