Saltar al contenido principal

SIFEN — Envío Asíncrono por Lotes y Sincronización de Estados

Versión: 2026-04-09 Implementación: SifenBatchService + SincronizarEstadosSifenJob WS SIFEN: siRecepLoteDE (envío) + siResultLoteDE (consulta)


1. Por qué existe este flujo

El WS síncrono (siRecepDE) responde aprobado/rechazado en la misma llamada HTTP. El WS asíncrono (siRecepLoteDE) solo confirma que el lote fue recibido — el resultado real llega después. Antes de este job, había que consultar manualmente los archivos .txt del portal de SIFEN para saber si los DEs de un lote fueron aprobados. SincronizarEstadosSifenJob automatiza esa segunda consulta.


2. Flujo completo

┌─────────────────────────────────────────────────────────────────────┐
│ FASE 1 — Envío del lote │
│ │
│ SifenController::sendBatch() │
│ → SifenBatchService::enviarLote() │
│ → construye ZIP con los XMLs firmados │
│ → POST siRecepLoteDE → respuesta 0300 (lote recibido) │
│ → guarda dProtConsLote en sifen_dtes.prot_lote │
│ → estado de cada DTE: sent │
└─────────────────────────────────────────────────────────────────────┘

(SIFEN procesa el lote internamente)

┌─────────────────────────────────────────────────────────────────────┐
│ FASE 2 — Sincronización (cada 5 minutos) │
│ │
│ Schedule::job(SincronizarEstadosSifenJob) ← scheduler │
│ → Horizon pickea el job de la cola 'default' │
│ → consulta sifen_dtes WHERE estado=sent AND prot_lote IS NOT NULL │
│ → agrupa por prot_lote │
│ → para cada lote: POST siResultLoteDE con dProtConsLote │
│ → 0361: lote en proceso → skip, próximo ciclo │
│ → 0362: concluido → lee xResultDE de cada DE │
│ → 0260 / 1005 → estado: approved │
│ → cualquier otro → estado: rejected │
│ → otro código → rechaza todos los DTEs del lote │
└─────────────────────────────────────────────────────────────────────┘

3. Archivos clave

ArchivoRol
app/Domains/Sifen/Services/Soap/SifenBatchService.phpFase 1: construye ZIP, envía lote, guarda prot_lote
app/Jobs/Sifen/SincronizarEstadosSifenJob.phpFase 2: consulta resultado y actualiza estados
app/Domains/Sifen/Services/Soap/SifenSoapEnvelopeBuilder.phpFábrica SOAP — buildConsultaLote() arma el envelope de consulta
app/Domains/Sifen/Services/Soap/SifenHttpClient.phpTransporte cURL mTLS para ambas fases
app/Domains/Sifen/Enums/EstadoDte.phpMáquina de estados: sent → approved / rejected
app/Domains/Sifen/Enums/SifenCodigoRespuesta.phpClasificación de códigos SIFEN (isAprobado(), LOTE_PROCESADO, etc.)
database/migrations/2026_03_09_000001_add_prot_lote_to_sifen_dtes_table.phpColumna prot_lote en sifen_dtes
routes/console.phpRegistro del scheduler everyFiveMinutes()

4. Endpoints SIFEN

WSAmbiente TESTAmbiente PROD
Envío lote (siRecepLoteDE)sifen-test.set.gov.py/de/ws/async/recibe-lote.wsdlsifen.set.gov.py/de/ws/async/recibe-lote
Consulta lote (siResultLoteDE)sifen-test.set.gov.py/de/ws/consultas/consulta-lotesifen.set.gov.py/de/ws/consultas/consulta-lote

Config en config/sifen.php → claves async y consulta_lote.


5. Códigos de respuesta de siResultLoteDE

CódigoSignificadoAcción del job
0361Lote aún en procesamientoSkip — se reintenta en el próximo ciclo (5 min)
0362Procesamiento concluidoParsear xResultDE de cada DE individualmente
0340RUC no autorizado a consultar el loteRechazar todos los DTEs del lote
0360Lote inexistenteRechazar todos los DTEs del lote
0320Mensaje de entrada > 1000 KBRechazar todos los DTEs del lote

Códigos individuales dentro de 0362

CódigoEstado resultante
0260approved
1005approved (extemporáneo — aprobado con observaciones)
Cualquier otrorejected

6. Estructura XML de la respuesta 0362

<env:Envelope>
<env:Body>
<ns2:rRetEnviConsLoteDe>
<ns2:dCodRes>0362</ns2:dCodRes>
<ns2:dMsgRes>Procesamiento de lote concluido</ns2:dMsgRes>
<ns2:xDR>
<ns2:xResultDE>
<ns2:dCDC>01800126006001001000000012026031517879345440</ns2:dCDC>
<ns2:dCodRes>0260</ns2:dCodRes>
<ns2:dMsgRes>Autorización del DE satisfactoria</ns2:dMsgRes>
</ns2:xResultDE>
<ns2:xResultDE>
<ns2:dCDC>01800126006001001000000022026031517879345441</ns2:dCDC>
<ns2:dCodRes>1002</ns2:dCodRes>
<ns2:dMsgRes>Documento electrónico duplicado</ns2:dMsgRes>
</ns2:xResultDE>
</ns2:xDR>
</ns2:rRetEnviConsLoteDe>
</env:Body>
</env:Envelope>

El job parsea cada xResultDE por su dCDC, lo cruza contra la BD por sifen_dtes.cdc y actualiza el estado individualmente.


7. Scheduler + Horizon

El scheduler solo dispara el job — no lo ejecuta directamente. Horizon es quien procesa:

crontab del server (una sola línea, requerida):
* * * * * cd /var/www/onnixconnect && php artisan schedule:run >> /dev/null 2>&1

routes/console.php:
Schedule::job(new SincronizarEstadosSifenJob())->everyFiveMinutes();

El job usa WithoutOverlapping con TTL de 10 minutos para evitar que dos instancias corran en paralelo si Horizon está lento.

Cola: default (no es crítica — puede convivir con otros jobs de Horizon).


8. Comportamiento ante errores

SituaciónComportamiento
Error de red/TLS al consultar SIFENSkip del lote — reintenta en el próximo ciclo del scheduler
SIFEN responde 0361 (en proceso)Skip — reintenta en 5 minutos automáticamente
SIFEN responde 0362 pero CDC no está en BDLog warning — otros DTEs del lote se actualizan igual
Body de 0362 no es XML válidoLog warning — no se actualiza ningún DTE
Job falla con excepción no capturadaHorizon reintenta 3 veces con backoff 30s / 60s / 120s

9. Cómo probar en Tinker

// Verificar DTEs en 'sent' con prot_lote pendiente
\App\Domains\Sifen\Models\SifenDte::where('estado', 'sent')
->whereNotNull('prot_lote')
->get(['id', 'cdc', 'prot_lote', 'last_sent_at']);

// Disparar el job manualmente (sync, sin cola)
\App\Jobs\Sifen\SincronizarEstadosSifenJob::dispatchSync();

// Ver que el scheduler lo reconoce
// (desde terminal): php artisan schedule:list

10. Referencia SIFEN

  • Manual Técnico e-kuatia v150 — §4.3 siResultLoteDE
  • docs/manual_v150_cap12_validaciones.md — sección BG–BI, códigos 0320–0379
  • docs/SIFEN_MODERN_ARCHITECTURE.md — §8 Máquina de estados del DTE