SIFEN — Envío Asíncrono por Lotes y Sincronización de Estados
Versión: 2026-04-09 Implementación:
SifenBatchService+SincronizarEstadosSifenJobWS 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
| Archivo | Rol |
|---|---|
app/Domains/Sifen/Services/Soap/SifenBatchService.php | Fase 1: construye ZIP, envía lote, guarda prot_lote |
app/Jobs/Sifen/SincronizarEstadosSifenJob.php | Fase 2: consulta resultado y actualiza estados |
app/Domains/Sifen/Services/Soap/SifenSoapEnvelopeBuilder.php | Fábrica SOAP — buildConsultaLote() arma el envelope de consulta |
app/Domains/Sifen/Services/Soap/SifenHttpClient.php | Transporte cURL mTLS para ambas fases |
app/Domains/Sifen/Enums/EstadoDte.php | Máquina de estados: sent → approved / rejected |
app/Domains/Sifen/Enums/SifenCodigoRespuesta.php | Clasificación de códigos SIFEN (isAprobado(), LOTE_PROCESADO, etc.) |
database/migrations/2026_03_09_000001_add_prot_lote_to_sifen_dtes_table.php | Columna prot_lote en sifen_dtes |
routes/console.php | Registro del scheduler everyFiveMinutes() |
4. Endpoints SIFEN
| WS | Ambiente TEST | Ambiente PROD |
|---|---|---|
Envío lote (siRecepLoteDE) | sifen-test.set.gov.py/de/ws/async/recibe-lote.wsdl | sifen.set.gov.py/de/ws/async/recibe-lote |
Consulta lote (siResultLoteDE) | sifen-test.set.gov.py/de/ws/consultas/consulta-lote | sifen.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ódigo | Significado | Acción del job |
|---|---|---|
0361 | Lote aún en procesamiento | Skip — se reintenta en el próximo ciclo (5 min) |
0362 | Procesamiento concluido | Parsear xResultDE de cada DE individualmente |
0340 | RUC no autorizado a consultar el lote | Rechazar todos los DTEs del lote |
0360 | Lote inexistente | Rechazar todos los DTEs del lote |
0320 | Mensaje de entrada > 1000 KB | Rechazar todos los DTEs del lote |
Códigos individuales dentro de 0362
| Código | Estado resultante |
|---|---|
0260 | approved |
1005 | approved (extemporáneo — aprobado con observaciones) |
| Cualquier otro | rejected |
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ón | Comportamiento |
|---|---|
| Error de red/TLS al consultar SIFEN | Skip 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 BD | Log warning — otros DTEs del lote se actualizan igual |
| Body de 0362 no es XML válido | Log warning — no se actualiza ningún DTE |
| Job falla con excepción no capturada | Horizon 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–0379docs/SIFEN_MODERN_ARCHITECTURE.md— §8 Máquina de estados del DTE