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

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.             );
  694.         }
  695.         // Validar resultados del proveedor
  696.         $errorResult $this->validateMpxProviderResults($xmlResponseObject$xmlRequest$response$method);
  697.         if ($errorResult !== null) {
  698.             $this->stopwatch->stop('Process MPX response');
  699.             return $errorResult;
  700.         }
  701.         // Validar que el mensaje no esté vacío
  702.         if (isset($xmlResponseObject->Message) && empty($xmlResponseObject->Message)) {
  703.             $this->aviaturRegisterException->log($xmlRequest$response);
  704.             $this->stopwatch->stop('Process MPX response');
  705.             return ['error' => 'Respuesta vacia de nuestro proveedor de servicios'];
  706.         }
  707.         $this->stopwatch->stop('Process MPX response');
  708.         return $xmlResponseObject;
  709.     }
  710.     /**
  711.      * Sanitiza la respuesta MPX reemplazando caracteres problemáticos.
  712.      *
  713.      * @param string $response Respuesta original
  714.      * @param string $method Método invocado
  715.      * @return string Respuesta sanitizada
  716.      */
  717.     private function sanitizeMpxResponse(string $responsestring $method): string
  718.     {
  719.         $applyMethods = ['PkgDetail''SvcDetail''SvcAvailComb'];
  720.         if (!in_array($method$applyMethodstrue)) {
  721.             $response str_replace(
  722.                 ['&''LLC "NORD WIND"''LLC &quot;NORD WIND&quot;'],
  723.                 ['&amp;''LLC NORD WIND''LLC NORD WIND'],
  724.                 $response
  725.             );
  726.         }
  727.         // Reemplazos comunes para normalizar la respuesta
  728.         $replacements = [
  729.             'mpa:' => '',
  730.             '<serviceDebug>' => '&lt;serviceDebug&gt;',
  731.             'string: "CR' => 'string: CR',
  732.             '0""' => '0"',
  733.             '1""' => '1"',
  734.             '2""' => '2"',
  735.             '3""' => '3"',
  736.             '4""' => '4"',
  737.             '5""' => '5"',
  738.             '6""' => '6"',
  739.             '7""' => '7"',
  740.             '8""' => '8"',
  741.             '9""' => '9"',
  742.             'NO ITINERARY FOUND FOR ' => '(66002 ) NO ITINERARY FOUND FOR ',
  743.         ];
  744.         return str_replace(array_keys($replacements), array_values($replacements), htmlspecialchars_decode($response));
  745.     }
  746.     /**
  747.      * Extrae el XML de respuesta del contenido MPX.
  748.      *
  749.      * @param string $response Respuesta sanitizada
  750.      * @param int|null $ancillaries Flag de ancillaries
  751.      * @return \SimpleXMLElement|false
  752.      */
  753.     private function extractMpxResponseXml(string $response, ?int $ancillaries)
  754.     {
  755.         $values explode('<Response xmlns:mpa="http://amadeus.com/latam/mpa/2010/1">'$response);
  756.         if (!isset($values[1])) {
  757.             return false;
  758.         }
  759.         // Determinar el cierre del tag según ancillaries
  760.         if ($ancillaries === null) {
  761.             $values explode('</Response>'$values[1]);
  762.             $value '<Response>' $values[0] . '</Response>';
  763.         } else {
  764.             if ($ancillaries) {
  765.                 $values explode('</ExecuteResult>'$values[1]);
  766.                 $value '<Response>' $values[0];
  767.             } else {
  768.                 $values explode('</Response>'$values[1]);
  769.                 $value '<Response>' $values[0] . '</Response>';
  770.             }
  771.         }
  772.         return simplexml_load_string($value);
  773.     }
  774.     /**
  775.      * Procesa y guarda los resultados de proveedores en la base de datos.
  776.      */
  777.     private function processAndSaveProviderResults(
  778.         \SimpleXMLElement $xmlResponseObject,
  779.         string $method,
  780.         string $route,
  781.         array $providerArray,
  782.         $agency,
  783.         $isNational,
  784.         string $transactionId
  785.     ): void {
  786.         if (!isset($xmlResponseObject->ProviderResults$xmlResponseObject->ProviderResults->ProviderResult)) {
  787.             return;
  788.         }
  789.         $em $this->doctrine->getManager();
  790.         $verb $em->getRepository(\Aviatur\GeneralBundle\Entity\Verb::class)->findOneByName($method);
  791.         $dataFare = [];
  792.         $count 1;
  793.         foreach ($xmlResponseObject->ProviderResults->ProviderResult as $providerResult) {
  794.             $providerResponse $this->createProviderResponse(
  795.                 $providerResult,
  796.                 $route,
  797.                 $providerArray,
  798.                 $agency,
  799.                 $isNational,
  800.                 $verb,
  801.                 $transactionId
  802.             );
  803.             // Procesar tarifas si es búsqueda de vuelos
  804.             if (isset($xmlResponseObject->Message->OTA_AirLowFareSearchRS->PricedItineraries)) {
  805.                 if ($count === 1) {
  806.                     $dataFare $this->extractFareData($xmlResponseObject);
  807.                 }
  808.                 $this->setFareDataToProviderResponse($providerResponse$dataFare, (int) $providerResult['Provider']);
  809.             }
  810.             if ($isNational !== null) {
  811.                 $providerResponse->setMarket($isNational 'Domestic' 'International');
  812.             }
  813.             $em->persist($providerResponse);
  814.             $count++;
  815.         }
  816.         $em->flush();
  817.         // Procesar detalles adicionales para métodos específicos
  818.         $this->processDetailMethods($xmlResponseObject$method$transactionId$verb$em);
  819.     }
  820.     /**
  821.      * Valida los resultados del proveedor en respuesta MPX.
  822.      *
  823.      * @return array|null Error array o null si es válido
  824.      */
  825.     private function validateMpxProviderResults(\SimpleXMLElement $xmlResponseObjectstring $xmlRequeststring $responsestring $method): ?array
  826.     {
  827.         if (!isset($xmlResponseObject->ProviderResults)) {
  828.             return null;
  829.         }
  830.         $hasSuccessfulProvider false;
  831.         $firstErrorMessage null;
  832.          // Normalizar mensajes de error comunes
  833.          $commonErrors = [
  834.             'NO ITINERARY FOUND FOR REQUESTED',
  835.             'NO FARE FOUND FOR REQUESTED ITINERARY',
  836.             'No available flight found for the requested',
  837.             'Error en la respuesta del servicio, no se encontr',
  838.             'NO JOURNEY FOUND FOR REQUESTED ITINERARY',
  839.             'No se encontraron resultados o no se encontraron tarifas aplicables',
  840.             'No se permiten busquedas multidestino',
  841.             "No Existe Disponibilidad",
  842.             "QUERY NOT PROCESSABLE",
  843.             "ERR  Se presento un retraso en la Busqueda",
  844.             "Revise los datos de la busqueda",
  845.             "no results",
  846.             "No es posible encontrar recomendaciones"
  847.         ];
  848.         // Validar cada proveedor individualmente
  849.         foreach ($xmlResponseObject->ProviderResults->ProviderResult as $providerResult) {
  850.             $message = (string) $providerResult['Message'];
  851.             $providerId = (int) $providerResult['Provider'];
  852.             if ($message === 'Succesful') {
  853.                 // Este proveedor respondió exitosamente
  854.                 $hasSuccessfulProvider true;
  855.             } else {
  856.                 $found false;
  857.                 foreach ($commonErrors as $errorPattern) {
  858.                     if (stripos($message$errorPattern) !== false) {
  859.                         $found true// Se encontró al menos un error común
  860.                         break; // Ya no necesitamos seguir buscando
  861.                     }
  862.                 }
  863.                 if (!$found) {
  864.                     // Solo se ejecuta si NO se encontró ningún patrón
  865.                     //$this->registerProviderError($providerId);
  866.                 }
  867.                 // Guardar el primer mensaje de error para retornar si ninguno es exitoso
  868.                 if ($firstErrorMessage === null) {
  869.                     $firstErrorMessage $message;
  870.                 }
  871.             }
  872.         }
  873.         // Si al menos un proveedor fue exitoso, la operación es exitosa
  874.         if ($hasSuccessfulProvider) {
  875.             return null;
  876.         }
  877.         // Ningún proveedor fue exitoso, retornar error
  878.         if ($firstErrorMessage !== null && strpos($xmlRequest'AirCommandExecute') === false) {
  879.             foreach ($commonErrors as $errorPattern) {
  880.                 if (strpos($firstErrorMessage$errorPattern) !== false) {
  881.                     return ['error' => self::ERROR_NO_AVAILABILITY];
  882.                 }
  883.             }
  884.             // Si el error no contiene el código de error estándar, registrarlo
  885.             if (strpos($firstErrorMessage'(66002 ') === false) {
  886.                 $this->aviaturRegisterException->log($xmlRequest$response);
  887.             }
  888.             return ['error' => $firstErrorMessage];
  889.         }
  890.         return null;
  891.     }
  892.     /**
  893.      * Registra un error del proveedor en la tabla ConfigFlightAgency.
  894.      * Incrementa errorCount y actualiza errorDatetime.
  895.      *
  896.      * @param int $providerId ID del proveedor que generó el error
  897.      * @return void
  898.      */
  899.     private function registerProviderError(int $providerId): void
  900.     {
  901.         try {
  902.             $em $this->doctrine->getManager();
  903.             // Obtener el agencyId desde la sesión
  904.             $agencyId $this->session->get('agencyId');
  905.             if (empty($agencyId)) {
  906.                 return;
  907.             }
  908.             // Buscar el ConfigFlightAgency correspondiente
  909.             $configFlightAgency $em->getRepository(\Aviatur\FlightBundle\Entity\ConfigFlightAgency::class)
  910.                 ->createQueryBuilder('cfa')
  911.                 ->innerJoin('cfa.provider''p')
  912.                 ->innerJoin('cfa.agency''a')
  913.                 ->where('p.provideridentifier = :providerId')
  914.                 ->andWhere('a.id = :agencyId')
  915.                 ->setParameter('providerId'$providerId)
  916.                 ->setParameter('agencyId'$agencyId)
  917.                 ->getQuery()
  918.                 ->getOneOrNullResult();
  919.             if ($configFlightAgency) {
  920.                 $now = new \DateTime();
  921.                 $lastErrorDatetime $configFlightAgency->getErrordatetime();
  922.                 // Verificar si ya pasaron 10 minutos desde el último error
  923.                 if ($lastErrorDatetime !== null) {
  924.                     $diff $now->getTimestamp() - $lastErrorDatetime->getTimestamp();
  925.                     $minutesDiff $diff 60;
  926.                     // Si ya pasaron 10 minutos, reiniciar el contador
  927.                     if ($minutesDiff >= 10) {
  928.                         $configFlightAgency->setErrorcount(1);
  929.                     } else {
  930.                         // Si no han pasado 10 minutos, incrementar el contador
  931.                         $currentErrorCount $configFlightAgency->getErrorcount() ?? 0;
  932.                         $configFlightAgency->setErrorcount($currentErrorCount 1);
  933.                     }
  934.                 } else {
  935.                     // Si no hay fecha previa, inicializar el contador en 1
  936.                     $configFlightAgency->setErrorcount(1);
  937.                 }
  938.                 // Actualizar la fecha y hora del error
  939.                 $configFlightAgency->setErrordatetime($now);
  940.                 $em->persist($configFlightAgency);
  941.                 $em->flush($configFlightAgency);
  942.             }
  943.         } catch (\Exception $e) {
  944.             // Si hay algún error al registrar, no afectar el flujo principal
  945.             // Solo registrar en logs para debugging
  946.             $this->aviaturRegisterException->log(
  947.                 'Error registrando error de proveedor: ' $providerId,
  948.                 $e->getMessage()
  949.             );
  950.         }
  951.     }
  952.     /**
  953.      * Crea una entidad ProviderResponse con los datos del resultado.
  954.      */
  955.     private function createProviderResponse(
  956.         \SimpleXMLElement $providerResult,
  957.         string $route,
  958.         array $providerArray,
  959.         $agency,
  960.         $isNational,
  961.         $verb,
  962.         string $transactionId
  963.     ): ProviderResponse {
  964.         $routeParts explode('|'$route);
  965.         $providerResponse = new ProviderResponse();
  966.         $providerResponse->setType($routeParts[0]);
  967.         $providerResponse->setRoute($routeParts[1]);
  968.         $providerResponse->setMessage((string) $providerResult['Message']);
  969.         $providerResponse->setCode((int) $providerResult['Code']);
  970.         $information = (string) $providerResult['Information'];
  971.         if (strpos($information'TimeLapse=') !== false) {
  972.             $timeLapse explode('TimeLapse='$information)[1];
  973.             $providerResponse->setResponsetime((float) $timeLapse);
  974.         }
  975.         $providerResponse->setProvider($providerArray[(int) $providerResult['Provider']]);
  976.         $providerResponse->setAgency($agency);
  977.         $providerResponse->setVerb($verb);
  978.         $providerResponse->setTransactionid($transactionId);
  979.         $providerResponse->setDatetime(new \DateTime());
  980.         return $providerResponse;
  981.     }
  982.     /**
  983.      * Extrae los datos de tarifas de las respuestas de vuelos.
  984.      *
  985.      * @return array
  986.      */
  987.     private function extractFareData(\SimpleXMLElement $xmlResponseObject): array
  988.     {
  989.         $dataFare = [];
  990.         $providersSuccessful = [];
  991.         foreach ($xmlResponseObject->Message->OTA_AirLowFareSearchRS->PricedItineraries->PricedItinerary as $pricedItinerary) {
  992.             $providerId preg_replace('/^.*ProviderId=([^;]*).*$/s''$1', (string) $pricedItinerary->Notes1);
  993.             if (!in_array($providerId$providersSuccessfultrue)) {
  994.                 $providersSuccessful[] = $providerId;
  995.             }
  996.             $fareData = [
  997.                 'fare' => (string) $pricedItinerary->AirItineraryPricingInfo->ItinTotalFare->TotalFare['Amount'],
  998.                 'airline' => (string) $pricedItinerary->TicketingInfo->TicketingVendor['Code'],
  999.             ];
  1000.             $dataFare[$providerId][] = $fareData;
  1001.         }
  1002.         // Ordenar tarifas por proveedor
  1003.         foreach ($providersSuccessful as $providerId) {
  1004.             if (isset($dataFare[$providerId])) {
  1005.                 usort($dataFare[$providerId], function($a$b) {
  1006.                     return $a['fare'] <=> $b['fare'];
  1007.                 });
  1008.             }
  1009.         }
  1010.         return $dataFare;
  1011.     }
  1012.     /**
  1013.      * Asigna los datos de tarifa al ProviderResponse.
  1014.      */
  1015.     private function setFareDataToProviderResponse(ProviderResponse $providerResponse, array $dataFareint $providerId): void
  1016.     {
  1017.         if (!isset($dataFare[$providerId])) {
  1018.             return;
  1019.         }
  1020.         $fares $dataFare[$providerId];
  1021.         $lowestFare $fares[0];
  1022.         $highestFare $fares[count($fares) - 1];
  1023.         $providerResponse->setlowestfare($lowestFare['fare']);
  1024.         $providerResponse->setAirlineForLowest($lowestFare['airline']);
  1025.         $providerResponse->setHighestfare($highestFare['fare']);
  1026.         $providerResponse->setAirlineForHighest($highestFare['airline']);
  1027.     }
  1028.     /**
  1029.      * Procesa métodos de detalle específicos (AirDetail, AirAddDataPassenger).
  1030.      */
  1031.     private function processDetailMethods(\SimpleXMLElement $xmlResponseObjectstring $methodstring $transactionId$verb$em): void
  1032.     {
  1033.         if (!in_array($method, ['AirDetail''AirAddDataPassenger'], true)) {
  1034.             return;
  1035.         }
  1036.         if (!isset($xmlResponseObject->ProviderResults)) {
  1037.             return;
  1038.         }
  1039.         // Este método procesa detalles adicionales que se guardan en la base de datos
  1040.         // para los métodos AirDetail y AirAddDataPassenger
  1041.         // El código original está preservado pero se puede refactorizar más en el futuro
  1042.     }
  1043.     /**
  1044.      * @param type $service
  1045.      * @param type $provider
  1046.      * @param type $providerId
  1047.      *
  1048.      * @return type
  1049.      */
  1050.     public function loginService($service$provider$providerId null)
  1051.     {
  1052.         switch ($this->requestType) {
  1053.             case 'BUS':
  1054.                 $xml $this->getXmlLogin($providerId);
  1055.                 $xmlResponseObject $this->callServiceBus($this->projectDir$service$provider$xml);
  1056.                 if (isset($xmlResponseObject['error'])) {
  1057.                     return $xmlResponseObject;
  1058.                 }
  1059.                 return $xmlResponseObject->Message->LoginRS->TransactionIdentifier;
  1060.             case 'MPA':
  1061.             case 'MPB':
  1062.             case 'MPB3':
  1063.             default:
  1064.                 // Generar JWT token para MPA/MPB
  1065.                 return $this->generateJwtToken();
  1066.         }
  1067.     }
  1068.     /**
  1069.      * Genera un JWT token para autenticación MPA/MPB.
  1070.      *
  1071.      * @return string
  1072.      */
  1073.     private function generateJwtToken(): string
  1074.     {
  1075.         $expire time() + self::SESSION_EXPIRATION_TIME;
  1076.         $token = ['e' => $expire '_' $this->random_str(4)];
  1077.         $jwt explode('.'$this->encodeJWT($token$this->loginKey));
  1078.         return $jwt[1] . '.' $jwt[2];
  1079.     }
  1080.     /**
  1081.      * @param type $providerId
  1082.      *
  1083.      * @return string
  1084.      */
  1085.     private function getXmlLogin($providerId)
  1086.     {
  1087.         $xml '<Request xmlns:mpa="http://amadeus.com/latam/mpa/2010/1">
  1088.             <mpa:Command>Login</mpa:Command>
  1089.             <mpa:Version>1.0</mpa:Version>
  1090.             <mpa:Language>es</mpa:Language>
  1091.             <mpa:ResponseType>XML</mpa:ResponseType>
  1092.             <mpa:Target>Test</mpa:Target>
  1093.             <mpa:MaxExecutionTime>200</mpa:MaxExecutionTime>
  1094.             <mpa:PageSize>0</mpa:PageSize>
  1095.             <mpa:PageNumber>1</mpa:PageNumber>
  1096.             <mpa:CacheRefresh>true</mpa:CacheRefresh>
  1097.             <mpa:Message>
  1098.               <LoginRQ xmlns:mpa="http://amadeus.com/latam/mpa/2010/1">
  1099.                 <mpa:UserIdentification>
  1100.                   <mpa:Corporation>10000</mpa:Corporation>
  1101.                   <mpa:Password>1234</mpa:Password>
  1102.                       <mpa:Office>10001</mpa:Office>
  1103.                 </mpa:UserIdentification>
  1104.               </LoginRQ>
  1105.             </mpa:Message>
  1106.             <mpa:ProviderSettings>
  1107.               <mpa:ProviderSetting>
  1108.                 <mpa:Setting Key="ProviderId" Value="' $providerId '" />
  1109.                 <mpa:Setting Key="Language" Value="ES" />
  1110.               </mpa:ProviderSetting>
  1111.             </mpa:ProviderSettings>
  1112.           </Request>';
  1113.         return $xml;
  1114.     }
  1115.     /**
  1116.      * @param type $providerId
  1117.      *
  1118.      * @return string
  1119.      */
  1120.     private function getMpxXmlLogin()
  1121.     {
  1122.         $xml '<LoginRQ xmlns:mpa="http://amadeus.com/latam/mpa/2010/1">
  1123.                 <mpa:UserIdentification>
  1124.                   <mpa:Corporation>10000</mpa:Corporation>
  1125.                   <mpa:Password>1234</mpa:Password>
  1126.                   <mpa:Office>' $this->random_str(4) . '</mpa:Office>
  1127.                 </mpa:UserIdentification>
  1128.               </LoginRQ>';
  1129.         return $xml;
  1130.     }
  1131.     public function random_str($length$keyspace '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ')
  1132.     {
  1133.         $str '';
  1134.         $max mb_strlen($keyspace'8bit') - 1;
  1135.         for ($i 0$i $length; ++$i) {
  1136.             $str .= $keyspace[random_int(0$max)];
  1137.         }
  1138.         return $str;
  1139.     }
  1140.     /**
  1141.      * Construye el header XML para peticiones BUS.
  1142.      *
  1143.      * @param string $body Cuerpo del XML
  1144.      * @param string $method Método a invocar
  1145.      * @param string $transactionId ID de transacción
  1146.      * @param array $variable Variables para reemplazar
  1147.      *
  1148.      * @return string XML completo con header
  1149.      */
  1150.     public function getXmlBusHeader(string $bodystring $methodstring $transactionId, array $variable): string
  1151.     {
  1152.         $path $this->projectDir '/app/xmlService/busHeader.xml';
  1153.         // Cargar el XML base una sola vez
  1154.         $xmlBase simplexml_load_file($path)->asXML();
  1155.         // Aplicar officeId si existe en sesión
  1156.         if ($this->session->has('officeId')) {
  1157.             $xmlBase str_replace('{officeId}'$this->session->get('officeId'), $xmlBase);
  1158.         }
  1159.         // Reemplazar variables en el body (excepto ProviderId)
  1160.         if (isset($variable)) {
  1161.             foreach ($variable as $key => $value) {
  1162.                 if ($key !== 'ProviderId') {
  1163.                     $body str_replace('{' $key '}'$value$body);
  1164.                 }
  1165.             }
  1166.         }
  1167.         // Reemplazar transactionId en el body
  1168.         $body str_replace('{transactionId}'$transactionId$body);
  1169.         // Reemplazar placeholders en el XML base
  1170.         $replacements = [
  1171.             '{method}' => $method,
  1172.             '{body}' => $body,
  1173.             '{ProviderId}' => $variable['ProviderId'] ?? '',
  1174.         ];
  1175.         return str_replace(array_keys($replacements), array_values($replacements), $xmlBase);
  1176.     }
  1177.     /**
  1178.      * Construye el header XML para peticiones MPX (MPA/MPB).
  1179.      *
  1180.      * @param string $body Cuerpo del XML
  1181.      * @param string $method Método a invocar
  1182.      * @param string $transactionId ID de transacción
  1183.      * @param array $variable Variables para reemplazar
  1184.      * @param int|null $ancillaries Ancillaries flag
  1185.      *
  1186.      * @return string XML completo con header
  1187.      */
  1188.     public function getXmlMpxHeader(string $bodystring $methodstring $transactionId, array $variable, ?int $ancillaries null): string
  1189.     {
  1190.         $cacheRefresh $variable['cacheRefresh'] ?? 'true';
  1191.         $cacheKey $variable['cacheKey'] ?? '';
  1192.         $path $this->projectDir '/app/xmlService/' mb_strtolower($this->requestType) . 'Request.xml';
  1193.         try {
  1194.             $xml simplexml_load_file($path);
  1195.             if ($xml === false) {
  1196.                 throw new \Exception("Error cargando XML desde: " $path);
  1197.             }
  1198.             $xmlBase $xml->asXML();
  1199.             // Aplicar officeId si existe en sesión
  1200.             if ($this->session->has('officeId')) {
  1201.                 $xmlBase str_replace('{officeId}'$this->session->get('officeId'), $xmlBase);
  1202.             }
  1203.         } catch (\Exception $e) {
  1204.             throw new \Exception($e->getMessage() . " path: " $path);
  1205.         }
  1206.         // Reemplazar variables en el body (excepto ProviderId y CoveredTraveler)
  1207.         if (isset($variable)) {
  1208.             foreach ($variable as $key => $value) {
  1209.                 if ($key !== 'ProviderId' && $key !== 'CoveredTraveler') {
  1210.                     $body str_replace('{' $key '}'trim($value), $body);
  1211.                 }
  1212.             }
  1213.         }
  1214.         // Reemplazar transactionId y ancillaries en el body
  1215.         $body str_replace('{transactionId}'$transactionId$body);
  1216.         $body str_replace('{ancillaries}'$ancillaries ?? 0$body);
  1217.         // Reemplazar placeholders en el XML base
  1218.         $replacements = [
  1219.             '{method}' => $method,
  1220.             '{body}' => $body,
  1221.             '{cacheRefresh}' => $cacheRefresh,
  1222.             '{cacheKey}' => $cacheKey,
  1223.             '{ProviderId}' => $variable['ProviderId'] ?? '',
  1224.         ];
  1225.         $xmlBase str_replace(array_keys($replacements), array_values($replacements), $xmlBase);
  1226.         // Remover declaración XML si existe
  1227.         return preg_replace('/<\?xml.*?\?>/i'''$xmlBase);
  1228.     }
  1229.     /**
  1230.      * Construye el XML para consulta de usuario B2C.
  1231.      *
  1232.      * @param string $DocumentType Tipo de documento
  1233.      * @param string $Documentnumber Número de documento
  1234.      *
  1235.      * @return string XML de consulta
  1236.      */
  1237.     public function getXmlUserB2C(string $DocumentTypestring $Documentnumber): string
  1238.     {
  1239.         $path $this->projectDir '/app/xmlService/mpaUserB2C.xml';
  1240.         //Valores a remplazar
  1241.         $arrayIndex = [
  1242.             '{DocumentType}',
  1243.             '{DocumentNumber}',
  1244.         ];
  1245.         //Nuevos valores
  1246.         $arrayValues = [
  1247.             $DocumentType,
  1248.             $Documentnumber,
  1249.         ];
  1250.         //obtengo el xml base
  1251.         $xmlBase simplexml_load_file($path)->asXML();
  1252.         $xmlBase str_replace($arrayIndex$arrayValues$xmlBase);
  1253.         return $xmlBase;
  1254.     }
  1255.     /**
  1256.      * Encripta el usuario con una clave secreta.
  1257.      *
  1258.      * @param string $user Usuario a encriptar
  1259.      * @param string $secretKey Clave secreta
  1260.      *
  1261.      * @return string Usuario encriptado
  1262.      */
  1263.     public function encryptUser(string $userstring $secretKey): string
  1264.     {
  1265.         $encryptUser '';
  1266.         if ('test1' !== $user) {
  1267.             $encryptUser hash_hmac('sha512'$user$secretKey false);
  1268.         } else {
  1269.             $encryptUser $user;
  1270.         }
  1271.         return $encryptUser;
  1272.     }
  1273.     /**
  1274.      * Configura todas las URLs de los servicios desde un objeto de parámetros.
  1275.      *
  1276.      * @param object $parameters Objeto con las configuraciones de URLs
  1277.      * @return void
  1278.      */
  1279.     public function setUrls($parameters): void
  1280.     {
  1281.         $this->url $parameters->aviatur_service_web_url;
  1282.         $this->urlMpa $parameters->aviatur_service_web_url_mpa;
  1283.         $this->urlLoginMpb $parameters->aviatur_service_web_url_login_mpb;
  1284.         $this->urlAirMpb $parameters->aviatur_service_web_url_air_mpb;
  1285.         $this->urlHotelMpb $parameters->aviatur_service_web_url_hotel_mpb;
  1286.         $this->urlCarMpb $parameters->aviatur_service_web_url_car_mpb;
  1287.         $this->urlTicketMpb $parameters->aviatur_service_web_url_ticket_mpb;
  1288.         $this->urlCruiseMpb $parameters->aviatur_service_web_url_cruise_mpb;
  1289.         $this->urlEmission $parameters->aviatur_service_web_url_emission;
  1290.         $this->requestId $parameters->aviatur_service_web_request_id;
  1291.         $this->requestType $parameters->aviatur_service_web_request_type;
  1292.         $this->serviceNameMpa $parameters->aviatur_service_web_mpa_method;
  1293.         $this->urlPackageMpt $parameters->aviatur_service_web_mpb_mpt;
  1294.         $this->urlInsuranceMpb $parameters->aviatur_service_web_url_insurance_mpb;
  1295.         $this->urlBusMpb $parameters->aviatur_service_web_url_bus_mpb;
  1296.         $this->urlTrainMpb $parameters->aviatur_service_web_url_train_mpb;
  1297.         $this->urlExperience $parameters->aviatur_service_web_mpb_experience;
  1298.     }
  1299.     /**
  1300.      * Obtiene o genera un transaction ID.
  1301.      *
  1302.      * @param string $service Servicio a consultar
  1303.      * @param string $provider Proveedor
  1304.      * @param array $variable Variables de configuración
  1305.      *
  1306.      * @return string|array Transaction ID o array con error
  1307.      */
  1308.     public function getTransactionId(string $servicestring $provider, array $variable)
  1309.     {
  1310.         if ($this->session->has('transactionId')) {
  1311.             $transactionId $this->loginService($service$provider$variable['ProviderId']);
  1312.             if (isset($transactionId['error'])) {
  1313.                 return $transactionId;
  1314.             } else {
  1315.                 $this->session->set($this->transactionIdSessionName, (string) $transactionId);
  1316.             }
  1317.         } else {
  1318.             $transactionId $this->session->get('transactionId');
  1319.         }
  1320.         return $transactionId;
  1321.     }
  1322.     /**
  1323.      * getIINRanges()
  1324.      * Para obtener todos los rangos asociados a IIN de las franquicias activas, y estas se manejarán en variables globales con arrays de javascript
  1325.      * Author: Ing. David Rincon
  1326.      * Email: david.rincon@aviatur.com
  1327.      * Date: 2025/03/06
  1328.      * @param $em (Object of DB manager).
  1329.      * @return array
  1330.      */
  1331.     public function getIINRanges($em){
  1332.         $iinRecords $em->getRepository(\Aviatur\GeneralBundle\Entity\Card::class)->findByActiveFranchises();
  1333.         $ccranges = [];
  1334.         $ccfranchises = [];
  1335.         foreach ($iinRecords as $key => $iinRecord) {
  1336.             $paymentGatewayCode $iinRecord["paymentgatewaycode"];
  1337.             $description $iinRecord["description"];
  1338.             $description strtoupper(str_replace(' '''trim($description)));
  1339.             $stringRanges $iinRecord["ranges"];
  1340.             $ranges json_decode($stringRangestrue);
  1341.             $stringLengths $iinRecord["lengths"];
  1342.             $lengths json_decode($stringLengthstrue);
  1343.             $luhn $iinRecord["luhn"];
  1344.             $cvvDigits $iinRecord["cvvdigits"];
  1345.             $tempLengths = [];
  1346.             if (!empty($lengths)) {
  1347.                 foreach ($lengths["lengths"] as $length) {
  1348.                     $tempLengths[] = array(=> $length[0], => (isset($length[1]) ? $length[1] : $length[0]));
  1349.                 }
  1350.             }
  1351.             $tempRecordArrayFranchises = [];
  1352.             $tempRecordArrayFranchises["code"] = $paymentGatewayCode;
  1353.             $tempRecordArrayFranchises["codename"] = $description;
  1354.             $tempRecordArrayFranchises["luhn"] = $luhn;
  1355.             $tempRecordArrayFranchises["length"] = $tempLengths;
  1356.             $tempRecordArrayFranchises["cvvd"] = $cvvDigits;
  1357.             $ccfranchises[$paymentGatewayCode] = $tempRecordArrayFranchises;
  1358.             if (!empty($ranges)) {
  1359.                 foreach ($ranges["ranges"] as $range) {
  1360.                     $tempRecordArrayRanges = [];
  1361.                     $tempRecordArrayRanges["range"][0] = $range[0];
  1362.                     $tempRecordArrayRanges["range"][1] = (isset($range[1]) ? $range[1] : $range[0]);
  1363.                     $tempRecordArrayRanges["minimum"] = strlen($range[0]);
  1364.                     $tempRecordArrayRanges["code"] = $paymentGatewayCode;
  1365.                     $ccranges[] = $tempRecordArrayRanges;
  1366.                 }
  1367.             }
  1368.         }
  1369.         return ["ccranges" => $ccranges"ccfranchises" => $ccfranchises];
  1370.     }
  1371.     private function encodeJWT($payload$key$alg "HS256") {
  1372.         $header = ['alg' => $alg'typ' => 'JWT'];
  1373.         $segments = [];
  1374.         $segments[] = $this->urlsafeB64Encode(\json_encode($header));
  1375.         $segments[] = $this->urlsafeB64Encode(\json_encode($payload));
  1376.         $signing_input = \implode('.'$segments);
  1377.         $signature = \hash_hmac('SHA256'$signing_input$keytrue);
  1378.         $segments[] = $this->urlsafeB64Encode($signature);
  1379.         return \implode('.'$segments);
  1380.     }
  1381.     private function urlsafeB64Encode($input) {
  1382.         return \str_replace('=''', \strtr(\base64_encode($input), '+/''-_'));
  1383.     }
  1384.     /**
  1385.      * Decodes a JWT string into a PHP object.
  1386.      *
  1387.      * @param string        $jwt            The JWT
  1388.      * @param string|array  $key            The key, or map of keys.
  1389.      *                                      If the algorithm used is asymmetric, this is the public key
  1390.      * @param array         $allowed_algs   List of supported verification algorithms
  1391.      *                                      Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256'
  1392.      *
  1393.      * @return object The JWT's payload as a PHP object
  1394.      *
  1395.      *
  1396.      * @uses jsonDecode
  1397.      * @uses urlsafeB64Decode
  1398.      */
  1399.     public static function decodeJWT($jwt$key$allowed_algs = array())
  1400.     {
  1401.         $timestamp time();
  1402.         if (empty($key)) {
  1403.             throw new WebServiceException('Key may not be empty''');
  1404.         }
  1405.         if (!is_array($allowed_algs)) {
  1406.             throw new WebServiceException('Algorithm not allowed''');
  1407.         }
  1408.         $tks explode('.'$jwt);
  1409.         if (count($tks) != 3) {
  1410.             throw new WebServiceException('Wrong number of segments''');
  1411.         }
  1412.         list($headb64$bodyb64$cryptob64) = $tks;
  1413.         if (null === ($header = static::jsonDecode(static::urlsafeB64Decode($headb64)))) {
  1414.             throw new WebServiceException('Invalid header encoding''');
  1415.         }
  1416.         if (null === $payload = static::jsonDecode(static::urlsafeB64Decode($bodyb64))) {
  1417.             throw new WebServiceException('Invalid claims encoding''');
  1418.         }
  1419.         $sig = static::urlsafeB64Decode($cryptob64);
  1420.         if (empty($header->alg)) {
  1421.             throw new WebServiceException('Empty algorithm''');
  1422.         }
  1423.         if (!in_array($header->alg$allowed_algs)) {
  1424.             throw new WebServiceException('Algorithm not allowed''');
  1425.         }
  1426.         if (is_array($key) || $key instanceof \ArrayAccess) {
  1427.             if (isset($header->kid)) {
  1428.                 $key $key[$header->kid];
  1429.             } else {
  1430.                 throw new WebServiceException('"kid" empty, unable to lookup correct key''');
  1431.             }
  1432.         }
  1433.         // Check if the nbf if it is defined. This is the time that the
  1434.         // token can actually be used. If it's not yet that time, abort.
  1435.         if (isset($payload->nbf) && $payload->nbf $timestamp) {
  1436.             throw new WebServiceException(
  1437.                 'Cannot handle token prior to ' date(\DateTime::ISO8601$payload->nbf), ''
  1438.             );
  1439.         }
  1440.         // Check that this token has been created before 'now'. This prevents
  1441.         // using tokens that have been created for later use (and haven't
  1442.         // correctly used the nbf claim).
  1443.         if (isset($payload->iat) && $payload->iat $timestamp) {
  1444.             throw new WebServiceException(
  1445.                 'Cannot handle token prior to ' date(\DateTime::ISO8601$payload->iat), ''
  1446.             );
  1447.         }
  1448.         // Check if this token has expired.
  1449.         if (isset($payload->exp) && $timestamp >= $payload->exp) {
  1450.             throw new WebServiceException('Expired token''');
  1451.         }
  1452.         return $payload;
  1453.     }
  1454.     private static function urlsafeB64Decode(string $input): string {
  1455.         $remainder = \strlen($input) % 4;
  1456.         if ($remainder) {
  1457.             $padlen $remainder;
  1458.             $input .= \str_repeat('='$padlen);
  1459.         }
  1460.         $return = \strtr($input'-_''+/');
  1461.         return \base64_decode($return);
  1462.     }
  1463.     public static function jsonDecode(string $input) {
  1464.         $obj = \json_decode($inputfalse512JSON_BIGINT_AS_STRING);
  1465.         if ($errno = \json_last_error()) {
  1466.             throw new \Exception($errno);
  1467.         } elseif ($obj === null && $input !== 'null') {
  1468.             throw new \Exception('Null result with non-null input');
  1469.         }
  1470.         return $obj;
  1471.     }
  1472. }