Plan FAC-84 — Eventos: Cancelación e Inutilización
Ticket Jira: FAC-84 — Actualización del WS de Anulación (Eventos) Sprint: 3.4 Fecha de plan: 2026-04-09 Estado: Planificación / no iniciado
Objetivo
Implementar el registro de eventos del emisor ante SIFEN vía el WS sincrónico siRecepEvento:
- Cancelación de DTE (
dTiGDE=1) — invalidar un DTE ya aprobado por SIFEN dentro del plazo legal. - Inutilización de número de DE (
dTiGDE=2) — comunicar a SIFEN un rango de números no utilizados (saltos, errores técnicos).
Ambos requieren:
- XML firmado con XMLDSig (mismo patrón que el DE).
- Envelope SOAP con namespace mixto (Fix #5 ya documentado).
- Llamada HTTPS mTLS al endpoint
evento.wsdl. - Persistencia del resultado en BD para auditoría.
Estructura del XML del evento (Manual v150 §11.5)
<rGesEve>
<rEve Id="<id correlativo numérico 1-10 dígitos>">
<dFecFirma>2026-04-09T15:30:00</dFecFirma>
<dVerFor>150</dVerFor>
<dTiGDE>1</dTiGDE> <!-- 1=Cancelación, 2=Inutilización -->
<gGroupTiEvt>
<!-- ┌─ dTiGDE=1: Cancelación ─┐ -->
<rGeVeCan>
<Id>01801260060001001000094322026040912850865440</Id> <!-- CDC 44 chars -->
<mOtEve>Motivo libre 5–500 caracteres</mOtEve>
</rGeVeCan>
<!-- ┌─ dTiGDE=2: Inutilización ─┐ -->
<rGeVeInu>
<dNumTim>80126006</dNumTim>
<dEst>001</dEst>
<dPunExp>001</dPunExp>
<dNumIn>0000001</dNumIn>
<dNumFin>0000005</dNumFin>
<iTiDE>1</iTiDE> <!-- 1=FE, 2=FE export, 3=FE import, 4=AFE, 5=NCE, 6=NDE, 7=NRE, 8=CRE -->
<mOtEve>Motivo libre 5–500 caracteres</mOtEve>
</rGeVeInu>
</gGroupTiEvt>
</rEve>
<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
<!-- Reference URI="#<id de rEve>" -->
</Signature>
</rGesEve>
Cardinalidad lote: hasta 15 eventos por llamada al WS (rGesEve ocurre 1–15).
Reglas de negocio (Manual v150 §11.1, Tabla J)
| Regla | Cancelación | Inutilización |
|---|---|---|
dTiGDE | 1 | 2 |
| Plazo | 48h post-aprobación si es FE; 168h si es NCE/NDE/NRE/AFE | Antes de aprobación SIFEN. Hasta 15 días del mes siguiente al hecho |
| Estado del DTE | approved o approved_obs (extemporaneidad) | DE NO debe existir aprobado en SIFEN |
| Identificador | CDC del DTE | Rango secuencial dNumIn → dNumFin |
| Tamaño máximo | 1 CDC por evento | Hasta 1000 números secuenciales por evento |
| Motivo | Campo libre 5–500 chars (mOtEve) | Campo libre 5–500 chars |
| Lote | hasta 15 eventos en un solo POST | hasta 15 |
| WS | siRecepEvento (sincrónico) | siRecepEvento (sincrónico) |
| Si tiene DTEs asociados | Cancelar primero el último, luego ascender | n/a |
Estado actual del proyecto
✅ Lo que YA existe
| Componente | Ubicación |
|---|---|
Endpoint evento.wsdl (test/prod) | config/sifen.php:21,30 |
| Envelope SOAP con namespace mixto Fix#5 | app/Domains/Sifen/Services/Soap/SifenSoapEnvelopeBuilder.php::buildEvento() |
| Signer XMLDSig reutilizable | app/Domains/Sifen/Services/Signing/SifenXmlDsigSigner.php |
| HTTP client mTLS | app/Domains/Sifen/Services/Soap/SifenHttpClient.php |
Enum MotivoEvento (NRE solo, no sirve para cancelación/inutilización) | app/Domains/Sifen/Enums/MotivoEvento.php |
❌ Lo que FALTA construir
| # | Componente | Descripción |
|---|---|---|
| 1 | Enum TipoEvento | 1=CANCELACION, 2=INUTILIZACION con dTiGDE y descripción |
| 2 | DTOs readonly | CancelacionEventoData, InutilizacionEventoData |
| 3 | Builder XML | SifenEventoXmlBuilder::buildCancelacion(...) y ::buildInutilizacion(...) |
| 4 | Action | RegistrarEventoAction — orquesta validate → build → sign → send → persist |
| 5 | Modelo Eloquent + migración | Tabla sifen_eventos (ver schema abajo) |
| 6 | Endpoints REST | POST /api/sifen/eventos/cancelacion y POST /api/sifen/eventos/inutilizacion |
| 7 | Validaciones de plazo y estado | DTE approved, dentro de 48h/168h, CDC válido; rango 1-1000 sin DEs aprobados |
| 8 | Scripts tinker | Demos end-to-end de cada evento |
| 9 | Documentación | docs/guias-tecnicas/SIFEN_EVENTOS_FUNCIONAMIENTO.md (post implementación) |
Schema BD propuesto: sifen_eventos
CREATE TABLE sifen_eventos (
id BIGSERIAL PRIMARY KEY,
sifen_emitter_id BIGINT NOT NULL REFERENCES sifen_emitters(id),
sifen_dte_id BIGINT NULL REFERENCES sifen_dtes(id), -- nullable: inutilización no tiene DTE asociado
tipo SMALLINT NOT NULL, -- 1=cancelación, 2=inutilización
cdc VARCHAR(44) NULL, -- solo cancelación
timbrado VARCHAR(8) NULL, -- solo inutilización
establecimiento VARCHAR(3) NULL, -- solo inutilización
punto_expedicion VARCHAR(3) NULL, -- solo inutilización
numero_inicio VARCHAR(7) NULL, -- solo inutilización
numero_fin VARCHAR(7) NULL, -- solo inutilización
tipo_documento SMALLINT NULL, -- solo inutilización (iTiDE)
motivo TEXT NOT NULL CHECK (length(motivo) BETWEEN 5 AND 500),
xml_signed_path VARCHAR(255) NULL, -- ruta al XML firmado en disco
estado VARCHAR(20) NOT NULL DEFAULT 'pending', -- pending|sent|approved|rejected
sifen_result_code VARCHAR(4) NULL,
sifen_result_msg VARCHAR(255) NULL,
prot_aut VARCHAR(15) NULL, -- nº de protocolo SIFEN
fecha_envio TIMESTAMP NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_sifen_eventos_dte_id ON sifen_eventos(sifen_dte_id);
CREATE INDEX idx_sifen_eventos_estado ON sifen_eventos(estado);
CREATE INDEX idx_sifen_eventos_tipo ON sifen_eventos(tipo);
Endpoints REST propuestos
POST /api/sifen/eventos/cancelacion
Body:
{
"cdc": "01801260060001001000094322026040912850865440",
"motivo": "Cancelación por error en datos del receptor"
}
Validaciones:
cdcrequerido, 44 chars numéricos, debe existir ensifen_dtescon estadoapproved.motivorequerido, string entre 5 y 500 chars.- Verificar que
now() - dte.fecha_aprobacion <= 48h(FE) o<= 168h(otros tipos). - Verificar que no existe ya un evento de cancelación aprobado para ese CDC.
Respuesta éxito:
{
"status": "approved",
"evento_id": 12,
"cod_res": "0660",
"msg_res": "Evento Cancelación registrado satisfactoriamente",
"prot_aut": "12345678901234"
}
POST /api/sifen/eventos/inutilizacion
Body:
{
"timbrado": "80126006",
"establecimiento": "001",
"punto_expedicion": "001",
"numero_inicio": "0000010",
"numero_fin": "0000015",
"tipo_documento": 1,
"motivo": "Saltos en numeración por reinicio del sistema de facturación"
}
Validaciones:
numero_inicio <= numero_fin.(numero_fin - numero_inicio + 1) <= 1000.- Para CADA número en el rango: NO debe existir un DTE en
sifen_dtescon estadoapprovedoapproved_obs. Si existe alguno → rechazar el request. motivorequerido, 5–500 chars.tipo_documentoválido (1–8).
Códigos de respuesta SIFEN para eventos (Manual §12)
| Código | Significado |
|---|---|
0660 | Evento de cancelación registrado satisfactoriamente |
0670 | Evento de inutilización registrado satisfactoriamente |
0661 | Cancelación rechazada (DTE no encontrado / fuera de plazo / ya cancelado) |
0671 | Inutilización rechazada (rango ya usado / inválido / superpuesto) |
0160 | XML mal formado (validar firma + estructura) |
Códigos exactos a verificar contra
docs/manuales-md/manual_v150_cap12_validaciones.mddurante implementación.
Pipeline interno (similar al SendDteJob actual)
HTTP POST /api/sifen/eventos/cancelacion
↓
RegistrarEventoAction::cancelacion()
↓
1. Validar (FormRequest) → CDC existe + DTE approved + plazo OK
2. SifenEventoXmlBuilder::buildCancelacion()
↓ rGesEve XML sin firmar
3. SifenXmlDsigSigner::sign($xml, "Id" => $eventoId)
↓ rGesEve firmado
4. SifenStorageService::persistSignedEventoXml()
↓ guardar en sifen/xml/eventos/{Y-m}/{evento_id}.xml
5. Persistir registro en sifen_eventos (estado=pending)
6. SifenSoapEnvelopeBuilder::buildEvento($xmlFirmado, $id)
7. SifenHttpClient::post(endpoint=evento.wsdl, soapAction=rEnviEventoDe)
8. Parsear respuesta SOAP → extraer dCodRes, dMsgRes, dProtAut
9. Update sifen_eventos.estado = approved|rejected
10. Si cancelación aprobada → update sifen_dtes.estado = canceled
11. Devolver SifenResponseData
Plan de ejecución por fases
Fase 1 — Cancelación end-to-end (la más usada)
- ✅ Plan documentado (este archivo).
- Enum
TipoEvento. - DTO
CancelacionEventoData. - Migración + Modelo
SifenEvento. SifenEventoXmlBuilder::buildCancelacion()con tests unitarios.- Adaptar
SifenXmlDsigSignersi necesita parámetros distintos para eventos. RegistrarEventoAction::cancelacion().- FormRequest + endpoint
POST /api/sifen/eventos/cancelacion. - Script tinker
tinker_test_cancelacion.php: crear DTE → aprobar → cancelar → verificar. - Smoke test contra TEST de SIFEN.
- Documentación
docs/guias-tecnicas/SIFEN_EVENTOS_FUNCIONAMIENTO.mdcon sección Cancelación.
Fase 2 — Inutilización
- DTO
InutilizacionEventoData. SifenEventoXmlBuilder::buildInutilizacion().RegistrarEventoAction::inutilizacion().- FormRequest con validación de rango + verificación de DEs aprobados en el rango.
- Endpoint
POST /api/sifen/eventos/inutilizacion. - Script tinker
tinker_test_inutilizacion.php. - Extender doc
SIFEN_EVENTOS_FUNCIONAMIENTO.mdcon sección Inutilización.
Fase 3 — Mejoras (opcional)
- Lote de eventos (hasta 15 por POST) —
RegistrarEventosLoteAction. - Auditoría visible vía Filament admin (cuando exista FAC-90).
- Job asíncrono
RegistrarEventoJobpara no bloquear la API en envíos masivos.
Riesgos conocidos
| Riesgo | Mitigación |
|---|---|
Firma XMLDSig sobre rEve (no sobre rGesEve) — error fácil | Reusar el patrón del DE: Reference URI="#<id de rEve>", signer envuelve la firma como sibling de rEve dentro de rGesEve |
| Namespace mixto del envelope (Fix #5) | Ya resuelto en buildEvento() — NO modificar |
| Error 0160 si el XML del evento tiene xmlns redundante | Sanitizar antes de embeber en CDATA — sanitizarXml() ya lo hace |
| Plazo de 48h no es trivial calcular si hay zonas horarias | Usar dFecFirma del DTE en America/Asuncion y calcular diferencia |
| Inutilización debe verificar que NO exista ningún DE aprobado en el rango | Query sifen_dtes WHERE timbrado=? AND est=? AND punto_exp=? AND numero BETWEEN ? AND ? AND estado IN (approved, approved_obs) antes de aceptar |
mOtEve mínimo 5 chars, máximo 500 | Validación en FormRequest + constraint en BD |
Referencias
docs/manuales-md/manual_v150_cap11_eventos.md— capítulo 11 completo (eventos)docs/pdf/ManualV150_cap_11_eventos.pdf— PDF originaldocs/pdf/Manual Técnico Versión 150.pdf§9.5 —siRecepEventodocs/manuales-md/manual_v150_cap12_validaciones.md— códigos de respuestaapp/Domains/Sifen/Services/Soap/SifenSoapEnvelopeBuilder.php::buildEvento()— envelope ya implementadodocs/guias-tecnicas/SIFEN_LOTE_ASYNC_FUNCIONAMIENTO.md— pipeline reference para los 3 fixes que aplicamos a lotes (mismas reglas aplican a eventos)- Jira: FAC-84