SIFEN_MODERN_ARCHITECTURE.md
Arquitectura de referencia extraída de
sifen_laravel_multiempresapara OnnixConnect OC. Fecha de análisis: 2026-03-03 | Stack: Laravel 12, DDD, Livewire v3 / Volt
ÍNDICE
- Estructura de Dominios
- Flujo de Datos: DteData → XML Firmado
- Lógica de Construcción XML Dinámica
- Firma XMLDSig (Enveloped)
- Capa SOAP: Envoltura y Transmisión HTTP
- Generación del QR (Post-Firma)
- Procesamiento Asíncrono: BuildAndSignDteJob
- Gestión de Estado del Documento (Máquina de Estados)
- Capa de Validación
- Patrones de UI Reactiva (Livewire/Volt)
- Esquema de Base de Datos
- Configuración Centralizada (config/sifen.php)
- 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
| Capa | Responsabilidad |
|---|---|
| DTO | Transporta datos crudos sin lógica. Inmutable (constructor PHP 8). |
| Xml | Traduce el estado del modelo Eloquent a nodos DOM. |
| Services | Algoritmos especializados: firma, QR, código de seguridad. |
| Soap | Protocolo de transporte hacia DNIT (SOAP 1.2 sin WSDL). |
| Validation | Verificación post-construcción: estructura + totales. |
| Jobs | Orquesta 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
| Prefijo | URI | Propósito |
|---|---|---|
| (default) | http://ekuatia.set.gov.py/sifen/xsd | Todos los nodos del DTE |
xmlns:xsi | http://www.w3.org/2001/XMLSchema-instance | xsi:schemaLocation |
xmlns:ds | http://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)
| Signer | Tecnología | Cuándo usar |
|---|---|---|
SifenXmlDsigSigner | xmlseclibs PHP puro | Producción (recomendado) |
SifenSigner | openssl CLI + xmlsec1 | Desarrollo/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:
mode | Técnica | Uso |
|---|---|---|
text | htmlspecialchars(ENT_XML1) | dId, strings simples |
cdata | <![CDATA[...]]> | xDE (XML literal del DTE) |
El CDATA se sanea:
- Se elimina el BOM UTF-8 (
\xEF\xBB\xBF). - Se elimina la declaración
<?xml ...?>. - 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ón | Endpoint | Descripción |
|---|---|---|
rEnviDe | sync/recibe | Envío síncrono (respuesta inmediata) |
rEnviDe | async/recibe | Envío asíncrono (se consulta después) |
| (eventos) | evento/recibe | Cancelación, inutilización, conformidad |
| (consulta) | consulta/recibe | Consulta 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écnica | Caso de uso |
|---|---|
wire:poll.{N}ms | Monitoreo de Jobs en background (DTE procesándose) |
$dispatch('dte-updated', ['id' => $id]) | Notificación inmediata desde Job (via broadcasting) |
| Livewire Events | Comunicació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_pathapunta al storage (no BLOB en BD → archivos grandes fuera de DB).cert_p12_password_encusaLaravel\Crypt(AES-256-CBC con APP_KEY).config JSONpermite agregar campos del emisor sin nuevas migraciones (CSC, IdCSC, etc.).softDeletespermite 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ón | Justificación |
|---|---|
INDEX idx_estado | El patrón más frecuente es filtrar por estado='error' para reenvíos |
INDEX idx_cdc | El 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 totales | Evita errores de punto flotante en montos fiscales |
BIGINT UNSIGNED en número | Permite millones de documentos por emisor sin overflow |
| Totales desnormalizados | Evita 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
DteDatacon campos adicionales de OC (e.g.,$orderId,$clienteEmail). - Multi-tenant: El patrón
SifenEmitteres multi-empresa. En OC agregar columnatenant_idcon scoping global viaGlobalScopede Eloquent. - Job: Agregar
$tries,$backoff[]y métodofailed()al Job. - Estado: Implementar la máquina de estados con
spatie/laravel-model-statespara transiciones auditadas. - QR: Siempre llamar
SifenQrServicedespué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.gitignorey 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.