Saltar al contenido principal

SIFEN_MODERN_ARCHITECTURE.md

Arquitectura de referencia extraída de sifen_laravel_multiempresa para OnnixConnect OC. Fecha de análisis: 2026-03-03 | Stack: Laravel 12, DDD, Livewire v3 / Volt


ÍNDICE

  1. Estructura de Dominios
  2. Flujo de Datos: DteData → XML Firmado
  3. Lógica de Construcción XML Dinámica
  4. Firma XMLDSig (Enveloped)
  5. Capa SOAP: Envoltura y Transmisión HTTP
  6. Generación del QR (Post-Firma)
  7. Procesamiento Asíncrono: BuildAndSignDteJob
  8. Gestión de Estado del Documento (Máquina de Estados)
  9. Capa de Validación
  10. Patrones de UI Reactiva (Livewire/Volt)
  11. Esquema de Base de Datos
  12. Configuración Centralizada (config/sifen.php)
  13. Patrones Reutilizables para OnnixConnect OC

1. ESTRUCTURA DE DOMINIOS

app/
├── Domains/
│ └── Sifen/
│ ├── DTO/
│ │ └── DteData.php ← Value Object de entrada (datos crudos)
│ ├── Xml/
│ │ └── SifenXmlBuilder.php ← Construcción DOM del XML v150
│ ├── Services/
│ │ ├── SecurityCodeGenerator.php ← Genera dCodSeg único (9 dígitos)
│ │ ├── SifenQrService.php ← Inyecta URL QR en XML firmado
│ │ ├── SifenSigner.php ← Firma via openssl CLI (legacy/fallback)
│ │ └── SifenXmlDsigSigner.php ← Firma via XMLSecLibs (PHP puro) ← PREFERIDO
│ ├── Soap/
│ │ ├── SifenSoapEnvelopeBuilder.php ← Construye sobre SOAP 1.2 / 1.1
│ │ ├── SifenSoapPayloadBuilder.php ← Empaqueta XML para envío
│ │ ├── SifenSoapClient.php ← Cliente SOAP (wrapper)
│ │ └── SifenSoapHttpClient.php ← HTTP raw con cURL + mTLS
│ └── Validation/
│ ├── SifenXmlValidator.php ← Valida estructura DOM/XPath
│ └── SifenTotalsValidator.php ← Recalcula y cruza totales
├── Jobs/
│ └── BuildAndSignDteJob.php ← Orquestador async (Queue)
├── Models/
│ └── Sifen/
│ ├── SifenEmitter.php ← Empresa emisora (multi-tenant)
│ ├── SifenDte.php ← Documento Tributario Electrónico
│ ├── SifenDteItem.php ← Ítems de línea del DTE
│ └── SifenLog.php ← Audit log de envíos/excepciones
└── Console/Commands/
├── SifenSendSyncHttp.php ← Envío síncrono SOAP (CLI)
├── SifenPayloadDte.php ← Inspección de payload
├── SifenTestDte.php ← Prueba de generación completa
├── SifenValidateDte.php ← Validación XSD/estructura
└── SifenWsdlInfo.php ← Introspección WSDL

Principio de Separación

CapaResponsabilidad
DTOTransporta datos crudos sin lógica. Inmutable (constructor PHP 8).
XmlTraduce el estado del modelo Eloquent a nodos DOM.
ServicesAlgoritmos especializados: firma, QR, código de seguridad.
SoapProtocolo de transporte hacia DNIT (SOAP 1.2 sin WSDL).
ValidationVerificación post-construcción: estructura + totales.
JobsOrquesta los pasos anteriores de forma asíncrona.

2. FLUJO DE DATOS: DteData → XML Firmado

[API / UI / Comando]


DteData (DTO) ← app/Domains/Sifen/DTO/DteData.php
Inmutable, tipado ← contiene: dteId, tipoDocumento, ruc, dv,
(PHP 8 constructor) clienteDocumento, clienteNombre, total, items[]

▼ dispatch(new BuildAndSignDteJob($dteId))

╔═══════════════════════════════════════════════════════════════╗
║ BuildAndSignDteJob::handle() ║
║ 1. SifenDte::with(['emitter','items'])->findOrFail($id) ║
║ 2. SifenXmlBuilder::build($dte) → xml_unsigned_path ║
║ 3. [Guard] Verificar cert_p12_path + cert_p12_password_enc ║
║ 4. SifenXmlDsigSigner::sign($dte) → xml_signed_path ║
║ (estado: draft → signed) ║
╚═══════════════════════════════════════════════════════════════╝

▼ (Post-firma, separado)
SifenQrService::updateSignedXmlQr($dte)
← Lee DigestValue del XML firmado
← Calcula URL QR con SHA-256 + CSC
← Inyecta <dCarQR> en <gCamFuFD>
← Re-serializa XML (mismo path)


SifenSoapEnvelopeBuilder::build(
operation: 'rEnviDe',
params: [ dId => id, xDE => xmlLiteral (CDATA) ]
)


SifenSoapHttpClient::postSoap(endpoint, soapAction, xml, emitter)
← mTLS con cert+key PEM (derivados del .p12 del emisor)
← estado: signed → sent / error

Puntos de control de estado en la tabla sifen_dtes:

draft → (xml generado) → draft
draft → (firmado) → signed
signed → (enviado OK) → sent
signed → (error envío) → error
sent → (cancelado) → cancelled

3. LÓGICA DE CONSTRUCCIÓN XML DINÁMICA

Archivo: app/Domains/Sifen/Xml/SifenXmlBuilder.php

3.1 Patrón de Construcción DOM

El builder usa la API nativa DOMDocument de PHP (no SimpleXML, no concatenación de strings). Esto garantiza:

  • XML bien formado por construcción (el DOM valida la estructura).
  • Namespaces declarados una sola vez en el nodo raíz.
  • Caracteres especiales (&, <, >) escapados automáticamente.
// Patrón base: createElement + appendChild en cascada
$dom = new DOMDocument('1.0', 'UTF-8');
$dom->preserveWhiteSpace = false;
$dom->formatOutput = false; // sin indentación → mínimo tamaño

$rDE = $dom->createElementNS($xmlnsSifen, 'rDE'); // ← Namespace en raíz
$dom->appendChild($rDE);

// Namespaces adicionales declarados en la raíz:
$rDE->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:xsi', '...');
$rDE->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:ds', '...');

3.2 Manejo de Namespaces

PrefijoURIPropósito
(default)http://ekuatia.set.gov.py/sifen/xsdTodos los nodos del DTE
xmlns:xsihttp://www.w3.org/2001/XMLSchema-instancexsi:schemaLocation
xmlns:dshttp://www.w3.org/2000/09/xmldsig#Nodos de firma XMLDSig

Técnica clave: El namespace SIFEN se declara con createElementNS() en el nodo raíz <rDE>. Todos los nodos hijos se crean con createElement() (sin NS explícito), y heredan el namespace por defecto del documento. Esto produce un XML válido para el XSD sin repetir el prefijo en cada nodo.

3.3 Mapeo Modelo → XML (secciones)

SifenDte + SifenEmitter → gOpeDE (tipo emisión, código seguridad)
SifenEmitter → gTimb (timbrado, establecimiento, punto)
SifenEmitter → gEmis (RUC, razón social, dirección, actividad)
SifenDte.cliente_* → gDatRec (tipo contribuyente, RUC/CI, nombre)
SifenDte.items[] → gCamItem[] (por cada ítem: código, descripción,
cantidad, precio, IVA por tasa)
items calculados → gTotSub (subtotales exentos/5%/10%, IVA total)
(placeholder vacío) → gCamFuFD/dCarQR ← se rellena POST-FIRMA

3.4 Algoritmo CDC (Clave de Control del Documento)

El CDC es el identificador único del DTE (44 caracteres + 1 DV):

CDC = TD(2) + RUC(8) + DV(1) + EST(3) + PUN(3) + NUM(7) + TC(1) + FECHA(8) + TE(1) + CODSEG(9) + DV_MOD11(1)

El DV se calcula con módulo 11 extendido (soporta letras vía ASCII ord):

// Peso rotativo 2..11 de derecha a izquierda.
// Si vResto <= 1 → dígito = 0, si no → 11 - vResto

3.5 Detección Automática de Tipo de Receptor

private function looksLikeRuc(string $doc): bool {
return str_contains($doc, '-'); // "80001607-6" es RUC, "1234567" es CI
}
  • RUC: emite <dRucRec> + <dDVRec> con iTiContRec=1
  • CI/Pasaporte: emite <dNumIDRec> con iTiContRec=2

3.6 Formato de Montos

// Guaraníes: siempre enteros, sin decimales
private function fmtMoney0(float $n): string {
return (string) (int) round($n, 0);
}
// Cantidades: preservar decimales para fracciones (litros, kg, etc.)
private function fmtQty($n): string {
return (string) (float) $n;
}

3.7 Mapa de Tipos de Documento

$mapTipo = [
'FE' => 1, // Factura electrónica
'FCE' => 1,
'NCE' => 5, // Nota de crédito
'NDE' => 6, // Nota de débito
'REM' => 7, // Remisión
];

4. FIRMA XMLDSig (ENVELOPED)

Archivo preferido: app/Domains/Sifen/Services/SifenXmlDsigSigner.php Librería: robrichards/xmlseclibs (PHP puro, sin dependencias externas)

4.1 Flujo de Firma

1. Leer XML sin firmar desde storage
2. Parsear con DOMDocument (preserveWhiteSpace=false, LIBXML_NOBLANKS)
3. Localizar <DE Id="CDC"> por XPath con namespace registrado
4. Registrar atributo Id como tipo ID: $de->setIdAttribute('Id', true)
→ necesario para que URI="#CDC" resuelva correctamente
5. Crear XMLSecurityDSig con método EXC_C14N (Canonical XML Exclusivo)
6. addReference($de, SHA256, [enveloped-signature, EXC_C14N], uri="#CDC")
7. Cargar clave privada (PEM, derivada del .p12)
8. dsig->sign($key)
9. dsig->add509Cert($certPem, true, false) → embebe X509Certificate
10. dsig->appendSignature($de) → inserta <ds:Signature> dentro de <DE>
11. REORDENAR: mover <ds:Signature> ANTES de <gCamFuFD>
(el QR debe ser el ÚLTIMO nodo del <DE>)
12. Serializar con $doc->saveXML()
13. Guardar DTE_{id}_signed.xml, actualizar DB: estado='signed'

4.2 Caché de Certificados PEM

El .p12 se convierte a PEM una sola vez por emisor y se cachea en:

storage/app/sifen/certs/pem/emitter_{id}/
cert.pem ← certificado público
key.pem ← clave privada (sin passphrase)
tls.pem ← cert + key concatenados (para mTLS en cURL)
// Patrón lazy: solo extrae si los archivos no existen
if (file_exists($certPem) && file_exists($keyPem)) {
return [$certPem, $keyPem];
}
// openssl_pkcs12_read($p12, $certs, $password)

4.3 Seguridad de Credenciales

// La password del .p12 se almacena CIFRADA en DB:
$password = Crypt::decryptString($emitter->cert_p12_password_enc);
// Usa la APP_KEY de Laravel como clave de cifrado (AES-256-CBC)

4.4 Doble Signer (Estrategia de Fallback)

SignerTecnologíaCuándo usar
SifenXmlDsigSignerxmlseclibs PHP puroProducción (recomendado)
SifenSigneropenssl CLI + xmlsec1Desarrollo/debug, entornos sin extensión OpenSSL

El SifenSigner implementa la misma cascada: primero intenta xmlsec1 (binario), si falla hace fallback a openssl smime.


5. CAPA SOAP: ENVOLTURA Y TRANSMISIÓN HTTP

5.1 SifenSoapEnvelopeBuilder

Sin WSDL. Construye el sobre SOAP 1.2 directamente como string:

<?xml version="1.0" encoding="UTF-8"?>
<env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope"
xmlns:ns2="{namespace}">
<env:Header/>
<env:Body>
<ns2:{operation}>
<ns2:dId>{id}</ns2:dId>
<ns2:xDE><![CDATA[{xml_firmado_completo}]]></ns2:xDE>
</ns2:{operation}>
</env:Body>
</env:Envelope>

Patrón de renderizado de parámetros:

modeTécnicaUso
texthtmlspecialchars(ENT_XML1)dId, strings simples
cdata<![CDATA[...]]>xDE (XML literal del DTE)

El CDATA se sanea:

  1. Se elimina el BOM UTF-8 (\xEF\xBB\xBF).
  2. Se elimina la declaración <?xml ...?>.
  3. Se escapan secuencias ]]> internas → ]]]]><![CDATA[>.

5.2 SifenSoapHttpClient (mTLS)

curl_setopt_array($ch, [
CURLOPT_SSLCERT => $tlsPem, // cert + key concatenados
CURLOPT_SSL_VERIFYPEER => $verify, // false en DEV (SIFEN_SSL_VERIFY=false)
CURLOPT_SSL_VERIFYHOST => $verify ? 2 : 0,
CURLOPT_TIMEOUT => 60,
CURLOPT_HTTPHEADER => [
'Content-Type: text/xml; charset=utf-8',
'SOAPAction: "rEnviDe"',
],
]);

5.3 Operaciones SOAP Disponibles

OperaciónEndpointDescripción
rEnviDesync/recibeEnvío síncrono (respuesta inmediata)
rEnviDeasync/recibeEnvío asíncrono (se consulta después)
(eventos)evento/recibeCancelación, inutilización, conformidad
(consulta)consulta/recibeConsulta estado CDC

6. GENERACIÓN DEL QR (POST-FIRMA)

Archivo: app/Domains/Sifen/Services/SifenQrService.php

Por qué se genera DESPUÉS de firmar: El QR incluye el DigestValue real de la firma XMLDSig, que solo existe en el XML ya firmado.

6.1 Algoritmo QR v150

Step 1 (parámetros ordenados exactos):
nVersion=150
&Id={CDC}
&dFeEmiDE={hex(fecha_en_ISO8601)} ← ASCII→HEX (bin2hex)
&dRucRec={ruc} o &dNumIDRec={ci} ← según tipo receptor
&dTotGralOpe={total_entero}
&dTotIVA={iva_total_entero}
&cItems={cantidad_items}
&DigestValue={hex(digestValue)} ← ASCII→HEX (bin2hex)
&IdCSC={idCsc_4_digits}

Step 2: step1 + CSC_del_emisor (32 chars, sin parámetro)

Step 3: cHashQR = sha256(step2)

URL final: {base_url}step1&cHashQR={cHashQR}

6.2 Inyección en el DOM

El builder coloca <dCarQR> vacío como placeholder durante la construcción:

<gCamFuFD><dCarQR></dCarQR></gCamFuFD>

Luego SifenQrService re-abre el XML firmado, calcula el QR real y actualiza el nodo. El orden garantizado es:

<DE Id="CDC">
... nodos del documento ...
<ds:Signature>...</ds:Signature> ← firma ANTES del QR
<gCamFuFD><dCarQR>URL_QR</dCarQR></gCamFuFD> ← QR ÚLTIMO
</DE>

7. PROCESAMIENTO ASÍNCRONO: BuildAndSignDteJob

Archivo: app/Jobs/BuildAndSignDteJob.php

class BuildAndSignDteJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

public int $dteId; // Solo el ID (no el modelo) → serialización segura
}

7.1 Patrón de Orquestación

public function handle(): void
{
// 1. Recargar el modelo fresquito desde DB (evitar datos stale del Job)
$dte = SifenDte::with(['emitter', 'items'])->findOrFail($this->dteId);

// 2. Construir XML
$relativeXmlPath = app(SifenXmlBuilder::class)->build($dte);

// 3. Guard: ¿tiene certificado?
$hasCertPath = !empty($emitter->cert_p12_path);
$hasPassword = !empty($emitter->cert_p12_password_enc);

if (!$hasCertPath || !$hasPassword) {
$dte->update(['estado' => 'draft']); // Modo desarrollo
return;
}

// 4. Verificar archivo físico antes de firmar
if (!file_exists($fullCertPath)) {
throw new \Exception("Certificado no encontrado"); // → Job falla, reintento
}

// 5. Firmar
app(SifenXmlDsigSigner::class)->sign($dte);
}

7.2 Gestión de Reintentos

El Job no define $tries ni $backoff explícitamente en este estado, por lo que hereda los defaults de Laravel Queue (tries=3, backoff exponencial). Para producción se recomienda:

// Patrón recomendado para OnnixConnect OC:
public int $tries = 5;
public int $maxExceptions = 3;
public array $backoff = [10, 30, 60, 120, 300]; // segundos

public function failed(\Throwable $e): void
{
SifenDte::where('id', $this->dteId)
->update([
'estado' => 'error',
'sifen_result_msg' => substr($e->getMessage(), 0, 500),
]);
// Notificar al emisor / admin
}

7.3 Rastreo de Reintentos en BD

La tabla sifen_dtes tiene columnas dedicadas:

retry_count UNSIGNED INT -- incrementar en cada intento
last_sent_at DATETIME -- timestamp del último envío
sifen_result_code VARCHAR(30) -- código DNIT (0300, 0422, etc.)
sifen_result_msg TEXT -- mensaje de error / aprobación

8. GESTIÓN DE ESTADO DEL DOCUMENTO (MÁQUINA DE ESTADOS)

┌─────────────────────────────────┐
│ ESTADOS │
└─────────────────────────────────┘

[CREADO EN BD]


'draft' ←── Inicio. XML no generado aún,
│ o sin certificado (modo dev)

▼ SifenXmlBuilder::build() [estado='draft' post-build]
'draft' ←── XML generado, sin firmar

▼ SifenXmlDsigSigner::sign()
'signed' ←── XML firmado listo para envío

├──► SifenSoapHttpClient::postSoap() [OK]
│ ▼
│ 'sent' ──────────────────► 'approved' (DNIT 0300)

└──► SifenSoapHttpClient::postSoap() [Error]

'error' ──► retry → vuelve a 'signed'

└─► 'rejected' (DNIT 0422, CDC duplicado, etc.)

9. CAPA DE VALIDACIÓN

9.1 SifenXmlValidator (Estructural)

Valida el DOM del XML generado mediante XPath. No usa XSD (validación local rápida):

// Registra namespace para poder usar XPath correcto:
$xp->registerNamespace('sifen', config('sifen.namespaces.sifen'));

// Nodos obligatorios mínimos:
'/sifen:rDE'
'/sifen:rDE/sifen:gTimb'
'/sifen:rDE/sifen:gDatGralOpe'
'/sifen:rDE/sifen:gEmis'
'/sifen:rDE/sifen:gDatRec'
'/sifen:rDE/sifen:gTotSub'

// Firma (warning si no está, no error → permite validar XML pre-firma)
'//ds:Signature/SignedInfo'
'//ds:Signature/SignatureValue'
'//ds:Signature/KeyInfo/X509Data/X509Certificate'

9.2 SifenTotalsValidator (Financiero)

Triple cruce de totales:

Items de BD ──┐
├── calculateFromItems() → totales calculados
DB (sifen_dtes)─┤ │
│ ├── diff < 0.01 → OK
XML (gTotSub) ─┘ extractTotalsFromXml() ───┘

Detecta discrepancias entre lo almacenado en BD, lo que dice el XML y lo que realmente suman los ítems.


10. PATRONES DE UI REACTIVA (LIVEWIRE / VOLT)

Nota: En el estado actual del proyecto analizado, los componentes Livewire de gestión de DTEs no están implementados en la capa de vistas (resources/views/livewire/). Los componentes existentes corresponden a autenticación y settings (Fortify/Jetstream). Los patrones a continuación documentan la arquitectura recomendada basada en la infraestructura existente.

10.1 Infraestructura Instalada

El proyecto usa Volt (Livewire v3 + componentes de archivo único) con Flux UI. El VoltServiceProvider está registrado, habilitando la sintaxis funcional con state(), mount(), action().

10.2 Patrón Recomendado: Tabla de DTEs con Polling

// resources/views/livewire/sifen/dte-list.blade.php
<?php
use function Livewire\Volt\{state, mount, action};
use App\Models\Sifen\SifenDte;

state([
'dtes' => [],
'selectedIds' => [],
'filterEstado' => 'all',
]);

mount(function () {
$this->loadDtes();
});

$loadDtes = action(function () {
$this->dtes = SifenDte::query()
->with('emitter')
->when($this->filterEstado !== 'all', fn($q) => $q->where('estado', $this->filterEstado))
->latest()
->paginate(50);
});

// Polling cada 5s para estado de Jobs en background:
// En la vista: <div wire:poll.5000ms="loadDtes">

10.3 Patrón de Comunicación Estado Backend → UI

TécnicaCaso de uso
wire:poll.{N}msMonitoreo de Jobs en background (DTE procesándose)
$dispatch('dte-updated', ['id' => $id])Notificación inmediata desde Job (via broadcasting)
Livewire EventsComunicación entre componentes hermanos (ej. filtros → tabla)

10.4 Patrón Bulk Actions (Acciones Masivas)

// Patrón estándar para reenvío masivo de documentos:
state([
'selectedIds' => [], // array de IDs seleccionados
'selectAll' => false,
]);

$toggleSelectAll = action(function () {
if ($this->selectAll) {
$this->selectedIds = SifenDte::where('estado', 'error')
->pluck('id')->toArray();
} else {
$this->selectedIds = [];
}
});

$bulkResend = action(function () {
foreach ($this->selectedIds as $dteId) {
// Re-despachar Job para cada DTE seleccionado
BuildAndSignDteJob::dispatch($dteId);
}
$this->selectedIds = [];
$this->dispatch('bulk-action-done', count: count($this->selectedIds));
});

10.5 Layout y Navegación

resources/views/components/layouts/app.blade.php ← Layout principal
├── app/header.blade.php ← Topbar con usuario
└── app/sidebar.blade.php ← Navegación lateral
← Usa Flux UI (flux:navlist, flux:icon)

11. ESQUEMA DE BASE DE DATOS

11.1 Tabla sifen_emitters (Multi-empresa)

CREATE TABLE sifen_emitters (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NULL, -- alias interno
ruc VARCHAR(20) NOT NULL,
dv VARCHAR(5) NULL,
razon_social VARCHAR(255) NOT NULL,
nombre_fantasia VARCHAR(255) NULL,
establecimiento VARCHAR(10) NULL, -- '001'
punto_expedicion VARCHAR(10) NULL, -- '001'
ambiente ENUM('test','prod') DEFAULT 'test',
cert_p12_path VARCHAR(255) NULL, -- storage path relativo
cert_p12_password_enc TEXT NULL, -- AES-256 cifrado (Crypt)
config JSON NULL, -- endpoints, CSC, etc.
active TINYINT(1) DEFAULT 1,
created_at TIMESTAMP,
updated_at TIMESTAMP,
deleted_at TIMESTAMP NULL, -- softDeletes

INDEX idx_ruc_dv (ruc, dv)
);

Decisiones de diseño:

  • cert_p12_path apunta al storage (no BLOB en BD → archivos grandes fuera de DB).
  • cert_p12_password_enc usa Laravel\Crypt (AES-256-CBC con APP_KEY).
  • config JSON permite agregar campos del emisor sin nuevas migraciones (CSC, IdCSC, etc.).
  • softDeletes permite desactivar emisores sin perder el historial de DTEs.
  • Índice compuesto (ruc, dv) para búsquedas por identificación fiscal.

11.2 Tabla sifen_dtes (Documentos Tributarios)

CREATE TABLE sifen_dtes (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
sifen_emitter_id BIGINT UNSIGNED NOT NULL,
tipo_documento VARCHAR(10), -- FE, NCE, NDE, REM
serie VARCHAR(20) NULL,
numero BIGINT UNSIGNED NULL,
fecha_emision DATETIME NULL,
condicion_venta TINYINT UNSIGNED DEFAULT 1, -- 1=contado, 2=crédito
moneda VARCHAR(3) DEFAULT 'PYG',

-- Cliente
cliente_documento VARCHAR(30) NULL, -- RUC "80001-6" o CI "1234567"
cliente_nombre VARCHAR(255) NULL,
cliente_email VARCHAR(255) NULL,
cliente_direccion VARCHAR(255) NULL,

-- Totales pre-calculados (desnormalización intencional)
gravada_10 DECIMAL(18,2) DEFAULT 0,
gravada_5 DECIMAL(18,2) DEFAULT 0,
exenta DECIMAL(18,2) DEFAULT 0,
iva_10 DECIMAL(18,2) DEFAULT 0,
iva_5 DECIMAL(18,2) DEFAULT 0,
total DECIMAL(18,2) DEFAULT 0,

-- Archivos (rutas relativas a storage/app/)
xml_unsigned_path VARCHAR(255) NULL,
xml_signed_path VARCHAR(255) NULL,
kude_pdf_path VARCHAR(255) NULL,

-- SIFEN
cdc VARCHAR(80) NULL,
estado VARCHAR(30) DEFAULT 'draft',
sifen_result_code VARCHAR(30) NULL,
sifen_result_msg TEXT NULL,

-- Reintentos
retry_count INT UNSIGNED DEFAULT 0,
last_sent_at DATETIME NULL,

created_at TIMESTAMP,
updated_at TIMESTAMP,

INDEX idx_estado (estado),
INDEX idx_cdc (cdc),
UNIQUE uq_sifen_doc_nro (sifen_emitter_id, tipo_documento, serie, numero),
FOREIGN KEY (sifen_emitter_id) REFERENCES sifen_emitters(id)
);

Decisiones de diseño (para +50.000 registros):

DecisiónJustificación
INDEX idx_estadoEl patrón más frecuente es filtrar por estado='error' para reenvíos
INDEX idx_cdcEl CDC es el identificador de consulta en DNIT (único, 44 chars)
UNIQUE (emitter, tipo, serie, numero)Previene duplicados a nivel de base de datos, no solo aplicación
DECIMAL(18,2) en totalesEvita errores de punto flotante en montos fiscales
BIGINT UNSIGNED en númeroPermite millones de documentos por emisor sin overflow
Totales desnormalizadosEvita recalcular en cada consulta de listados masivos
Rutas de archivo (no BLOB)Los XML pueden llegar a 50KB+; mejor en filesystem/S3

11.3 Tabla sifen_dte_items

CREATE TABLE sifen_dte_items (
id BIGINT UNSIGNED PRIMARY KEY,
sifen_dte_id BIGINT UNSIGNED NOT NULL,
codigo VARCHAR(50) NULL,
descripcion VARCHAR(255) NOT NULL,
cantidad DECIMAL(18,6) DEFAULT 1, -- 6 decimales para fracciones
precio_unitario DECIMAL(18,2) DEFAULT 0,
descuento DECIMAL(18,2) DEFAULT 0,
base_iva DECIMAL(18,2) DEFAULT 0,
tasa_iva TINYINT UNSIGNED DEFAULT 10, -- 10/5/0 (tinyint = ahorro)
iva_monto DECIMAL(18,2) DEFAULT 0,
total_linea DECIMAL(18,2) DEFAULT 0,

FOREIGN KEY (sifen_dte_id) REFERENCES sifen_dtes(id) ON DELETE CASCADE
);

Nota: tasa_iva usa TINYINT UNSIGNED (1 byte) en lugar de INT (4 bytes). Con millones de ítems, el ahorro de espacio mejora el uso de caché del motor.

11.4 Tabla sifen_logs

CREATE TABLE sifen_logs (
id BIGINT UNSIGNED PRIMARY KEY,
sifen_dte_id BIGINT UNSIGNED NULL, -- NULL si es log de sistema
tipo VARCHAR(30), -- sign/send/response/exception
request_path VARCHAR(255) NULL, -- path al XML de request guardado
response_path VARCHAR(255) NULL, -- path a la respuesta SOAP
payload_small JSON NULL, -- resumen (no el XML completo)

INDEX idx_tipo (tipo),
FOREIGN KEY (sifen_dte_id) REFERENCES sifen_dtes(id) ON DELETE SET NULL
);

12. CONFIGURACIÓN CENTRALIZADA (config/sifen.php)

return [
'version' => '150',
'environment' => env('SIFEN_ENV', 'test'),
'ssl_verify' => filter_var(env('SIFEN_SSL_VERIFY', true), FILTER_VALIDATE_BOOLEAN),

'endpoints' => [
'test' => [
'sync' => 'https://sifen-test.set.gov.py/de/ws/sync/recibe',
'async' => 'https://sifen-test.set.gov.py/de/ws/async/recibe',
'evento' => 'https://sifen-test.set.gov.py/de/ws/evento/recibe',
'consulta' => 'https://sifen-test.set.gov.py/de/ws/consulta/recibe',
],
'prod' => [
// misma estructura, URLs de producción
],
],

'namespaces' => [
'sifen' => 'http://ekuatia.set.gov.py/sifen/xsd', // ← NUNCA CAMBIA
'xmldsig' => 'http://www.w3.org/2000/09/xmldsig#',
'soap12' => 'http://www.w3.org/2003/05/soap-envelope',
],

'paths' => [
'xml' => storage_path('app/sifen/xml'),
'certs' => storage_path('app/sifen/certs'),
'responses' => storage_path('app/sifen/responses'),
'kude' => storage_path('app/sifen/kude'),
],
];

Patrón de acceso: Siempre via config('sifen.xxx'), nunca hardcodeado en clases del dominio.


13. PATRONES REUTILIZABLES PARA ONNIXCONNECT OC

13.1 Checklist de Adaptación

  • DTO: Extender DteData con campos adicionales de OC (e.g., $orderId, $clienteEmail).
  • Multi-tenant: El patrón SifenEmitter es multi-empresa. En OC agregar columna tenant_id con scoping global via GlobalScope de Eloquent.
  • Job: Agregar $tries, $backoff[] y método failed() al Job.
  • Estado: Implementar la máquina de estados con spatie/laravel-model-states para transiciones auditadas.
  • QR: Siempre llamar SifenQrService después de firmar antes de enviar a DNIT.
  • Logs: Guardar request/response SOAP en filesystem con path en sifen_logs (no en la columna directamente).
  • Cert cache: El directorio pem/emitter_{id} debe estar en .gitignore y fuera de backups públicos.

13.2 Orden Canónico de Operaciones

1. Crear SifenEmitter (tenant) → con P12 + password cifrado
2. Crear SifenDte + SifenDteItems → estado='draft'
3. dispatch(BuildAndSignDteJob::class) → estado='signed'
4. SifenQrService::updateSignedXmlQr() → QR inyectado en XML firmado
5. SifenSoapEnvelopeBuilder::build() → sobre SOAP 1.2
6. SifenSoapHttpClient::postSoap() → respuesta DNIT
7. Parsear respuesta → actualizar estado → guardar en sifen_logs

13.3 Variables de Entorno Requeridas

APP_KEY=base64:... # Necesaria para Crypt (passwords de .p12)
SIFEN_ENV=test # test | prod
SIFEN_SSL_VERIFY=true # false solo en desarrollo local
QUEUE_CONNECTION=database # o redis para producción

13.4 Dependencias Composer Críticas

{
"require": {
"robrichards/xmlseclibs": "^3.1"
}
}

Documento generado por análisis estático del código fuente. Refleja el estado del proyecto a 2026-03-03.