Saltar al contenido principal

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:

  1. Cancelación de DTE (dTiGDE=1) — invalidar un DTE ya aprobado por SIFEN dentro del plazo legal.
  2. 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)

ReglaCancelaciónInutilización
dTiGDE12
Plazo48h post-aprobación si es FE; 168h si es NCE/NDE/NRE/AFEAntes de aprobación SIFEN. Hasta 15 días del mes siguiente al hecho
Estado del DTEapproved o approved_obs (extemporaneidad)DE NO debe existir aprobado en SIFEN
IdentificadorCDC del DTERango secuencial dNumIn → dNumFin
Tamaño máximo1 CDC por eventoHasta 1000 números secuenciales por evento
MotivoCampo libre 5–500 chars (mOtEve)Campo libre 5–500 chars
Lotehasta 15 eventos en un solo POSThasta 15
WSsiRecepEvento (sincrónico)siRecepEvento (sincrónico)
Si tiene DTEs asociadosCancelar primero el último, luego ascendern/a

Estado actual del proyecto

✅ Lo que YA existe

ComponenteUbicación
Endpoint evento.wsdl (test/prod)config/sifen.php:21,30
Envelope SOAP con namespace mixto Fix#5app/Domains/Sifen/Services/Soap/SifenSoapEnvelopeBuilder.php::buildEvento()
Signer XMLDSig reutilizableapp/Domains/Sifen/Services/Signing/SifenXmlDsigSigner.php
HTTP client mTLSapp/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

#ComponenteDescripción
1Enum TipoEvento1=CANCELACION, 2=INUTILIZACION con dTiGDE y descripción
2DTOs readonlyCancelacionEventoData, InutilizacionEventoData
3Builder XMLSifenEventoXmlBuilder::buildCancelacion(...) y ::buildInutilizacion(...)
4ActionRegistrarEventoAction — orquesta validate → build → sign → send → persist
5Modelo Eloquent + migraciónTabla sifen_eventos (ver schema abajo)
6Endpoints RESTPOST /api/sifen/eventos/cancelacion y POST /api/sifen/eventos/inutilizacion
7Validaciones de plazo y estadoDTE approved, dentro de 48h/168h, CDC válido; rango 1-1000 sin DEs aprobados
8Scripts tinkerDemos end-to-end de cada evento
9Documentacióndocs/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:

  • cdc requerido, 44 chars numéricos, debe existir en sifen_dtes con estado approved.
  • motivo requerido, 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_dtes con estado approved o approved_obs. Si existe alguno → rechazar el request.
  • motivo requerido, 5–500 chars.
  • tipo_documento válido (1–8).

Códigos de respuesta SIFEN para eventos (Manual §12)

CódigoSignificado
0660Evento de cancelación registrado satisfactoriamente
0670Evento de inutilización registrado satisfactoriamente
0661Cancelación rechazada (DTE no encontrado / fuera de plazo / ya cancelado)
0671Inutilización rechazada (rango ya usado / inválido / superpuesto)
0160XML mal formado (validar firma + estructura)

Códigos exactos a verificar contra docs/manuales-md/manual_v150_cap12_validaciones.md durante 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)

  1. ✅ Plan documentado (este archivo).
  2. Enum TipoEvento.
  3. DTO CancelacionEventoData.
  4. Migración + Modelo SifenEvento.
  5. SifenEventoXmlBuilder::buildCancelacion() con tests unitarios.
  6. Adaptar SifenXmlDsigSigner si necesita parámetros distintos para eventos.
  7. RegistrarEventoAction::cancelacion().
  8. FormRequest + endpoint POST /api/sifen/eventos/cancelacion.
  9. Script tinker tinker_test_cancelacion.php: crear DTE → aprobar → cancelar → verificar.
  10. Smoke test contra TEST de SIFEN.
  11. Documentación docs/guias-tecnicas/SIFEN_EVENTOS_FUNCIONAMIENTO.md con sección Cancelación.

Fase 2 — Inutilización

  1. DTO InutilizacionEventoData.
  2. SifenEventoXmlBuilder::buildInutilizacion().
  3. RegistrarEventoAction::inutilizacion().
  4. FormRequest con validación de rango + verificación de DEs aprobados en el rango.
  5. Endpoint POST /api/sifen/eventos/inutilizacion.
  6. Script tinker tinker_test_inutilizacion.php.
  7. Extender doc SIFEN_EVENTOS_FUNCIONAMIENTO.md con 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 RegistrarEventoJob para no bloquear la API en envíos masivos.

Riesgos conocidos

RiesgoMitigación
Firma XMLDSig sobre rEve (no sobre rGesEve) — error fácilReusar 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 redundanteSanitizar antes de embeber en CDATA — sanitizarXml() ya lo hace
Plazo de 48h no es trivial calcular si hay zonas horariasUsar dFecFirma del DTE en America/Asuncion y calcular diferencia
Inutilización debe verificar que NO exista ningún DE aprobado en el rangoQuery 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 500Validació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 original
  • docs/pdf/Manual Técnico Versión 150.pdf §9.5 — siRecepEvento
  • docs/manuales-md/manual_v150_cap12_validaciones.md — códigos de respuesta
  • app/Domains/Sifen/Services/Soap/SifenSoapEnvelopeBuilder.php::buildEvento() — envelope ya implementado
  • docs/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