feat: implement parallel SOAP requests for vehicle verification and add supervisor configuration

This commit is contained in:
Juan Felipe Zapata Moreno 2026-03-30 14:51:01 -06:00
parent b17b6608d3
commit f2a15ef113
8 changed files with 241 additions and 14 deletions

View File

@ -17,6 +17,7 @@ RUN apk add --no-cache \
mysql-client \
libreoffice \
ttf-dejavu \
supervisor \
&& docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd zip \
&& echo "upload_max_filesize=150M" > /usr/local/etc/php/conf.d/uploads.ini \
&& echo "post_max_size=150M" >> /usr/local/etc/php/conf.d/uploads.ini
@ -32,6 +33,9 @@ COPY . .
COPY entrypoint-dev.sh /usr/local/bin/entrypoint-dev.sh
RUN chmod +x /usr/local/bin/entrypoint-dev.sh
COPY Docker/supervisor/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
RUN mkdir -p /var/log/supervisor
RUN mkdir -p storage/app/keys storage/logs bootstrap/cache
RUN chown -R www-data:www-data /var/www/repuve-backend-v1/storage /var/www/repuve-backend-v1/bootstrap/cache
@ -40,4 +44,4 @@ RUN chmod -R 775 /var/www/repuve-backend-v1/storage /var/www/repuve-backend-v1/b
EXPOSE 9000
ENTRYPOINT ["/usr/local/bin/entrypoint-dev.sh"]
CMD ["php-fpm"]
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]

View File

@ -14,6 +14,7 @@ RUN apk add --no-cache \
bash \
libreoffice \
ttf-dejavu \
supervisor \
&& docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd zip \
&& echo "upload_max_filesize=150M" > /usr/local/etc/php/conf.d/uploads.ini \
&& echo "post_max_size=150M" >> /usr/local/etc/php/conf.d/uploads.ini
@ -29,6 +30,9 @@ COPY . .
COPY entrypoint-prod.sh /usr/local/bin/entrypoint-prod.sh
RUN chmod +x /usr/local/bin/entrypoint-prod.sh
COPY Docker/supervisor/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
RUN mkdir -p /var/log/supervisor
RUN mkdir -p storage/app/keys storage/logs bootstrap/cache
RUN chown -R www-data:www-data /var/www/repuve-backend-v1/storage /var/www/repuve-backend-v1/bootstrap/cache
@ -37,4 +41,4 @@ RUN chmod -R 775 /var/www/repuve-backend-v1/storage /var/www/repuve-backend-v1/b
EXPOSE 9000
ENTRYPOINT ["/usr/local/bin/entrypoint-prod.sh"]
CMD ["php-fpm"]
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]

View File

@ -0,0 +1,30 @@
[supervisord]
nodaemon=true
user=root
logfile=/dev/null
logfile_maxbytes=0
pidfile=/var/run/supervisord.pid
[program:php-fpm]
command=php-fpm
autostart=true
autorestart=true
priority=1
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:queue-worker]
command=php /var/www/repuve-backend-v1/artisan queue:work --tries=3 --timeout=300 --sleep=3 --max-time=3600
autostart=true
autorestart=true
priority=2
user=www-data
numprocs=1
stopwaitsecs=60
stdout_logfile=/var/www/repuve-backend-v1/storage/logs/worker.log
stdout_logfile_maxbytes=50MB
stdout_logfile_backups=5
stderr_logfile=/var/www/repuve-backend-v1/storage/logs/worker.log
stderr_logfile_maxbytes=0

View File

@ -19,6 +19,7 @@
use App\Services\RepuveService;
use App\Services\PadronEstatalService;
use App\Jobs\ProcessRepuveResponse;
use App\Supports\SoapParallelExecutor;
use Illuminate\Routing\Controllers\HasMiddleware;
class InscriptionController extends Controller implements HasMiddleware
@ -119,8 +120,17 @@ public function vehicleInscription(VehicleStoreRequest $request)
]);
}
// Consultar REPUVE Nacional para corroborar el vehículo y obtener folio_CI
$repuveNacionalData = $this->repuveService->consultarVehiculo($niv, $placa);
// Consultar REPUVE Nacional y verificar robo en paralelo
$parallelRequests = [
'repuve' => $this->repuveService->prepareConsultarVehiculoRequest($niv, $placa),
'robo' => $this->repuveService->prepareVerificarRoboRequest($niv, $placa),
];
$parallelResults = SoapParallelExecutor::execute($parallelRequests);
// Parsear respuesta REPUVE Nacional
$repuveNacionalData = $this->repuveService->parseConsultarVehiculoResponse(
$parallelResults['repuve']['response'] ?: ''
);
// Verificar si hubo error en la consulta a REPUVE Nacional
if ($repuveNacionalData['has_error'] ?? false) {
@ -136,8 +146,11 @@ public function vehicleInscription(VehicleStoreRequest $request)
$folioRepuve = $repuveNacionalData['folio_CI'] ?? null;
$actionType = empty($folioRepuve) ? 'sustitucion_primera_vez' : 'sustitucion';
// Verificar robo
$roboResult = $this->checkIfStolen($niv, $placa);
// Parsear respuesta de robo
$roboResult = $this->repuveService->parseRoboResponse(
$parallelResults['robo']['response'] ?: '',
$niv ?: $placa ?: 'N/A'
);
// Solo bloquear si está marcado como robado
if ($roboResult['is_robado'] ?? false) {
DB::rollBack();

View File

@ -21,6 +21,7 @@
use Exception;
use Illuminate\Support\Facades\Auth;
use App\Jobs\ProcessRepuveResponse;
use App\Supports\SoapParallelExecutor;
use App\Models\CatalogTagStatus;
use Illuminate\Routing\Controllers\HasMiddleware;
@ -141,10 +142,20 @@ public function tagSubstitution(Request $request)
]);
}
// Validar vehículo en PadronEstatal antes de proceder con la sustitución
// Consultar PadronEstatal, verificar robo y consultar REPUVE en paralelo
$parallelRequests = [
'padron' => $this->padronEstatalService->prepareConsultarPadronRequest('placa', $vehicle->placa),
'robo' => $this->repuveService->prepareVerificarRoboRequest($vehicle->niv, null),
'repuve' => $this->repuveService->prepareConsultarVehiculoRequest($vehicle->niv, $vehicle->placa),
];
$parallelResults = SoapParallelExecutor::execute($parallelRequests);
// Parsear respuesta del Padrón Estatal
try {
$datosEstatal = $this->padronEstatalService->getVehiculoByPlaca($vehicle->placa);
$vehicleDataEstatal = $this->padronEstatalService->extraerDatosVehiculo($datosEstatal);
$datosEstatalRaw = $this->padronEstatalService->parsearRespuesta(
$parallelResults['padron']['response'] ?: ''
);
$vehicleDataEstatal = $this->padronEstatalService->extraerDatosVehiculo($datosEstatalRaw);
} catch (PadronEstatalException $e) {
return ApiResponse::BAD_REQUEST->response([
'message' => 'No se pudo validar el vehículo en el Padrón Estatal.',
@ -161,7 +172,11 @@ public function tagSubstitution(Request $request)
]);
}
$roboResult = $this->checkIfStolen($vehicle->niv);
// Parsear respuesta de robo
$roboResult = $this->repuveService->parseRoboResponse(
$parallelResults['robo']['response'] ?: '',
$vehicle->niv
);
// Solo bloquear si explícitamente está marcado como robado
if ($roboResult['is_robado'] ?? false) {
@ -170,8 +185,10 @@ public function tagSubstitution(Request $request)
]);
}
// Obtener folio_CI de REPUVE antes de cancelar (folio anterior)
$repuveData = $this->repuveService->consultarVehiculo($vehicle->niv, $vehicle->placa);
// Parsear respuesta REPUVE para obtener folio_CI anterior
$repuveData = $this->repuveService->parseConsultarVehiculoResponse(
$parallelResults['repuve']['response'] ?: ''
);
$folioAnterior = $repuveData['folio_CI'] ?? null;
DB::beginTransaction();

View File

@ -31,7 +31,35 @@ public function getVehiculoByFolio(string $folio): array
/**
* Consulta el padrón vehicular estatal
* Prepara la petición SOAP sin ejecutarla.
* Retorna ['url', 'body', 'headers'] para usar con SoapParallelExecutor.
*/
public function prepareConsultarPadronRequest(string $tipo, string $valor): array
{
$data = json_encode(['tipo' => $tipo, 'valor' => $valor]);
$soapBody = <<<XML
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:wsdl="http://mx/tgc/ConsultaPadronVehicular.wsdl">
<soapenv:Header/>
<soapenv:Body>
<wsdl:getVehiculosRepuve>
<Data>{$data}</Data>
</wsdl:getVehiculosRepuve>
</soapenv:Body>
</soapenv:Envelope>
XML;
return [
'url' => $this->soapUrl,
'body' => $soapBody,
'headers' => [
'Content-Type: text/xml; charset=utf-8',
'SOAPAction: ""',
'Content-Length: ' . strlen($soapBody),
],
];
}
private function consultarPadron(string $tipo, string $valor): array
{
$logger = Log::channel('padron_estatal');
@ -114,7 +142,7 @@ private function consultarPadron(string $tipo, string $valor): array
/**
* Parsea la respuesta del padrón estatal
*/
private function parsearRespuesta(string $soapResponse): array
public function parsearRespuesta(string $soapResponse): array
{
$logger = Log::channel('padron_estatal');

View File

@ -470,6 +470,84 @@ public function consultarVehiculo(?string $niv = null, ?string $placa = null)
}
}
/**
* Prepara la petición SOAP de consultarVehiculo sin ejecutarla.
* Retorna ['url', 'body', 'headers'] para usar con SoapParallelExecutor.
*/
public function prepareConsultarVehiculoRequest(?string $niv = null, ?string $placa = null): array
{
$this->asegurarCargaCredenciales();
$url = $this->baseUrl . '/jaxws-consultarpv/ConsultaRpv';
$campos = array_fill(0, 8, '');
$campos[0] = $niv ?? '';
$campos[2] = $placa ?? '';
$arg2 = implode('|', $campos);
$soapBody = <<<XML
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:wsdl="http://consultaRpv.org/wsdl">
<soapenv:Header/>
<soapenv:Body>
<wsdl:doConsRPV>
<arg0>{$this->username}</arg0>
<arg1>{$this->password}</arg1>
<arg2>{$arg2}</arg2>
</wsdl:doConsRPV>
</soapenv:Body>
</soapenv:Envelope>
XML;
return [
'url' => $url,
'body' => $soapBody,
'headers' => [
'Content-Type: text/xml; charset=utf-8',
'SOAPAction: "doConsRPV"',
'Content-Length: ' . strlen($soapBody),
],
];
}
/**
* Prepara la petición SOAP de verificarRobo sin ejecutarla.
* Retorna ['url', 'body', 'headers'] para usar con SoapParallelExecutor.
*/
public function prepareVerificarRoboRequest(?string $niv = null, ?string $placa = null): array
{
$this->asegurarCargaCredenciales();
$url = $this->baseUrl . $this->roboEndpoint;
$campos = array_fill(0, 8, '');
$campos[0] = $niv ?? '';
$campos[2] = $placa ?? '';
$arg2 = implode('|', $campos);
$soapBody = <<<XML
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:wsdl="http://consultaRpv.org/wsdl">
<soapenv:Header/>
<soapenv:Body>
<wsdl:doConsRepRobo>
<arg0>{$this->username}</arg0>
<arg1>{$this->password}</arg1>
<arg2>{$arg2}</arg2>
</wsdl:doConsRepRobo>
</soapenv:Body>
</soapenv:Envelope>
XML;
return [
'url' => $url,
'body' => $soapBody,
'headers' => [
'Content-Type: text/xml; charset=utf-8',
'SOAPAction: "doConsRepRobo"',
'Content-Length: ' . strlen($soapBody),
],
];
}
public function inscribirVehiculo(array $datos)
{
$this->asegurarCargaCredenciales();
@ -674,7 +752,7 @@ private function parsearRespuestaInscripcion(string $soapResponse)
];
}
private function parseRoboResponse(string $soapResponse, string $valor): array
public function parseRoboResponse(string $soapResponse, string $valor): array
{
// Extraer contenido del tag <return>
preg_match('/<return>(.*?)<\/return>/s', $soapResponse, $matches);

View File

@ -0,0 +1,53 @@
<?php
namespace App\Supports;
class SoapParallelExecutor
{
/**
* Ejecuta múltiples peticiones SOAP en paralelo usando curl_multi_exec.
*
* @param array $requests Mapa de clave => ['url' => string, 'body' => string, 'headers' => array]
* @return array Mapa de clave => ['response' => string|false, 'http_code' => int, 'curl_error' => string]
*/
public static function execute(array $requests): array
{
$multiHandle = curl_multi_init();
$handles = [];
foreach ($requests as $key => $req) {
$ch = curl_init($req['url']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $req['body']);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
curl_setopt($ch, CURLOPT_HTTPHEADER, $req['headers']);
curl_multi_add_handle($multiHandle, $ch);
$handles[$key] = $ch;
}
$running = null;
do {
$status = curl_multi_exec($multiHandle, $running);
if ($running) {
curl_multi_select($multiHandle);
}
} while ($running > 0 && $status === CURLM_OK);
$results = [];
foreach ($handles as $key => $ch) {
$results[$key] = [
'response' => curl_multi_getcontent($ch),
'http_code' => curl_getinfo($ch, CURLINFO_HTTP_CODE),
'curl_error' => curl_error($ch),
];
curl_multi_remove_handle($multiHandle, $ch);
curl_close($ch);
}
curl_multi_close($multiHandle);
return $results;
}
}