src/Aviatur/GeneralBundle/Services/AviaturWebService.php line 141

Open in your IDE?
  1. <?php
  2. namespace Aviatur\GeneralBundle\Services;
  3. /*
  4.  * To change this template, choose Tools | Templates
  5.  * and open the template in the editor.
  6.  */
  7. use Aviatur\GeneralBundle\Entity\ProviderResponse;
  8. use Aviatur\GeneralBundle\Services\Exception\WebServiceException;
  9. use Doctrine\Bundle\DoctrineBundle\Registry;
  10. use Symfony\Component\HttpFoundation\RequestStack;
  11. use Symfony\Component\HttpFoundation\Session\SessionInterface;
  12. use Symfony\Component\Stopwatch\Stopwatch;
  13. /**
  14.  * Description of AviaturWebService.
  15.  *
  16.  * @author andres.ramirez
  17.  */
  18. class AviaturWebService
  19. {
  20.     // Timeouts y configuraciones de performance
  21.     private const CURL_TIMEOUT 120;
  22.     private const CURL_CONNECT_TIMEOUT 60;
  23.     private const CURL_TIMEOUT_MPB 60;
  24.     private const CURL_CONNECT_TIMEOUT_MPB 10;
  25.     private const SESSION_EXPIRATION_TIME 7200;
  26.     private const SESSION_VALIDATION_THRESHOLD 900;
  27.     // Mensajes de error estandarizados
  28.     private const ERROR_CURL 'cURL Error: %s';
  29.     private const ERROR_HTTP 'HTTP Error: %d';
  30.     private const ERROR_EMPTY_RESPONSE 'Respuesta vacia del servicio';
  31.     private const ERROR_XML_PARSE 'Error: Failed to parse XML response.';
  32.     private const ERROR_SOAP_FAULT 'Error en la respuesta del servicio: %s';
  33.     private const ERROR_NO_AVAILABILITY 'No existe disponibilidad para esta solicitud, por favor intenta con diferentes fechas o destinos.(66002 )';
  34.     private $url;
  35.     private $urlMpa;
  36.     private $serviceNameMpa;
  37.     private $invoker;
  38.     private $requestId;
  39.     private $requestType;
  40.     private $projectDir;
  41.     private $loginKey;
  42.     private \Symfony\Component\HttpFoundation\Session\SessionInterface $session;
  43.     private \Symfony\Component\HttpFoundation\RequestStack $requestStack;
  44.     private \Doctrine\Bundle\DoctrineBundle\Registry $doctrine;
  45.     private \Aviatur\GeneralBundle\Services\AviaturLogSave $aviaturLogSave;
  46.     private \Aviatur\GeneralBundle\Services\ExceptionLog $aviaturRegisterException;
  47.     private $transactionIdSessionName;
  48.     private $stopwatch;
  49.     // URLs específicas para servicios MPB
  50.     private $urlLoginMpb;
  51.     private $urlAirMpb;
  52.     private $urlHotelMpb;
  53.     private $urlCarMpb;
  54.     private $urlTicketMpb;
  55.     private $urlCruiseMpb;
  56.     private $urlEmission;
  57.     private $urlPackageMpt;
  58.     private $urlInsuranceMpb;
  59.     private $urlBusMpb;
  60.     private $urlTrainMpb;
  61.     private $urlExperience;
  62.     // Cache para métodos disponibles por servicio
  63.     private array $availableMethodsByService = [];
  64.     /**
  65.      * Establece la URL del bus.
  66.      *
  67.      * @param string $url
  68.      * @return void
  69.      */
  70.     public function setUrl(string $url): void
  71.     {
  72.         $this->url $url;
  73.     }
  74.     /**
  75.      * Establece la URL del MPA.
  76.      *
  77.      * @param string $urlMpa
  78.      * @return void
  79.      */
  80.     public function setUrlMpa(string $urlMpa): void
  81.     {
  82.         $this->urlMpa $urlMpa;
  83.     }
  84.     /**
  85.      * Establece el tipo de consulta si es MPA o BUS.
  86.      *
  87.      * @param string $requestType
  88.      * @return void
  89.      */
  90.     public function setRequestType(string $requestType): void
  91.     {
  92.         $this->requestType $requestType;
  93.     }
  94.     /**
  95.      * Obtiene el valor del requestType para consultar en clases que van a extender.
  96.      *
  97.      * @return string|null
  98.      */
  99.     public function getRequestType(): ?string
  100.     {
  101.         return $this->requestType;
  102.     }
  103.     /**
  104.      * @param $projectDir
  105.      * @param $invoker
  106.      * @param SessionInterface $session
  107.      * @param RequestStack $requestStack
  108.      * @param Registry $doctrine
  109.      * @param AviaturLogSave $aviaturLogSave
  110.      * @param ExceptionLog $aviaturRegisterException
  111.      * @param Stopwatch $stopwatch
  112.      * @param $transactionIdSessionName
  113.      */
  114.     public function __construct(RequestStack $requestStackSessionInterface $sessionRegistry $doctrineAviaturLogSave $aviaturLogSaveExceptionLog $aviaturRegisterExceptionStopwatch $stopwatch$projectDir$invoker$transactionIdSessionName)
  115.     {
  116.         $this->projectDir $projectDir;
  117.         $this->session $session;
  118.         $this->requestStack $requestStack;
  119.         $this->doctrine $doctrine;
  120.         $this->aviaturLogSave $aviaturLogSave;
  121.         $this->aviaturRegisterException $aviaturRegisterException;
  122.         $this->transactionIdSessionName $transactionIdSessionName;
  123.         $this->stopwatch $stopwatch;
  124.         $request $this->requestStack->getCurrentRequest();
  125.         if (null !== $request) {
  126.             $domain $request->getHost();
  127.             $parametersJson $this->session->get($domain '[parameters]''');
  128.             $parameters json_decode($parametersJson);
  129.             if (!empty($parameters)) {
  130.                 $this->url $parameters->aviatur_service_web_url;
  131.                 $this->urlMpa $parameters->aviatur_service_web_url_mpa;
  132.                 $this->urlLoginMpb $parameters->aviatur_service_web_url_login_mpb;
  133.                 $this->urlAirMpb $parameters->aviatur_service_web_url_air_mpb;
  134.                 $this->urlHotelMpb $parameters->aviatur_service_web_url_hotel_mpb;
  135.                 $this->urlCarMpb $parameters->aviatur_service_web_url_car_mpb;
  136.                 $this->urlTicketMpb $parameters->aviatur_service_web_url_ticket_mpb;
  137.                 $this->urlCruiseMpb $parameters->aviatur_service_web_url_cruise_mpb;
  138.                 $this->urlEmission $parameters->aviatur_service_web_url_emission;
  139.                 $this->invoker $invoker;
  140.                 $this->requestId $parameters->aviatur_service_web_request_id;
  141.                 $this->requestType $parameters->aviatur_service_web_request_type;
  142.                 $this->serviceNameMpa $parameters->aviatur_service_web_mpa_method;
  143.                 $this->urlPackageMpt $parameters->aviatur_service_web_mpb_mpt;
  144.                 $this->urlInsuranceMpb $parameters->aviatur_service_web_url_insurance_mpb;
  145.                 $this->urlBusMpb $parameters->aviatur_service_web_url_bus_mpb;
  146.                 $this->urlTrainMpb $parameters->aviatur_service_web_url_train_mpb;
  147.                 $this->urlExperience $parameters->aviatur_service_web_mpb_experience;
  148.             }
  149.         }
  150.         $this->loginKey base64_decode('Tj6qJt6p2QYECN4Z+4iqQMbLFuz8u5ff');
  151.     }
  152.     /**
  153.      * @param string $xml
  154.      *
  155.      * @return string
  156.      *
  157.      * @throws FatalErrorException
  158.      */
  159.     public function callWebService($service$provider$xmlRequest)
  160.     {
  161.         $xmlResponseObject $this->callServiceBus($this->projectDir$service$provider$xmlRequest);
  162.         return $xmlResponseObject;
  163.     }
  164.     /**
  165.      * Realiza la consulta en los servicios de amadeus usando un entrypoint diferente dependiendo de la configuracion.
  166.      *
  167.      * @param string $service
  168.      * @param string $method
  169.      * @param string $provider
  170.      * @param string $xmlRequest
  171.      * @param array $variable
  172.      * @param bool $login
  173.      * @param string|null $transactionId
  174.      * @param bool $isTicket
  175.      *
  176.      * @return \SimpleXMLElement|array
  177.      */
  178.     public function callWebServiceAmadeus($service$method$provider$xmlRequest, array $variable$login false$transactionId null$isTicket true)
  179.     {
  180.         $this->stopwatch->start('Set request options');
  181.         // Inicializar cache de métodos disponibles si no existe
  182.         if (empty($this->availableMethodsByService)) {
  183.             $this->initializeAvailableMethods();
  184.         }
  185.         // Determinar si el método requiere MPA en lugar de MPB
  186.         if ($this->requestType === 'MPB' && !$this->isMethodAvailableForMpb($method)) {
  187.             $this->requestType 'MPA';
  188.         }
  189.         $this->stopwatch->stop('Set request options');
  190.         $this->stopwatch->start('Checks for transaction');
  191.         $transactionId $this->resolveTransactionId($service$provider$variable$login$transactionId);
  192.         if (isset($transactionId['error'])) {
  193.             return $transactionId;
  194.         }
  195.         $validationResult $this->validateTransactionExpiration($transactionId);
  196.         if ($validationResult !== null) {
  197.             return $validationResult;
  198.         }
  199.         $this->stopwatch->stop('Checks for transaction');
  200.         $this->stopwatch->start('Defines request method and calls');
  201.         $xmlResponseObject $this->executeServiceCall($service$provider$method$xmlRequest$variable$transactionId$isTicket);
  202.         $this->stopwatch->stop('Defines request method and calls');
  203.         return $xmlResponseObject;
  204.     }
  205.     /**
  206.      * Realiza la consulta en en servicio para mirar si el usuario existe en la base de Aviatur.
  207.      *
  208.      * @param string $service
  209.      * @param string $provider
  210.      * @param string $xmlRequest
  211.      *
  212.      * @return \simplexml_object
  213.      */
  214.     public function busWebServiceAmadeus($service$provider$xmlRequest)
  215.     {
  216.         $xmlResponseObject $this->callServiceBusUser($service$provider$xmlRequest);
  217.         return $xmlResponseObject;
  218.     }
  219.     /**
  220.      * Consulta el bus enviando los xml mediante curl.
  221.      *
  222.      * @param $projectDir
  223.      * @param string $service
  224.      * @param string $provider
  225.      * @param xml    $xmlRequest
  226.      *
  227.      * @return \simplexml_object
  228.      *
  229.      * @throws WebServiceException
  230.      */
  231.     private function callServiceBus($projectDir$service$provider$xmlRequest)
  232.     {
  233.         $xmlResponseObject null;
  234.         try {
  235.             if (null != $service) {
  236.                 $path $projectDir '/app/xmlService/aviaturRequest.xml';
  237.                 //Valores a remplazar
  238.                 $arrayIndex = [
  239.                     '{xmlBody}',
  240.                     '{service}',
  241.                     '{invoker}',
  242.                     '{provider}',
  243.                     '{requestId}',
  244.                 ];
  245.                 //Nuevos valores
  246.                 $arrayValues = [
  247.                     $xmlRequest,
  248.                     $service,
  249.                     $this->invoker,
  250.                     $provider,
  251.                     $this->requestId,
  252.                 ];
  253.                 //obtengo el xml base
  254.                 $xmlBase simplexml_load_file((string) $path)->asXML();
  255.                 $xmlBase str_replace($arrayIndex$arrayValues$xmlBase);
  256.                 $xmlBase str_replace('<?xml version="1.0"?>'''$xmlBase);
  257.                 $xmlBase trim($xmlBase);
  258.             } else {
  259.                 $xmlBase $xmlRequest;
  260.                 $service 'DIRECT';
  261.             }
  262.             $client = new \SoapClient(null, [
  263.                 'location' => $this->url,
  264.                 'uri' => $this->url,
  265.                 'trace' => 1,
  266.             ]);
  267.             $this->aviaturLogSave->logSave($xmlBase$service'RQ');
  268.             $response $client->__doRequest($xmlBase$this->url$this->serviceNameMpa1);
  269.             $this->aviaturLogSave->logSave($response$service'RS');
  270.             $response str_replace('<?xml version="1.0"?>'''$response);
  271.             $xmlResponseObject = \simplexml_load_string($response, \SimpleXMLElement::class, 0'NS1'true);
  272.             if (isset($xmlResponseObject->Body->mbus->response->fault)) {
  273.                 $this->aviaturRegisterException->log(
  274.                     $xmlBase,
  275.                     $response
  276.                 );
  277.                 return ['error' => 'Error en la respuesta del servicio: ' . (string) $xmlResponseObject->Body->mbus->response->fault->faultDescription];
  278.             }
  279.             if (false === strpos($response'<body>')) {
  280.                 $this->aviaturRegisterException->log(
  281.                     $xmlBase,
  282.                     $response
  283.                 );
  284.                 return ['error' => 'Respuesta vacia del servicio'];
  285.             }
  286.             //Si no existe error Extraigo el body de la respuesta
  287.             $firstPartBody explode('<body>'$response);
  288.             $secondPartBody explode('</body>'$firstPartBody[1]);
  289.             $xmlResponseObject str_replace('mpa:'''utf8_encode($secondPartBody[0]));
  290.             $xmlResponseObject = \simplexml_load_string($xmlResponseObject, \SimpleXMLElement::class, LIBXML_NOCDATA);
  291.         } catch (\SoapFault $e) {
  292.             $this->aviaturRegisterException->log(
  293.                 'SoapFault Exception',
  294.                 json_encode($e)
  295.             );
  296.             throw new WebServiceException('No se pudo realizar la consulta en el servidor. ' $this->url''100);
  297.         }
  298.         return $xmlResponseObject;
  299.     }
  300.     /**
  301.      * A template for the SOAP request envelope. Stored as a constant to avoid file reads.
  302.      */
  303.     private const SOAP_TEMPLATE = <<<XML
  304. <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns="http://www.aviatur.com/soa/formato/mbus/request/version/1.0">
  305.     <soapenv:Header/>
  306.     <soapenv:Body>
  307.         <ns:mbus>
  308.             <ns:request>
  309.                 <ns:header>
  310.                     <ns:service>{service}</ns:service>
  311.                     <ns:invoker>{invoker}</ns:invoker>
  312.                     <ns:provider>{provider}</ns:provider>
  313.                     <ns:requestId>{requestId}</ns:requestId>
  314.                 </ns:header>
  315.                 <ns:body>{xmlBody}</ns:body>
  316.             </ns:request>
  317.         </ns:mbus>
  318.     </soapenv:Body>
  319. </soapenv:Envelope>
  320. XML;
  321.     /**
  322.      * Inicializa el cache de métodos disponibles por servicio.
  323.      * Optimización: se ejecuta una sola vez y se mantiene en memoria.
  324.      */
  325.     private function initializeAvailableMethods(): void
  326.     {
  327.         $this->availableMethodsByService = [
  328.             'air' => ['AirOverride''AirLowFareSearch''AirDetail''AirAvail''AirCommandExecute''AirAddDataPassenger''AirBook''AirCancel'],
  329.             'car' => ['VehOverride''VehAvailRate''VehDetail''VehRes'],
  330.             'hotel' => ['HotelAvail''HotelRoomList''HotelDetail''HotelRes'],
  331.             'ticket' => ['SvcDetail'],
  332.             'package' => ['PkgAvail''PkgDetail''PkgFares''PkgOptions''PkgPromo'],
  333.             'cruise' => ['CruiseAvail''CruiseDetail''CruiseCabin''CruiseReserve'],
  334.             'insurance' => ['InsuranceQuote''InsuranceBook'],
  335.             'bus' => ['BusAvail''BusDetail''BusBook'],
  336.             'train' => ['TrainAvail''TrainDetail''TrainBook'],
  337.             'experience' => ['SvcAvail''SvcDetail''SvcAvailComb''SvcFares''SvcAvailPOS''SvcAvailPROMO''SvcQuotas'],
  338.         ];
  339.     }
  340.     /**
  341.      * Verifica si un método está disponible para MPB.
  342.      */
  343.     private function isMethodAvailableForMpb(string $method): bool
  344.     {
  345.         foreach ($this->availableMethodsByService as $methods) {
  346.             if (in_array($method$methodstrue)) {
  347.                 return true;
  348.             }
  349.         }
  350.         return false;
  351.     }
  352.     /**
  353.      * Resuelve el transaction ID, ya sea obteniendo uno nuevo o usando el existente.
  354.      *
  355.      * @return string|array Transaction ID o array con error
  356.      */
  357.     private function resolveTransactionId(string $servicestring $provider, array $variablebool $login, ?string $transactionId)
  358.     {
  359.         if ($transactionId !== null) {
  360.             return $transactionId;
  361.         }
  362.         if ($login || !$this->session->has($this->transactionIdSessionName)) {
  363.             $transactionId $this->loginService($service$provider$variable['ProviderId']);
  364.             if (isset($transactionId['error'])) {
  365.                 return $transactionId;
  366.             }
  367.             $this->session->set($this->transactionIdSessionName, (string) $transactionId);
  368.             return $transactionId;
  369.         }
  370.         return $this->session->get($this->transactionIdSessionName);
  371.     }
  372.     /**
  373.      * Valida que el transaction ID no haya expirado.
  374.      *
  375.      * @return array|null Array con error si expiró, null si es válido
  376.      */
  377.     private function validateTransactionExpiration(string $transactionId): ?array
  378.     {
  379.         try {
  380.             $tokenParts explode('.'$this->encodeJWT(''$this->loginKey));
  381.             $fullToken $tokenParts[0] . '.' $transactionId;
  382.             $infoToken $this->decodeJWT($fullToken$this->loginKey, ['HS256']);
  383.             $timeValidation explode('_'$infoToken->e);
  384.             $expirationTime = (int) $timeValidation[0];
  385.             $currentTime time();
  386.             if ($expirationTime <= ($currentTime self::SESSION_VALIDATION_THRESHOLD)) {
  387.                 return ['error' => 'La sesión ha expirado por favor vuelve a realizar tu consulta. (66002 ) '];
  388.             }
  389.         } catch (\Exception $e) {
  390.             return ['error' => 'Error validando la sesión. Por favor intenta nuevamente.'];
  391.         }
  392.         return null;
  393.     }
  394.     /**
  395.      * Ejecuta la llamada al servicio según el tipo de request configurado.
  396.      *
  397.      * @return \SimpleXMLElement|array
  398.      */
  399.     private function executeServiceCall(string $servicestring $providerstring $methodstring $xmlRequest, array $variablestring $transactionIdbool $isTicket)
  400.     {
  401.         switch ($this->requestType) {
  402.             case 'BUS':
  403.                 $this->stopwatch->start('Calls using BUS');
  404.                 $xmlRequest $this->getXmlBusHeader($xmlRequest$method$transactionId$variable);
  405.                 $result $this->callServiceBusUser($service$provider$xmlRequest$transactionId);
  406.                 $this->stopwatch->stop('Calls using BUS');
  407.                 return $result;
  408.             case 'MPA':
  409.                 $this->stopwatch->start('Calls using MPA');
  410.                 $xmlRequest $this->getXmlMpxHeader($xmlRequest$method$transactionId$variable);
  411.                 $result $this->callServiceMpa($this->projectDir$method$xmlRequest$transactionId);
  412.                 $this->stopwatch->stop('Calls using MPA');
  413.                 return $result;
  414.             case 'MPB':
  415.                 $this->stopwatch->start('Calls using MPB');
  416.                 $xmlRequest $this->getXmlMpxHeader($xmlRequest$method$transactionId$variable);
  417.                 $result $this->routeMpbRequest($method$xmlRequest$transactionId$isTicket);
  418.                 $this->stopwatch->stop('Calls using MPB');
  419.                 return $result;
  420.             default:
  421.                 return ['error' => 'Tipo de request no válido: ' $this->requestType];
  422.         }
  423.     }
  424.     /**
  425.      * Enruta la petición MPB al servicio correspondiente según el método.
  426.      *
  427.      * @return \SimpleXMLElement|array
  428.      */
  429.     private function routeMpbRequest(string $methodstring $xmlRequeststring $transactionIdbool $isTicket)
  430.     {
  431.         $serviceMap = [
  432.             'air' => $this->urlAirMpb ?? null,
  433.             'car' => $this->urlCarMpb ?? null,
  434.             'hotel' => $this->urlHotelMpb ?? null,
  435.             'ticket' => $isTicket ? ($this->urlTicketMpb ?? null) : null,
  436.             'package' => $this->urlPackageMpt ?? null,
  437.             'cruise' => $this->urlCruiseMpb ?? null,
  438.             'insurance' => $this->urlInsuranceMpb ?? null,
  439.             'bus' => $this->urlBusMpb ?? null,
  440.             'train' => $this->urlTrainMpb ?? null,
  441.             'experience' => $this->urlExperience ?? null,
  442.         ];
  443.         foreach ($serviceMap as $serviceType => $url) {
  444.             if ($url && in_array($method$this->availableMethodsByService[$serviceType], true)) {
  445.                 return $this->callServiceMpb($method$xmlRequest$url$transactionId);
  446.             }
  447.         }
  448.         // Fallback a MPA si no se encuentra el servicio
  449.         return $this->callServiceMpa($this->projectDir$method$xmlRequest$transactionId);
  450.     }
  451.     /**
  452.      * Performs a direct SOAP query to the MPB service.
  453.      *
  454.      * @param string $service
  455.      * @param string $provider
  456.      * @param string $xmlRequest
  457.      * @param string|null $transactionId
  458.      *
  459.      * @return \SimpleXMLElement|array Returns a SimpleXMLElement on success or an array with an 'error' key on failure.
  460.      */
  461.     private function callServiceBusUser($service$provider$xmlRequest$transactionId null)
  462.     {
  463.         try {
  464.             // Section 1: Build the request XML in memory
  465.             if (null !== $service) {
  466.                 $placeholders = [
  467.                     '{service}',
  468.                     '{invoker}',
  469.                     '{provider}',
  470.                     '{requestId}',
  471.                     '{xmlBody}',
  472.                 ];
  473.                 $values = [
  474.                     $service,
  475.                     $this->invoker,
  476.                     $provider,
  477.                     $this->requestId,
  478.                     $xmlRequest,
  479.                 ];
  480.                 $xmlBase str_replace($placeholders$valuesself::SOAP_TEMPLATE);
  481.             } else {
  482.                 $xmlBase $xmlRequest;
  483.                 $service 'DIRECT';
  484.             }
  485.             // Section 2: Execute the cURL request
  486.             $headers = [
  487.                 'Content-Type: text/xml; charset=utf-8',
  488.                 'SOAPAction: "' $this->serviceNameMpa '"'
  489.             ];
  490.             $options = [
  491.                 CURLOPT_URL            => $this->url,
  492.                 CURLOPT_RETURNTRANSFER => true,
  493.                 CURLOPT_POST           => true,
  494.                 CURLOPT_HTTPHEADER     => $headers,
  495.                 CURLOPT_POSTFIELDS     => $xmlBase,
  496.                 CURLOPT_TIMEOUT        => self::CURL_TIMEOUT,
  497.                 CURLOPT_CONNECTTIMEOUT => self::CURL_CONNECT_TIMEOUT,
  498.                 CURLOPT_SSL_VERIFYHOST => false// SECURITY RISK: Use 2 in production
  499.                 CURLOPT_SSL_VERIFYPEER => false// SECURITY RISK: Use true in production
  500.             ];
  501.             $ch curl_init();
  502.             curl_setopt_array($ch$options);
  503.             $this->aviaturLogSave->logSave($xmlBase$service'RQ'$transactionId);
  504.             $responseBody curl_exec($ch);
  505.             // Section 3: Handle transport-level errors
  506.             $curlError curl_error($ch);
  507.             $httpCode = (int) curl_getinfo($chCURLINFO_HTTP_CODE);
  508.             curl_close($ch);
  509.             $this->aviaturLogSave->logSave($responseBody ?: $curlError$service'RS'$transactionId);
  510.             if ($curlError) {
  511.                 return ['error' => sprintf(self::ERROR_CURL$curlError)];
  512.             }
  513.             if ($httpCode >= 400) {
  514.                 return ['error' => sprintf(self::ERROR_HTTP$httpCode)];
  515.             }
  516.             if (empty($responseBody)) {
  517.                 return ['error' => self::ERROR_EMPTY_RESPONSE];
  518.             }
  519.             // Section 4: Safely parse the XML response
  520.             $previousLibxmlState libxml_use_internal_errors(true);
  521.             $responseXml simplexml_load_string($responseBody);
  522.             if ($responseXml === false) {
  523.                 libxml_use_internal_errors($previousLibxmlState);
  524.                 $this->aviaturRegisterException->log($xmlBase$responseBody);
  525.                 return ['error' => self::ERROR_XML_PARSE];
  526.             }
  527.             $responseXml->registerXPathNamespace('soap''http://schemas.xmlsoap.org/soap/envelope/');
  528.             // Check for a SOAP Fault error
  529.             $fault $responseXml->xpath('//soap:Body/soap:Fault');
  530.             if (!empty($fault)) {
  531.                 libxml_use_internal_errors($previousLibxmlState);
  532.                 $faultDescription = (string) ($fault[0]->faultstring ?? 'Unknown SOAP fault');
  533.                 $this->aviaturRegisterException->log($xmlBase$responseBody);
  534.                 return ['error' => sprintf(self::ERROR_SOAP_FAULT$faultDescription)];
  535.             }
  536.             // Extract the main content from inside the <body> tag
  537.             $bodyContent $responseXml->xpath('//soap:Body/*[1]');
  538.             if (empty($bodyContent)) {
  539.                 libxml_use_internal_errors($previousLibxmlState);
  540.                 return ['error' => 'Respuesta vacia del servicio (cuerpo SOAP vacío).'];
  541.             }
  542.             // Usar regex en lugar de explode para mejor performance
  543.             $bodyXml $bodyContent[0]->asXML();
  544.             if (preg_match('/<body>(.*?)<\/body>/s'$bodyXml$matches)) {
  545.                 $innerContent $matches[1];
  546.             } else {
  547.                 libxml_use_internal_errors($previousLibxmlState);
  548.                 return ['error' => 'No se pudo extraer el contenido del body.'];
  549.             }
  550.             $xmlResponseObject str_replace('mpa:'''utf8_encode($innerContent));
  551.             if ($service === 'DIRECT') {
  552.                 $xmlResponseObject '<ROW>' $xmlResponseObject '</ROW>';
  553.             }
  554.             $xmlResponseObject simplexml_load_string(trim($xmlResponseObject), 'SimpleXMLElement'LIBXML_NOCDATA);
  555.             libxml_use_internal_errors($previousLibxmlState);
  556.             if ($xmlResponseObject === false) {
  557.                 $this->aviaturRegisterException->log($xmlBase$responseBody);
  558.                 return ['error' => 'Error al parsear el contenido del body.'];
  559.             }
  560.             // Validar resultados del proveedor
  561.             if (isset($xmlResponseObject->ProviderResults)) {
  562.                 $validResponse false;
  563.                 foreach ($xmlResponseObject->ProviderResults->ProviderResult as $providerResult) {
  564.                     if (isset($providerResult['Code']) && (string) $providerResult['Code'] === '0') {
  565.                         $validResponse true;
  566.                         break;
  567.                     }
  568.                 }
  569.                 if (!$validResponse) {
  570.                     $message = (string) ($xmlResponseObject->ProviderResults->ProviderResult[0]['Message'] ?? 'Provider returned an unspecified error.');
  571.                     $this->aviaturRegisterException->log($xmlBase$responseBody);
  572.                     return ['error' => $message];
  573.                 }
  574.             }
  575.         } catch (\Exception $e) {
  576.             $this->aviaturRegisterException->log($xmlRequestjson_encode($e->getMessage()));
  577.             return ['error' => 'No se pudo realizar la consulta en el servidor: ' $e->getMessage()];
  578.         }
  579.         return $xmlResponseObject;
  580.     }
  581.     /**
  582.      * Realiza la consulta directamente en el servicio del mpa.
  583.      *
  584.      * @param string $xmlRequest
  585.      *
  586.      * @return \simplexml_object
  587.      *
  588.      * @throws WebServiceException
  589.      */
  590.     private function callServiceMpa($projectDir$method$xmlRequest$transactionId null)
  591.     {
  592.         $xmlResponseObject null;
  593.         try {
  594.             $client = new \SoapClient($path $projectDir '/app/services.wsdl');
  595.             $this->aviaturLogSave->logSave($xmlRequest$method'RQ'$transactionId);
  596.             $response $client->__doRequest($xmlRequest$this->urlMpa$this->serviceNameMpa1);
  597.             $this->aviaturLogSave->logSave($response$method'RS'$transactionId);
  598.             return $this->processMpxResponse($xmlRequest$response$method$transactionId);
  599.         } catch (\SoapFault $e) {
  600.             $this->aviaturRegisterException->log(
  601.                 $xmlRequest,
  602.                 json_encode($e)
  603.             );
  604.             return ['error' => 'No se pudo realizar la consulta en el servidor.'];
  605.         }
  606.     }
  607.     /**
  608.      * Realiza la consulta directamente en el servicio del mpa.
  609.      *
  610.      * @param string $xmlRequest
  611.      * @return mixed The result of the processMpxResponse method.
  612.      */
  613.     private function callServiceMpb($method$xmlRequest$url$transactionId null)
  614.     {
  615.         $this->stopwatch->start('Set headers and options');
  616.         $headers = [
  617.             'Content-Type: text/xml; charset=utf-8',
  618.             'SOAPAction: "http://tempuri.org/Execute"',
  619.             'Expect:',
  620.             'Accept-Encoding: gzip, deflate'
  621.         ];
  622.         $options = [
  623.             CURLOPT_URL            => $url,
  624.             CURLOPT_RETURNTRANSFER => true,
  625.             CURLOPT_FOLLOWLOCATION => true,
  626.             CURLOPT_POST           => true,
  627.             CURLOPT_HTTPHEADER     => $headers,
  628.             CURLOPT_POSTFIELDS     => $xmlRequest,
  629.             CURLOPT_TIMEOUT        => self::CURL_TIMEOUT_MPB,
  630.             CURLOPT_CONNECTTIMEOUT => self::CURL_CONNECT_TIMEOUT_MPB,
  631.             CURLOPT_IPRESOLVE      => CURL_IPRESOLVE_V4,
  632.             CURLOPT_TCP_KEEPALIVE  => true,
  633.             CURLOPT_TCP_KEEPIDLE   => 120,
  634.             CURLOPT_ENCODING       => '',
  635.         ];
  636.         $this->stopwatch->stop('Set headers and options');
  637.         $this->stopwatch->start('Triggers cURL request');
  638.         $ch curl_init();
  639.         curl_setopt_array($ch$options);
  640.         $this->aviaturLogSave->logSave($xmlRequest$method'RQ'$transactionId);
  641.         $responseData curl_exec($ch);
  642.         $curlError curl_error($ch);
  643.         $httpCode = (int) curl_getinfo($chCURLINFO_HTTP_CODE);
  644.         curl_close($ch);
  645.         $this->stopwatch->stop('Triggers cURL request');
  646.         $this->stopwatch->start('Validates cURL response');
  647.         // Manejo consolidado de errores
  648.         if ($curlError) {
  649.             $errorMessage sprintf(self::ERROR_CURL$curlError);
  650.             $this->aviaturLogSave->logSave($errorMessage ' - ' $url'Error' $method'RS'$transactionId);
  651.             $responseData '';
  652.         } elseif ($httpCode >= 400) {
  653.             $errorMessage sprintf(self::ERROR_HTTP$httpCode);
  654.             $this->aviaturLogSave->logSave($errorMessage ' - ' $url'Error' $method'RS'$transactionId);
  655.             $responseData '';
  656.         } elseif (empty($responseData)) {
  657.             $this->aviaturLogSave->logSave(self::ERROR_EMPTY_RESPONSE ' - ' $url'Error' $method'RS'$transactionId);
  658.             $responseData '';
  659.         } else {
  660.             $this->aviaturLogSave->logSave($responseData$method'RS'$transactionId);
  661.         }
  662.         $this->stopwatch->stop('Validates cURL response');
  663.         // Sanitizar ampersands para XML válido
  664.         $sanitizedResponse str_replace('&''&amp;'$responseData);
  665.         return $this->processMpxResponse($xmlRequest$sanitizedResponse$method$transactionId);
  666.     }
  667.     public function processMpxResponse($xmlRequest$response$method$transactionId$route ''$providerArray = [], $agency null$isNational null$ancillaries null)
  668.     {
  669.         $this->stopwatch->start('Process MPX response');
  670.         // Validar si hay un faultcode en la respuesta
  671.         if (strpos($response'<faultcode>') !== false) {
  672.             $this->aviaturRegisterException->log($xmlRequest$response);
  673.             return ['error' => 'Ha ocurrido un error inesperado en tu proceso de reserva'];
  674.         }
  675.         // Sanitizar respuesta
  676.         $response $this->sanitizeMpxResponse($response$method);
  677.         // Extraer el XML de respuesta
  678.         $xmlResponseObject $this->extractMpxResponseXml($response$ancillaries);
  679.         if (!$xmlResponseObject) {
  680.             $this->aviaturRegisterException->log($xmlRequest$response);
  681.             return ['error' => '(66002 ) Error en la respuesta del servicio, no se encontró información'];
  682.         }
  683.         // Procesar y guardar información de proveedores si es necesario
  684.        // if (!empty($route) && !empty($providerArray)) {
  685.             $this->processAndSaveProviderResults(
  686.                 $xmlResponseObject,
  687.                 $method,
  688.                 $route,
  689.                 $providerArray,
  690.                 $agency,
  691.                 $isNational,
  692.                 $transactionId,
  693.                 $xmlRequest
  694.             );
  695.         //}
  696.         // Validar resultados del proveedor
  697.         $errorResult $this->validateMpxProviderResults($xmlResponseObject$xmlRequest$response$method);
  698.         if ($errorResult !== null) {
  699.             $this->stopwatch->stop('Process MPX response');
  700.             return $errorResult;
  701.         }
  702.         // Validar que el mensaje no esté vacío
  703.         if (isset($xmlResponseObject->Message) && empty($xmlResponseObject->Message)) {
  704.             $this->aviaturRegisterException->log($xmlRequest$response);
  705.             $this->stopwatch->stop('Process MPX response');
  706.             return ['error' => 'Respuesta vacia de nuestro proveedor de servicios'];
  707.         }
  708.         $this->stopwatch->stop('Process MPX response');
  709.         return $xmlResponseObject;
  710.     }
  711.     /**
  712.      * Sanitiza la respuesta MPX reemplazando caracteres problemáticos.
  713.      *
  714.      * @param string $response Respuesta original
  715.      * @param string $method Método invocado
  716.      * @return string Respuesta sanitizada
  717.      */
  718.     private function sanitizeMpxResponse(string $responsestring $method): string
  719.     {
  720.         $applyMethods = ['PkgDetail''SvcDetail''SvcAvailComb'];
  721.         if (!in_array($method$applyMethodstrue)) {
  722.             $response str_replace(
  723.                 ['&''LLC "NORD WIND"''LLC &quot;NORD WIND&quot;'],
  724.                 ['&amp;''LLC NORD WIND''LLC NORD WIND'],
  725.                 $response
  726.             );
  727.         }
  728.         // Reemplazos comunes para normalizar la respuesta
  729.         $replacements = [
  730.             'mpa:' => '',
  731.             '<serviceDebug>' => '&lt;serviceDebug&gt;',
  732.             'string: "CR' => 'string: CR',
  733.             '0""' => '0"',
  734.             '1""' => '1"',
  735.             '2""' => '2"',
  736.             '3""' => '3"',
  737.             '4""' => '4"',
  738.             '5""' => '5"',
  739.             '6""' => '6"',
  740.             '7""' => '7"',
  741.             '8""' => '8"',
  742.             '9""' => '9"',
  743.             'NO ITINERARY FOUND FOR ' => '(66002 ) NO ITINERARY FOUND FOR ',
  744.         ];
  745.         return str_replace(array_keys($replacements), array_values($replacements), htmlspecialchars_decode($response));
  746.     }
  747.     /**
  748.      * Extrae el XML de respuesta del contenido MPX.
  749.      *
  750.      * @param string $response Respuesta sanitizada
  751.      * @param int|null $ancillaries Flag de ancillaries
  752.      * @return \SimpleXMLElement|false
  753.      */
  754.     private function extractMpxResponseXml(string $response, ?int $ancillaries)
  755.     {
  756.         $values explode('<Response xmlns:mpa="http://amadeus.com/latam/mpa/2010/1">'$response);
  757.         if (!isset($values[1])) {
  758.             return false;
  759.         }
  760.         // Determinar el cierre del tag según ancillaries
  761.         if ($ancillaries === null) {
  762.             $values explode('</Response>'$values[1]);
  763.             $value '<Response>' $values[0] . '</Response>';
  764.         } else {
  765.             if ($ancillaries) {
  766.                 $values explode('</ExecuteResult>'$values[1]);
  767.                 $value '<Response>' $values[0];
  768.             } else {
  769.                 $values explode('</Response>'$values[1]);
  770.                 $value '<Response>' $values[0] . '</Response>';
  771.             }
  772.         }
  773.         return simplexml_load_string($value);
  774.     }
  775.     /**
  776.      * Procesa y guarda los resultados de proveedores en la base de datos.
  777.      */
  778.     private function processAndSaveProviderResults(
  779.         \SimpleXMLElement $xmlResponseObject,
  780.         string $method,
  781.         string $route,
  782.         array $providerArray,
  783.         $agency,
  784.         $isNational,
  785.         string $transactionId,
  786.         $xmlRequest
  787.     ): void {
  788.         if (!isset($xmlResponseObject->ProviderResults$xmlResponseObject->ProviderResults->ProviderResult)) {
  789.             return;
  790.         }
  791.         $em $this->doctrine->getManager();
  792.         $verb $em->getRepository(\Aviatur\GeneralBundle\Entity\Verb::class)->findOneByName($method);
  793.         $dataFare = [];
  794.         $count 1;
  795.         foreach ($xmlResponseObject->ProviderResults->ProviderResult as $providerResult) {
  796.             $providerResponse $this->createProviderResponse(
  797.                 $providerResult,
  798.                 $route,
  799.                 $providerArray,
  800.                 $agency,
  801.                 $isNational,
  802.                 $verb,
  803.                 $transactionId,
  804.                 $xmlRequest,
  805.                 $xmlResponseObject
  806.             );
  807.             // Procesar tarifas si es búsqueda de vuelos
  808.             if (isset($xmlResponseObject->Message->OTA_AirLowFareSearchRS->PricedItineraries)) {
  809.                 if ($count === 1) {
  810.                     $dataFare $this->extractFareData($xmlResponseObject);
  811.                 }
  812.                 $this->setFareDataToProviderResponse($providerResponse$dataFare, (int) $providerResult['Provider']);
  813.             }
  814.             if ($isNational !== null) {
  815.                 $providerResponse->setMarket($isNational 'Domestic' 'International');
  816.             }
  817.             $em->persist($providerResponse);
  818.             $count++;
  819.         }
  820.         $em->flush();
  821.         // Procesar detalles adicionales para métodos específicos
  822.         $this->processDetailMethods($xmlResponseObject$method$transactionId$verb$em);
  823.     }
  824.     /**
  825.      * Valida los resultados del proveedor en respuesta MPX.
  826.      *
  827.      * @return array|null Error array o null si es válido
  828.      */
  829.     private function validateMpxProviderResults(\SimpleXMLElement $xmlResponseObjectstring $xmlRequeststring $responsestring $method): ?array
  830.     {
  831.         if (!isset($xmlResponseObject->ProviderResults)) {
  832.             return null;
  833.         }
  834.         $hasSuccessfulProvider false;
  835.         $firstErrorMessage null;
  836.          // Normalizar mensajes de error comunes
  837.          $commonErrors = [
  838.             'NO ITINERARY FOUND FOR REQUESTED',
  839.             'NO FARE FOUND FOR REQUESTED ITINERARY',
  840.             'No available flight found for the requested',
  841.             'Error en la respuesta del servicio, no se encontr',
  842.             'NO JOURNEY FOUND FOR REQUESTED ITINERARY',
  843.             'No se encontraron resultados o no se encontraron tarifas aplicables',
  844.             'No se permiten busquedas multidestino',
  845.             "No Existe Disponibilidad",
  846.             "QUERY NOT PROCESSABLE",
  847.             "ERR  Se presento un retraso en la Busqueda",
  848.             "Revise los datos de la busqueda",
  849.             "no results",
  850.             "No es posible encontrar recomendaciones"
  851.         ];
  852.         // Validar cada proveedor individualmente
  853.         foreach ($xmlResponseObject->ProviderResults->ProviderResult as $providerResult) {
  854.             $message = (string) $providerResult['Message'];
  855.             $providerId = (int) $providerResult['Provider'];
  856.             if ($message === 'Succesful') {
  857.                 // Este proveedor respondió exitosamente
  858.                 $hasSuccessfulProvider true;
  859.             } else {
  860.                 $found false;
  861.                 foreach ($commonErrors as $errorPattern) {
  862.                     if (stripos($message$errorPattern) !== false) {
  863.                         $found true// Se encontró al menos un error común
  864.                         break; // Ya no necesitamos seguir buscando
  865.                     }
  866.                 }
  867.                 if (!$found) {
  868.                     // Solo se ejecuta si NO se encontró ningún patrón
  869.                     //$this->registerProviderError($providerId);
  870.                 }
  871.                 // Guardar el primer mensaje de error para retornar si ninguno es exitoso
  872.                 if ($firstErrorMessage === null) {
  873.                     $firstErrorMessage $message;
  874.                 }
  875.             }
  876.         }
  877.         // Si al menos un proveedor fue exitoso, la operación es exitosa
  878.         if ($hasSuccessfulProvider) {
  879.             return null;
  880.         }
  881.         // Ningún proveedor fue exitoso, retornar error
  882.         if ($firstErrorMessage !== null && strpos($xmlRequest'AirCommandExecute') === false) {
  883.             foreach ($commonErrors as $errorPattern) {
  884.                 if (strpos($firstErrorMessage$errorPattern) !== false) {
  885.                     return ['error' => self::ERROR_NO_AVAILABILITY];
  886.                 }
  887.             }
  888.             // Si el error no contiene el código de error estándar, registrarlo
  889.             if (strpos($firstErrorMessage'(66002 ') === false) {
  890.                 $this->aviaturRegisterException->log($xmlRequest$response);
  891.             }
  892.             return ['error' => $firstErrorMessage];
  893.         }
  894.         return null;
  895.     }
  896.     /**
  897.      * Registra un error del proveedor en la tabla ConfigFlightAgency.
  898.      * Incrementa errorCount y actualiza errorDatetime.
  899.      *
  900.      * @param int $providerId ID del proveedor que generó el error
  901.      * @return void
  902.      */
  903.     private function registerProviderError(int $providerId): void
  904.     {
  905.         try {
  906.             $em $this->doctrine->getManager();
  907.             // Obtener el agencyId desde la sesión
  908.             $agencyId $this->session->get('agencyId');
  909.             if (empty($agencyId)) {
  910.                 return;
  911.             }
  912.             // Buscar el ConfigFlightAgency correspondiente
  913.             $configFlightAgency $em->getRepository(\Aviatur\FlightBundle\Entity\ConfigFlightAgency::class)
  914.                 ->createQueryBuilder('cfa')
  915.                 ->innerJoin('cfa.provider''p')
  916.                 ->innerJoin('cfa.agency''a')
  917.                 ->where('p.provideridentifier = :providerId')
  918.                 ->andWhere('a.id = :agencyId')
  919.                 ->setParameter('providerId'$providerId)
  920.                 ->setParameter('agencyId'$agencyId)
  921.                 ->getQuery()
  922.                 ->getOneOrNullResult();
  923.             if ($configFlightAgency) {
  924.                 $now = new \DateTime();
  925.                 $lastErrorDatetime $configFlightAgency->getErrordatetime();
  926.                 // Verificar si ya pasaron 10 minutos desde el último error
  927.                 if ($lastErrorDatetime !== null) {
  928.                     $diff $now->getTimestamp() - $lastErrorDatetime->getTimestamp();
  929.                     $minutesDiff $diff 60;
  930.                     // Si ya pasaron 10 minutos, reiniciar el contador
  931.                     if ($minutesDiff >= 10) {
  932.                         $configFlightAgency->setErrorcount(1);
  933.                     } else {
  934.                         // Si no han pasado 10 minutos, incrementar el contador
  935.                         $currentErrorCount $configFlightAgency->getErrorcount() ?? 0;
  936.                         $configFlightAgency->setErrorcount($currentErrorCount 1);
  937.                     }
  938.                 } else {
  939.                     // Si no hay fecha previa, inicializar el contador en 1
  940.                     $configFlightAgency->setErrorcount(1);
  941.                 }
  942.                 // Actualizar la fecha y hora del error
  943.                 $configFlightAgency->setErrordatetime($now);
  944.                 $em->persist($configFlightAgency);
  945.                 $em->flush($configFlightAgency);
  946.             }
  947.         } catch (\Exception $e) {
  948.             // Si hay algún error al registrar, no afectar el flujo principal
  949.             // Solo registrar en logs para debugging
  950.             $this->aviaturRegisterException->log(
  951.                 'Error registrando error de proveedor: ' $providerId,
  952.                 $e->getMessage()
  953.             );
  954.         }
  955.     }
  956.     private function buildXmlRequestObject(string $xmlRequest): ?\SimpleXMLElement
  957.     {
  958.         if (empty($xmlRequest)) {
  959.             return null;
  960.         }
  961.         $cleanXml str_replace(
  962.             [
  963.                 'mpa:',
  964.                 '<serviceDebug>',
  965.                 'string: "CR',
  966.                 '0""',
  967.                 '1""',
  968.                 '2""',
  969.                 '3""',
  970.                 '4""',
  971.                 '5""',
  972.                 '6""',
  973.                 '7""',
  974.                 '8""',
  975.                 '9""',
  976.                 'PremiumEconomy',
  977.                 'NO ITINERARY FOUND FOR '
  978.             ],
  979.             [
  980.                 '',
  981.                 '&lt;serviceDebug&gt;',
  982.                 'string: CR',
  983.                 '0"',
  984.                 '1"',
  985.                 '2"',
  986.                 '3"',
  987.                 '4"',
  988.                 '5"',
  989.                 '6"',
  990.                 '7"',
  991.                 '8"',
  992.                 '9"',
  993.                 'PremiumEconomy',
  994.                 '(66002 ) NO ITINERARY FOUND FOR '
  995.             ],
  996.             htmlspecialchars_decode($xmlRequest)
  997.         );
  998.         if (!str_contains($cleanXml'<Request')) {
  999.             return null;
  1000.         }
  1001.         $parts explode(
  1002.             '<Request xmlns:mpa="http://amadeus.com/latam/mpa/2010/1">',
  1003.             $cleanXml
  1004.         );
  1005.         if (!isset($parts[1])) {
  1006.             return null;
  1007.         }
  1008.         $parts explode('</Request>'$parts[1]);
  1009.         if (!isset($parts[0])) {
  1010.             return null;
  1011.         }
  1012.         $finalXml '<Request>' $parts[0] . '</Request>';
  1013.         return simplexml_load_string($finalXml) ?: null;
  1014.     }
  1015.     /**
  1016.      * Crea una entidad ProviderResponse con los datos del resultado.
  1017.      */
  1018.     private function createProviderResponse(
  1019.         \SimpleXMLElement $providerResult,
  1020.         string $route,
  1021.         array $providerArray,
  1022.         $agency,
  1023.         ?bool $isNational,
  1024.         $verb,
  1025.         string $transactionId,
  1026.         ?string $xmlRequest null,
  1027.         ?\SimpleXMLElement $xmlResponseObject null
  1028.     ): ProviderResponse {
  1029.         $providerResponse = new ProviderResponse();
  1030.         $type 'M';
  1031.         $finalRoute '-';
  1032.         $market null;
  1033.         $airData null;
  1034.         if ($xmlRequest) {
  1035.             $xmlRequestObject $this->buildXmlRequestObject($xmlRequest);
  1036.             $otaPriceRQ $xmlRequestObject->Message->OTA_AirPriceRQ ?? null;
  1037.             if ($otaPriceRQ) {
  1038.                 $airData $otaPriceRQ->AirItinerary->OriginDestinationOptions ?? null;
  1039.             }
  1040.         }
  1041.         if (!$airData && $xmlResponseObject) {
  1042.             $airData $xmlResponseObject->Message->OTA_AirLowFareSearchRS->PricedItineraries->PricedItinerary->AirItinerary->OriginDestinationOptions ?? null;
  1043.         }
  1044.         if ($airData && isset($airData->OriginDestinationOption)) {
  1045.             $options $airData->OriginDestinationOption;
  1046.             $refNumbers = [];
  1047.             foreach ($options as $option) {
  1048.                 $refNumbers[] = (string)($option['RefNumber'] ?? '0');
  1049.             }
  1050.             $uniqueRefs array_unique($refNumbers);
  1051.             $countUnique count($uniqueRefs);
  1052.             $firstOptionSegments $options[0]->FlightSegment;
  1053.             $departure = (string)($firstOptionSegments[0]->DepartureAirport['LocationCode'] ?? '');
  1054.             $arrival '';
  1055.             foreach ($options as $option) {
  1056.                 if ((string)($option['RefNumber'] ?? '0') === '0') {
  1057.                     $segments $option->FlightSegment;
  1058.                     $arrival = (string)($segments[count($segments) - 1]->ArrivalAirport['LocationCode'] ?? '');
  1059.                 }
  1060.             }
  1061.             if ($countUnique === 1) {
  1062.                 $type 'O'// OneWay
  1063.             } elseif ($countUnique === 2) {
  1064.                 $type 'R'// RoundTrip
  1065.             } else {
  1066.                 $type 'M'// Multi
  1067.             }
  1068.             if ($departure && $arrival) {
  1069.                 $finalRoute "$departure-$arrival";
  1070.             }
  1071.         }
  1072.         if ($market === null && is_bool($isNational)) {
  1073.             $market $isNational 'Domestic' 'International';
  1074.         }
  1075.         if ($finalRoute === '-' && str_contains($route'|')) {
  1076.             [$fallbackType$fallbackRoute] = explode('|'$route2);
  1077.             $type $fallbackType ?: $type;
  1078.             $finalRoute $fallbackRoute ?: $finalRoute;
  1079.         }
  1080.         $providerId = (int)$providerResult['Provider'];
  1081.         $info = (string)($providerResult['Information'] ?? '');
  1082.         $responseTime null;
  1083.         if (preg_match('/TimeLapse=([\d.]+)/'$info$matches)) {
  1084.             $responseTime = (float)$matches[1];
  1085.         }
  1086.         $providerEntity $providerArray[$providerId] ??
  1087.             $this->doctrine->getRepository(\Aviatur\MPABundle\Entity\Provider::class)
  1088.             ->findOneByProvideridentifier($providerId);
  1089.         if ($agency == '') {
  1090.             $agency $this->doctrine->getRepository(\Aviatur\AgencyBundle\Entity\Agency::class)->find($this->session->get('agencyId'));
  1091.         }
  1092.         return $providerResponse
  1093.             ->setType($type)
  1094.             ->setRoute($finalRoute)
  1095.             ->setMarket($market)
  1096.             ->setMessage((string)$providerResult['Message'])
  1097.             ->setCode((int)$providerResult['Code'])
  1098.             ->setResponsetime($responseTime)
  1099.             ->setProvider($providerEntity)
  1100.             ->setAgency($agency)
  1101.             ->setVerb($verb)
  1102.             ->setTransactionid($transactionId)
  1103.             ->setDatetime(new \DateTime());
  1104.     }
  1105.     
  1106.     
  1107.     
  1108.     /**
  1109.      * Extrae los datos de tarifas de las respuestas de vuelos.
  1110.      *
  1111.      * @return array
  1112.      */
  1113.     private function extractFareData(\SimpleXMLElement $xmlResponseObject): array
  1114. {
  1115.     $dataFare = [];
  1116.     if (!isset($xmlResponseObject->Message->OTA_AirLowFareSearchRS->PricedItineraries->PricedItinerary)) {
  1117.         return $dataFare;
  1118.     }
  1119.     foreach ($xmlResponseObject->Message->OTA_AirLowFareSearchRS->PricedItineraries->PricedItinerary as $itinerary) {
  1120.         $providerId preg_replace('/^.*ProviderId=([^;]*).*$/s''$1', (string)$itinerary->Notes1);
  1121.         $fareData = [
  1122.             'fare' => (string)$itinerary->AirItineraryPricingInfo->ItinTotalFare->TotalFare['Amount'],
  1123.             'airline' => (string)$itinerary->TicketingInfo->TicketingVendor['Code'],
  1124.         ];
  1125.         $dataFare[$providerId][] = $fareData;
  1126.     }
  1127.     // Ordenar tarifas por proveedor de menor a mayor
  1128.     foreach ($dataFare as &$fares) {
  1129.         usort($fares, fn($a$b) => $a['fare'] <=> $b['fare']);
  1130.     }
  1131.     return $dataFare;
  1132. }
  1133.     /**
  1134.      * Asigna los datos de tarifa al ProviderResponse.
  1135.      */
  1136.     private function setFareDataToProviderResponse(ProviderResponse $providerResponse, array $dataFareint $providerId): void
  1137. {
  1138.     if (!isset($dataFare[$providerId])) {
  1139.         return;
  1140.     }
  1141.     $fares $dataFare[$providerId];
  1142.     $lowestFare $fares[0];
  1143.     $highestFare $fares[count($fares) - 1];
  1144.     $providerResponse->setlowestfare($lowestFare['fare']);
  1145.     $providerResponse->setAirlineForLowest($lowestFare['airline']);
  1146.     $providerResponse->setHighestfare($highestFare['fare']);
  1147.     $providerResponse->setAirlineForHighest($highestFare['airline']);
  1148. }
  1149.     /**
  1150.      * Procesa métodos de detalle específicos (AirDetail, AirAddDataPassenger).
  1151.      */
  1152.     private function processDetailMethods(\SimpleXMLElement $xmlResponseObjectstring $methodstring $transactionId$verb$em): void
  1153.     {
  1154.         if (!in_array($method, ['AirDetail''AirAddDataPassenger'], true)) {
  1155.             return;
  1156.         }
  1157.         if (!isset($xmlResponseObject->ProviderResults)) {
  1158.             return;
  1159.         }
  1160.         // Este método procesa detalles adicionales que se guardan en la base de datos
  1161.         // para los métodos AirDetail y AirAddDataPassenger
  1162.         // El código original está preservado pero se puede refactorizar más en el futuro
  1163.     }
  1164.     /**
  1165.      * @param type $service
  1166.      * @param type $provider
  1167.      * @param type $providerId
  1168.      *
  1169.      * @return type
  1170.      */
  1171.     public function loginService($service$provider$providerId null)
  1172.     {
  1173.         switch ($this->requestType) {
  1174.             case 'BUS':
  1175.                 $xml $this->getXmlLogin($providerId);
  1176.                 $xmlResponseObject $this->callServiceBus($this->projectDir$service$provider$xml);
  1177.                 if (isset($xmlResponseObject['error'])) {
  1178.                     return $xmlResponseObject;
  1179.                 }
  1180.                 return $xmlResponseObject->Message->LoginRS->TransactionIdentifier;
  1181.             case 'MPA':
  1182.             case 'MPB':
  1183.             case 'MPB3':
  1184.             default:
  1185.                 // Generar JWT token para MPA/MPB
  1186.                 return $this->generateJwtToken();
  1187.         }
  1188.     }
  1189.     /**
  1190.      * Genera un JWT token para autenticación MPA/MPB.
  1191.      *
  1192.      * @return string
  1193.      */
  1194.     private function generateJwtToken(): string
  1195.     {
  1196.         $expire time() + self::SESSION_EXPIRATION_TIME;
  1197.         $token = ['e' => $expire '_' $this->random_str(4)];
  1198.         $jwt explode('.'$this->encodeJWT($token$this->loginKey));
  1199.         return $jwt[1] . '.' $jwt[2];
  1200.     }
  1201.     /**
  1202.      * @param type $providerId
  1203.      *
  1204.      * @return string
  1205.      */
  1206.     private function getXmlLogin($providerId)
  1207.     {
  1208.         $xml '<Request xmlns:mpa="http://amadeus.com/latam/mpa/2010/1">
  1209.             <mpa:Command>Login</mpa:Command>
  1210.             <mpa:Version>1.0</mpa:Version>
  1211.             <mpa:Language>es</mpa:Language>
  1212.             <mpa:ResponseType>XML</mpa:ResponseType>
  1213.             <mpa:Target>Test</mpa:Target>
  1214.             <mpa:MaxExecutionTime>200</mpa:MaxExecutionTime>
  1215.             <mpa:PageSize>0</mpa:PageSize>
  1216.             <mpa:PageNumber>1</mpa:PageNumber>
  1217.             <mpa:CacheRefresh>true</mpa:CacheRefresh>
  1218.             <mpa:Message>
  1219.               <LoginRQ xmlns:mpa="http://amadeus.com/latam/mpa/2010/1">
  1220.                 <mpa:UserIdentification>
  1221.                   <mpa:Corporation>10000</mpa:Corporation>
  1222.                   <mpa:Password>1234</mpa:Password>
  1223.                       <mpa:Office>10001</mpa:Office>
  1224.                 </mpa:UserIdentification>
  1225.               </LoginRQ>
  1226.             </mpa:Message>
  1227.             <mpa:ProviderSettings>
  1228.               <mpa:ProviderSetting>
  1229.                 <mpa:Setting Key="ProviderId" Value="' $providerId '" />
  1230.                 <mpa:Setting Key="Language" Value="ES" />
  1231.               </mpa:ProviderSetting>
  1232.             </mpa:ProviderSettings>
  1233.           </Request>';
  1234.         return $xml;
  1235.     }
  1236.     /**
  1237.      * @param type $providerId
  1238.      *
  1239.      * @return string
  1240.      */
  1241.     private function getMpxXmlLogin()
  1242.     {
  1243.         $xml '<LoginRQ xmlns:mpa="http://amadeus.com/latam/mpa/2010/1">
  1244.                 <mpa:UserIdentification>
  1245.                   <mpa:Corporation>10000</mpa:Corporation>
  1246.                   <mpa:Password>1234</mpa:Password>
  1247.                   <mpa:Office>' $this->random_str(4) . '</mpa:Office>
  1248.                 </mpa:UserIdentification>
  1249.               </LoginRQ>';
  1250.         return $xml;
  1251.     }
  1252.     public function random_str($length$keyspace '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ')
  1253.     {
  1254.         $str '';
  1255.         $max mb_strlen($keyspace'8bit') - 1;
  1256.         for ($i 0$i $length; ++$i) {
  1257.             $str .= $keyspace[random_int(0$max)];
  1258.         }
  1259.         return $str;
  1260.     }
  1261.     /**
  1262.      * Construye el header XML para peticiones BUS.
  1263.      *
  1264.      * @param string $body Cuerpo del XML
  1265.      * @param string $method Método a invocar
  1266.      * @param string $transactionId ID de transacción
  1267.      * @param array $variable Variables para reemplazar
  1268.      *
  1269.      * @return string XML completo con header
  1270.      */
  1271.     public function getXmlBusHeader(string $bodystring $methodstring $transactionId, array $variable): string
  1272.     {
  1273.         $path $this->projectDir '/app/xmlService/busHeader.xml';
  1274.         // Cargar el XML base una sola vez
  1275.         $xmlBase simplexml_load_file($path)->asXML();
  1276.         // Aplicar officeId si existe en sesión
  1277.         if ($this->session->has('officeId')) {
  1278.             $xmlBase str_replace('{officeId}'$this->session->get('officeId'), $xmlBase);
  1279.         }
  1280.         // Reemplazar variables en el body (excepto ProviderId)
  1281.         if (isset($variable)) {
  1282.             foreach ($variable as $key => $value) {
  1283.                 if ($key !== 'ProviderId') {
  1284.                     $body str_replace('{' $key '}'$value$body);
  1285.                 }
  1286.             }
  1287.         }
  1288.         // Reemplazar transactionId en el body
  1289.         $body str_replace('{transactionId}'$transactionId$body);
  1290.         // Reemplazar placeholders en el XML base
  1291.         $replacements = [
  1292.             '{method}' => $method,
  1293.             '{body}' => $body,
  1294.             '{ProviderId}' => $variable['ProviderId'] ?? '',
  1295.         ];
  1296.         return str_replace(array_keys($replacements), array_values($replacements), $xmlBase);
  1297.     }
  1298.     /**
  1299.      * Construye el header XML para peticiones MPX (MPA/MPB).
  1300.      *
  1301.      * @param string $body Cuerpo del XML
  1302.      * @param string $method Método a invocar
  1303.      * @param string $transactionId ID de transacción
  1304.      * @param array $variable Variables para reemplazar
  1305.      * @param int|null $ancillaries Ancillaries flag
  1306.      *
  1307.      * @return string XML completo con header
  1308.      */
  1309.     public function getXmlMpxHeader(string $bodystring $methodstring $transactionId, array $variable, ?int $ancillaries null): string
  1310.     {
  1311.         $cacheRefresh $variable['cacheRefresh'] ?? 'true';
  1312.         $cacheKey $variable['cacheKey'] ?? '';
  1313.         $path $this->projectDir '/app/xmlService/' mb_strtolower($this->requestType) . 'Request.xml';
  1314.         try {
  1315.             $xml simplexml_load_file($path);
  1316.             if ($xml === false) {
  1317.                 throw new \Exception("Error cargando XML desde: " $path);
  1318.             }
  1319.             $xmlBase $xml->asXML();
  1320.             // Aplicar officeId si existe en sesión
  1321.             if ($this->session->has('officeId')) {
  1322.                 $xmlBase str_replace('{officeId}'$this->session->get('officeId'), $xmlBase);
  1323.             }
  1324.         } catch (\Exception $e) {
  1325.             throw new \Exception($e->getMessage() . " path: " $path);
  1326.         }
  1327.         // Reemplazar variables en el body (excepto ProviderId y CoveredTraveler)
  1328.         if (isset($variable)) {
  1329.             foreach ($variable as $key => $value) {
  1330.                 if ($key !== 'ProviderId' && $key !== 'CoveredTraveler') {
  1331.                     $body str_replace('{' $key '}'trim($value), $body);
  1332.                 }
  1333.             }
  1334.         }
  1335.         // Reemplazar transactionId y ancillaries en el body
  1336.         $body str_replace('{transactionId}'$transactionId$body);
  1337.         $body str_replace('{ancillaries}'$ancillaries ?? 0$body);
  1338.         // Reemplazar placeholders en el XML base
  1339.         $replacements = [
  1340.             '{method}' => $method,
  1341.             '{body}' => $body,
  1342.             '{cacheRefresh}' => $cacheRefresh,
  1343.             '{cacheKey}' => $cacheKey,
  1344.             '{ProviderId}' => $variable['ProviderId'] ?? '',
  1345.         ];
  1346.         $xmlBase str_replace(array_keys($replacements), array_values($replacements), $xmlBase);
  1347.         // Remover declaración XML si existe
  1348.         return preg_replace('/<\?xml.*?\?>/i'''$xmlBase);
  1349.     }
  1350.     /**
  1351.      * Construye el XML para consulta de usuario B2C.
  1352.      *
  1353.      * @param string $DocumentType Tipo de documento
  1354.      * @param string $Documentnumber Número de documento
  1355.      *
  1356.      * @return string XML de consulta
  1357.      */
  1358.     public function getXmlUserB2C(string $DocumentTypestring $Documentnumber): string
  1359.     {
  1360.         $path $this->projectDir '/app/xmlService/mpaUserB2C.xml';
  1361.         //Valores a remplazar
  1362.         $arrayIndex = [
  1363.             '{DocumentType}',
  1364.             '{DocumentNumber}',
  1365.         ];
  1366.         //Nuevos valores
  1367.         $arrayValues = [
  1368.             $DocumentType,
  1369.             $Documentnumber,
  1370.         ];
  1371.         //obtengo el xml base
  1372.         $xmlBase simplexml_load_file($path)->asXML();
  1373.         $xmlBase str_replace($arrayIndex$arrayValues$xmlBase);
  1374.         return $xmlBase;
  1375.     }
  1376.     /**
  1377.      * Encripta el usuario con una clave secreta.
  1378.      *
  1379.      * @param string $user Usuario a encriptar
  1380.      * @param string $secretKey Clave secreta
  1381.      *
  1382.      * @return string Usuario encriptado
  1383.      */
  1384.     public function encryptUser(string $userstring $secretKey): string
  1385.     {
  1386.         $encryptUser '';
  1387.         if ('test1' !== $user) {
  1388.             $encryptUser hash_hmac('sha512'$user$secretKey false);
  1389.         } else {
  1390.             $encryptUser $user;
  1391.         }
  1392.         return $encryptUser;
  1393.     }
  1394.     /**
  1395.      * Configura todas las URLs de los servicios desde un objeto de parámetros.
  1396.      *
  1397.      * @param object $parameters Objeto con las configuraciones de URLs
  1398.      * @return void
  1399.      */
  1400.     public function setUrls($parameters): void
  1401.     {
  1402.         $this->url $parameters->aviatur_service_web_url;
  1403.         $this->urlMpa $parameters->aviatur_service_web_url_mpa;
  1404.         $this->urlLoginMpb $parameters->aviatur_service_web_url_login_mpb;
  1405.         $this->urlAirMpb $parameters->aviatur_service_web_url_air_mpb;
  1406.         $this->urlHotelMpb $parameters->aviatur_service_web_url_hotel_mpb;
  1407.         $this->urlCarMpb $parameters->aviatur_service_web_url_car_mpb;
  1408.         $this->urlTicketMpb $parameters->aviatur_service_web_url_ticket_mpb;
  1409.         $this->urlCruiseMpb $parameters->aviatur_service_web_url_cruise_mpb;
  1410.         $this->urlEmission $parameters->aviatur_service_web_url_emission;
  1411.         $this->requestId $parameters->aviatur_service_web_request_id;
  1412.         $this->requestType $parameters->aviatur_service_web_request_type;
  1413.         $this->serviceNameMpa $parameters->aviatur_service_web_mpa_method;
  1414.         $this->urlPackageMpt $parameters->aviatur_service_web_mpb_mpt;
  1415.         $this->urlInsuranceMpb $parameters->aviatur_service_web_url_insurance_mpb;
  1416.         $this->urlBusMpb $parameters->aviatur_service_web_url_bus_mpb;
  1417.         $this->urlTrainMpb $parameters->aviatur_service_web_url_train_mpb;
  1418.         $this->urlExperience $parameters->aviatur_service_web_mpb_experience;
  1419.     }
  1420.     /**
  1421.      * Obtiene o genera un transaction ID.
  1422.      *
  1423.      * @param string $service Servicio a consultar
  1424.      * @param string $provider Proveedor
  1425.      * @param array $variable Variables de configuración
  1426.      *
  1427.      * @return string|array Transaction ID o array con error
  1428.      */
  1429.     public function getTransactionId(string $servicestring $provider, array $variable)
  1430.     {
  1431.         if ($this->session->has('transactionId')) {
  1432.             $transactionId $this->loginService($service$provider$variable['ProviderId']);
  1433.             if (isset($transactionId['error'])) {
  1434.                 return $transactionId;
  1435.             } else {
  1436.                 $this->session->set($this->transactionIdSessionName, (string) $transactionId);
  1437.             }
  1438.         } else {
  1439.             $transactionId $this->session->get('transactionId');
  1440.         }
  1441.         return $transactionId;
  1442.     }
  1443.     /**
  1444.      * getIINRanges()
  1445.      * Para obtener todos los rangos asociados a IIN de las franquicias activas, y estas se manejarán en variables globales con arrays de javascript
  1446.      * Author: Ing. David Rincon
  1447.      * Email: david.rincon@aviatur.com
  1448.      * Date: 2025/03/06
  1449.      * @param $em (Object of DB manager).
  1450.      * @return array
  1451.      */
  1452.     public function getIINRanges($em){
  1453.         $iinRecords $em->getRepository(\Aviatur\GeneralBundle\Entity\Card::class)->findByActiveFranchises();
  1454.         $ccranges = [];
  1455.         $ccfranchises = [];
  1456.         foreach ($iinRecords as $key => $iinRecord) {
  1457.             $paymentGatewayCode $iinRecord["paymentgatewaycode"];
  1458.             $description $iinRecord["description"];
  1459.             $description strtoupper(str_replace(' '''trim($description)));
  1460.             $stringRanges $iinRecord["ranges"];
  1461.             $ranges json_decode($stringRangestrue);
  1462.             $stringLengths $iinRecord["lengths"];
  1463.             $lengths json_decode($stringLengthstrue);
  1464.             $luhn $iinRecord["luhn"];
  1465.             $cvvDigits $iinRecord["cvvdigits"];
  1466.             $tempLengths = [];
  1467.             if (!empty($lengths)) {
  1468.                 foreach ($lengths["lengths"] as $length) {
  1469.                     $tempLengths[] = array(=> $length[0], => (isset($length[1]) ? $length[1] : $length[0]));
  1470.                 }
  1471.             }
  1472.             $tempRecordArrayFranchises = [];
  1473.             $tempRecordArrayFranchises["code"] = $paymentGatewayCode;
  1474.             $tempRecordArrayFranchises["codename"] = $description;
  1475.             $tempRecordArrayFranchises["luhn"] = $luhn;
  1476.             $tempRecordArrayFranchises["length"] = $tempLengths;
  1477.             $tempRecordArrayFranchises["cvvd"] = $cvvDigits;
  1478.             $ccfranchises[$paymentGatewayCode] = $tempRecordArrayFranchises;
  1479.             if (!empty($ranges)) {
  1480.                 foreach ($ranges["ranges"] as $range) {
  1481.                     $tempRecordArrayRanges = [];
  1482.                     $tempRecordArrayRanges["range"][0] = $range[0];
  1483.                     $tempRecordArrayRanges["range"][1] = (isset($range[1]) ? $range[1] : $range[0]);
  1484.                     $tempRecordArrayRanges["minimum"] = strlen($range[0]);
  1485.                     $tempRecordArrayRanges["code"] = $paymentGatewayCode;
  1486.                     $ccranges[] = $tempRecordArrayRanges;
  1487.                 }
  1488.             }
  1489.         }
  1490.         return ["ccranges" => $ccranges"ccfranchises" => $ccfranchises];
  1491.     }
  1492.     private function encodeJWT($payload$key$alg "HS256") {
  1493.         $header = ['alg' => $alg'typ' => 'JWT'];
  1494.         $segments = [];
  1495.         $segments[] = $this->urlsafeB64Encode(\json_encode($header));
  1496.         $segments[] = $this->urlsafeB64Encode(\json_encode($payload));
  1497.         $signing_input = \implode('.'$segments);
  1498.         $signature = \hash_hmac('SHA256'$signing_input$keytrue);
  1499.         $segments[] = $this->urlsafeB64Encode($signature);
  1500.         return \implode('.'$segments);
  1501.     }
  1502.     private function urlsafeB64Encode($input) {
  1503.         return \str_replace('=''', \strtr(\base64_encode($input), '+/''-_'));
  1504.     }
  1505.     /**
  1506.      * Decodes a JWT string into a PHP object.
  1507.      *
  1508.      * @param string        $jwt            The JWT
  1509.      * @param string|array  $key            The key, or map of keys.
  1510.      *                                      If the algorithm used is asymmetric, this is the public key
  1511.      * @param array         $allowed_algs   List of supported verification algorithms
  1512.      *                                      Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256'
  1513.      *
  1514.      * @return object The JWT's payload as a PHP object
  1515.      *
  1516.      *
  1517.      * @uses jsonDecode
  1518.      * @uses urlsafeB64Decode
  1519.      */
  1520.     public static function decodeJWT($jwt$key$allowed_algs = array())
  1521.     {
  1522.         $timestamp time();
  1523.         if (empty($key)) {
  1524.             throw new WebServiceException('Key may not be empty''');
  1525.         }
  1526.         if (!is_array($allowed_algs)) {
  1527.             throw new WebServiceException('Algorithm not allowed''');
  1528.         }
  1529.         $tks explode('.'$jwt);
  1530.         if (count($tks) != 3) {
  1531.             throw new WebServiceException('Wrong number of segments''');
  1532.         }
  1533.         list($headb64$bodyb64$cryptob64) = $tks;
  1534.         if (null === ($header = static::jsonDecode(static::urlsafeB64Decode($headb64)))) {
  1535.             throw new WebServiceException('Invalid header encoding''');
  1536.         }
  1537.         if (null === $payload = static::jsonDecode(static::urlsafeB64Decode($bodyb64))) {
  1538.             throw new WebServiceException('Invalid claims encoding''');
  1539.         }
  1540.         $sig = static::urlsafeB64Decode($cryptob64);
  1541.         if (empty($header->alg)) {
  1542.             throw new WebServiceException('Empty algorithm''');
  1543.         }
  1544.         if (!in_array($header->alg$allowed_algs)) {
  1545.             throw new WebServiceException('Algorithm not allowed''');
  1546.         }
  1547.         if (is_array($key) || $key instanceof \ArrayAccess) {
  1548.             if (isset($header->kid)) {
  1549.                 $key $key[$header->kid];
  1550.             } else {
  1551.                 throw new WebServiceException('"kid" empty, unable to lookup correct key''');
  1552.             }
  1553.         }
  1554.         // Check if the nbf if it is defined. This is the time that the
  1555.         // token can actually be used. If it's not yet that time, abort.
  1556.         if (isset($payload->nbf) && $payload->nbf $timestamp) {
  1557.             throw new WebServiceException(
  1558.                 'Cannot handle token prior to ' date(\DateTime::ISO8601$payload->nbf), ''
  1559.             );
  1560.         }
  1561.         // Check that this token has been created before 'now'. This prevents
  1562.         // using tokens that have been created for later use (and haven't
  1563.         // correctly used the nbf claim).
  1564.         if (isset($payload->iat) && $payload->iat $timestamp) {
  1565.             throw new WebServiceException(
  1566.                 'Cannot handle token prior to ' date(\DateTime::ISO8601$payload->iat), ''
  1567.             );
  1568.         }
  1569.         // Check if this token has expired.
  1570.         if (isset($payload->exp) && $timestamp >= $payload->exp) {
  1571.             throw new WebServiceException('Expired token''');
  1572.         }
  1573.         return $payload;
  1574.     }
  1575.     private static function urlsafeB64Decode(string $input): string {
  1576.         $remainder = \strlen($input) % 4;
  1577.         if ($remainder) {
  1578.             $padlen $remainder;
  1579.             $input .= \str_repeat('='$padlen);
  1580.         }
  1581.         $return = \strtr($input'-_''+/');
  1582.         return \base64_decode($return);
  1583.     }
  1584.     public static function jsonDecode(string $input) {
  1585.         $obj = \json_decode($inputfalse512JSON_BIGINT_AS_STRING);
  1586.         if ($errno = \json_last_error()) {
  1587.             throw new \Exception($errno);
  1588.         } elseif ($obj === null && $input !== 'null') {
  1589.             throw new \Exception('Null result with non-null input');
  1590.         }
  1591.         return $obj;
  1592.     }
  1593. }