Base de Datos
Ventajas:
- Búsqueda rápida
- Respaldos automáticos
- Control de versiones
Consideraciones:
- Comprimir con gzip
- Usar MEDIUMBLOB/LONGBLOB
- Indexar por número, CUNE, fecha
El método downloadXML permite descargar el documento XML de nómina electrónica firmado digitalmente por la DIAN. Este XML:
| Parámetro | Tipo | Obligatorio | Descripción |
|---|---|---|---|
username | string | OK | Usuario de acceso al servicio |
password | string | OK | Contraseña encriptada en SHA256 |
prefijo | string | OK | Prefijo del documento (ej: “NOM”) |
folio | string | OK | Consecutivo/folio del documento (ej: “35921”) |
<?xml version="1.0" encoding="UTF-8"?><x:Envelope xmlns:x="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:dem="urn:https://ws-nomina.facturatech.co/v1/demo/"> <x:Header/> <x:Body> <dem:FtechAction.downloadXML x:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"> <username xsi:type="xsd:string">DATAEM19112025</username> <password xsi:type="xsd:string">f8c0e8471f126c77bd23f664f3ce251b8f9943f1d95a6ffdda72f8e6b76c9b93</password> <prefijo xsi:type="xsd:string">NOM</prefijo> <folio xsi:type="xsd:string">35921</folio> </dem:FtechAction.downloadXML> </x:Body></x:Envelope><?php$wsdl = 'https://ws-nomina.facturatech.co/v1/demo/index.php?wsdl';
$client = new SoapClient($wsdl, [ 'trace' => 1, 'exceptions' => true, 'encoding' => 'UTF-8', 'soap_version' => SOAP_1_1]);
$params = [ 'username' => 'DATAEM19112025', 'password' => 'f8c0e8471f126c77bd23f664f3ce251b8f9943f1d95a6ffdda72f8e6b76c9b93', 'prefijo' => 'NOM', 'folio' => '35921'];
try { $response = $client->__soapCall('FtechAction.downloadXML', [$params]);
if ($response->codigo == 200) { // Decodificar Base64 $xmlContent = base64_decode($response->documentoBase64);
// Guardar archivo $filename = "nomina_{$params['prefijo']}{$params['folio']}_firmado.xml"; file_put_contents($filename, $xmlContent);
echo " XML descargado: $filename\n"; echo "Tamaño: " . strlen($xmlContent) . " bytes\n"; } else { echo " Error: " . $response->mensaje . "\n"; }
} catch (SoapFault $e) { echo "Error SOAP: " . $e->getMessage() . "\n";}<?phpfunction descargarYGuardarXML($prefijo, $folio){ $soapClient = new Dataemunah_Facturatech_SoapClient(); $db = Zend_Db_Table::getDefaultAdapter();
// Descargar XML $response = $soapClient->downloadXML($prefijo, $folio);
if (!$response['success']) { throw new Exception("Error al descargar XML: " . $response['mensaje']); }
// Decodificar $xmlFirmado = base64_decode($response['documentoBase64']);
// Comprimir para ahorrar espacio (opcional) $xmlComprimido = gzcompress($xmlFirmado, 9);
// Actualizar en BD $numero = $prefijo . $folio; $db->update('nomina_electronica', [ 'xml_firmado' => $xmlComprimido, 'xml_firmado_hash' => hash('sha256', $xmlFirmado), 'xml_firmado_tamano' => strlen($xmlFirmado), 'fecha_descarga' => new Zend_Db_Expr('NOW()'), 'estado' => 'DESCARGADO' ], "numero = '$numero'");
Zend_Log::info("XML firmado descargado y almacenado: $numero");
return [ 'xml_content' => $xmlFirmado, 'tamano' => strlen($xmlFirmado), 'hash' => hash('sha256', $xmlFirmado) ];}<downloadXMLResponse> <codigo>200</codigo> <documentoBase64>PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPE5vbWlu...</documentoBase64> <mensaje>XML descargado correctamente</mensaje> <mensajeError></mensajeError></downloadXMLResponse>| Campo | Tipo | Descripción |
|---|---|---|
codigo | int | Código de estado HTTP |
documentoBase64 | string | XML firmado codificado en Base64 |
mensaje | string | Descripción del resultado |
mensajeError | string | Detalle del error (si aplica) |
El XML descargado incluye elementos adicionales comparado con el XML original:
<ext:UBLExtensions> <ext:UBLExtension> <ext:ExtensionContent> <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"> <ds:SignedInfo> <ds:CanonicalizationMethod Algorithm="..."/> <ds:SignatureMethod Algorithm="..."/> <ds:Reference URI=""> <ds:DigestMethod Algorithm="..."/> <ds:DigestValue>...</ds:DigestValue> </ds:Reference> </ds:SignedInfo> <ds:SignatureValue>...</ds:SignatureValue> <ds:KeyInfo>...</ds:KeyInfo> </ds:Signature> </ext:ExtensionContent> </ext:UBLExtension></ext:UBLExtensions><InformacionGeneral Version="V1.0: Documento Soporte de Pago de Nómina Electrónica" Ambiente="2" TipoXML="102" FechaGen="2024-11-20" HoraGen="15:13:46-05:00" CUNE="a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6" EncripCUNE="SHA384" PeriodoNomina="5" TipoMoneda="COP" TRM="1"/><ProveedorXML RazonSocial="FACTURATECH SAS" NIT="900123456" DV="7" SoftwareID="software-id-facturatech" SoftwareSC="security-code-123"/>| Elemento | XML Original | XML Firmado |
|---|---|---|
| ext:UBLExtensions | Vacío | Con firma digital completa |
| CUNE | Vacío | Hash SHA384 generado |
| EncripCUNE | Vacío | ”SHA384” |
| ProveedorXML | No incluido | Datos de Facturatech |
| CodigoQR | Generado por ERP | Validado/actualizado |
| Tamaño | ~15-20 KB | ~45-60 KB |
<?phpfunction validarXMLFirmado($xmlContent){ $xml = new DOMDocument(); $xml->loadXML($xmlContent);
// 1. Verificar que tiene firma $firmas = $xml->getElementsByTagNameNS( 'http://www.w3.org/2000/09/xmldsig#', 'Signature' );
if ($firmas->length === 0) { throw new Exception('XML no contiene firma digital'); }
// 2. Verificar que tiene CUNE $infoGeneral = $xml->getElementsByTagName('InformacionGeneral')->item(0); $cune = $infoGeneral->getAttribute('CUNE');
if (empty($cune)) { throw new Exception('XML no contiene CUNE'); }
// 3. Verificar formato CUNE (64 caracteres hexadecimales) if (!preg_match('/^[a-f0-9]{64}$/i', $cune)) { throw new Exception('Formato de CUNE inválido'); }
// 4. Verificar proveedor $proveedor = $xml->getElementsByTagName('ProveedorXML')->item(0); if (!$proveedor) { throw new Exception('XML no contiene información del proveedor'); }
return [ 'valido' => true, 'cune' => $cune, 'proveedor' => $proveedor->getAttribute('RazonSocial'), 'fecha_firma' => $infoGeneral->getAttribute('FechaGen') ];}Base de Datos
Ventajas:
Consideraciones:
Sistema de Archivos
Ventajas:
Consideraciones:
Object Storage
Ventajas:
Consideraciones:
Enfoque Híbrido
Recomendado:
Beneficios:
<?phpclass Nomina_Storage_XMLManager{ private $baseDir = '/var/www/dataemunah/storage/nominas'; private $db;
public function __construct() { $this->db = Zend_Db_Table::getDefaultAdapter(); }
/** * Almacena XML firmado en sistema de archivos y metadata en BD */ public function almacenarXMLFirmado($numero, $xmlContent, $cune) { // 1. Generar ruta por fecha $fecha = date('Y-m'); $dir = "{$this->baseDir}/{$fecha}";
if (!is_dir($dir)) { mkdir($dir, 0755, true); }
// 2. Comprimir XML $xmlComprimido = gzcompress($xmlContent, 9);
// 3. Guardar archivo $filename = "{$numero}_firmado.xml.gz"; $filepath = "{$dir}/{$filename}";
if (file_put_contents($filepath, $xmlComprimido) === false) { throw new Exception("Error al guardar archivo XML"); }
// 4. Calcular hash para verificación de integridad $hash = hash('sha256', $xmlContent);
// 5. Actualizar metadata en BD $this->db->update('nomina_electronica', [ 'xml_firmado_path' => $filepath, 'xml_firmado_hash' => $hash, 'xml_firmado_tamano' => strlen($xmlContent), 'xml_firmado_comprimido_tamano' => strlen($xmlComprimido), 'cune' => $cune, 'fecha_descarga_xml' => new Zend_Db_Expr('NOW()'), 'estado' => 'XML_DESCARGADO' ], "numero = " . $this->db->quote($numero));
// 6. Log Zend_Log::info("XML firmado almacenado", [ 'numero' => $numero, 'path' => $filepath, 'tamano_original' => strlen($xmlContent), 'tamano_comprimido' => strlen($xmlComprimido), 'ratio_compresion' => round((1 - strlen($xmlComprimido) / strlen($xmlContent)) * 100, 2) . '%' ]);
return [ 'path' => $filepath, 'hash' => $hash, 'tamano' => strlen($xmlContent) ]; }
/** * Recupera XML firmado desde storage */ public function recuperarXMLFirmado($numero) { // 1. Obtener path desde BD $row = $this->db->fetchRow( "SELECT xml_firmado_path, xml_firmado_hash FROM nomina_electronica WHERE numero = ?", [$numero] );
if (!$row) { throw new Exception("Nómina no encontrada: $numero"); }
$filepath = $row['xml_firmado_path'];
if (!file_exists($filepath)) { throw new Exception("Archivo XML no encontrado: $filepath"); }
// 2. Leer y descomprimir $xmlComprimido = file_get_contents($filepath); $xmlContent = gzuncompress($xmlComprimido);
// 3. Verificar integridad $hashCalculado = hash('sha256', $xmlContent); if ($hashCalculado !== $row['xml_firmado_hash']) { Zend_Log::error("Hash de XML no coincide para nómina $numero"); throw new Exception("Integridad del XML comprometida"); }
return $xmlContent; }
/** * Genera archivo XML para descarga web */ public function generarDescarga($numero, $outputFilename = null) { $xmlContent = $this->recuperarXMLFirmado($numero);
if ($outputFilename === null) { $outputFilename = "{$numero}_nomina_electronica.xml"; }
header('Content-Type: application/xml; charset=utf-8'); header('Content-Disposition: attachment; filename="' . $outputFilename . '"'); header('Content-Length: ' . strlen($xmlContent)); header('Cache-Control: no-cache, must-revalidate'); header('Pragma: no-cache');
echo $xmlContent; exit; }}<?phpfunction verificarPermisoDescarga($usuarioId, $nominaId){ $db = Zend_Db_Table::getDefaultAdapter();
// Verificar que el usuario tenga acceso a esta nómina $acceso = $db->fetchOne(" SELECT COUNT(*) FROM nomina_electronica ne INNER JOIN empleados e ON ne.empleado_id = e.id INNER JOIN usuarios_empresas ue ON e.empresa_id = ue.empresa_id WHERE ne.id = ? AND ue.usuario_id = ? AND ue.puede_ver_nominas = 1 ", [$nominaId, $usuarioId]);
if ($acceso == 0) { throw new Exception("Usuario no autorizado para ver esta nómina"); }
// Log de acceso $db->insert('nomina_accesos_log', [ 'nomina_id' => $nominaId, 'usuario_id' => $usuarioId, 'accion' => 'DESCARGA_XML', 'ip' => $_SERVER['REMOTE_ADDR'], 'user_agent' => $_SERVER['HTTP_USER_AGENT'], 'fecha' => new Zend_Db_Expr('NOW()') ]);
return true;}<?phpclass Nomina_Security_XMLEncryptor{ private $encryptionKey; private $cipher = 'aes-256-gcm';
public function __construct() { // Obtener key desde configuración segura $config = Zend_Registry::get('config'); $this->encryptionKey = $config->security->xml_encryption_key; }
public function encrypt($xmlContent) { $ivlen = openssl_cipher_iv_length($this->cipher); $iv = openssl_random_pseudo_bytes($ivlen); $tag = null;
$ciphertext = openssl_encrypt( $xmlContent, $this->cipher, $this->encryptionKey, OPENSSL_RAW_DATA, $iv, $tag );
// Empaquetar iv + tag + ciphertext return base64_encode($iv . $tag . $ciphertext); }
public function decrypt($encryptedData) { $data = base64_decode($encryptedData);
$ivlen = openssl_cipher_iv_length($this->cipher); $taglen = 16;
$iv = substr($data, 0, $ivlen); $tag = substr($data, $ivlen, $taglen); $ciphertext = substr($data, $ivlen + $taglen);
return openssl_decrypt( $ciphertext, $this->cipher, $this->encryptionKey, OPENSSL_RAW_DATA, $iv, $tag ); }}CREATE TABLE nomina_xml_descargas ( id INT PRIMARY KEY AUTO_INCREMENT, nomina_id INT NOT NULL, usuario_id INT NOT NULL, tipo_descarga ENUM('XML', 'PDF', 'CUNE') NOT NULL, ip_address VARCHAR(45), user_agent TEXT, fecha_descarga TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_nomina (nomina_id), INDEX idx_usuario (usuario_id), INDEX idx_fecha (fecha_descarga),
FOREIGN KEY (nomina_id) REFERENCES nomina_electronica(id), FOREIGN KEY (usuario_id) REFERENCES usuarios(id)) ENGINE=InnoDB;// Registrar cada descargafunction registrarDescarga($nominaId, $usuarioId, $tipo = 'XML'){ $db = Zend_Db_Table::getDefaultAdapter();
$db->insert('nomina_xml_descargas', [ 'nomina_id' => $nominaId, 'usuario_id' => $usuarioId, 'tipo_descarga' => $tipo, 'ip_address' => $_SERVER['REMOTE_ADDR'], 'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? null ]);}<?phpclass Nomina_Validator_XMLFirmado{ public function validar($xmlContent) { $errores = [];
// 1. Validar que sea XML válido libxml_use_internal_errors(true); $xml = simplexml_load_string($xmlContent);
if ($xml === false) { foreach (libxml_get_errors() as $error) { $errores[] = "XML mal formado: " . $error->message; } return ['valido' => false, 'errores' => $errores]; }
// 2. Verificar namespace correcto $namespaces = $xml->getNamespaces(true); if (!isset($namespaces[''])) { $errores[] = "Namespace DIAN no encontrado"; }
// 3. Verificar firma digital $ext = $xml->children('ext', true); if (!isset($ext->UBLExtensions)) { $errores[] = "Firma digital no encontrada"; }
// 4. Verificar CUNE $infoGeneral = $xml->InformacionGeneral; $cune = (string)$infoGeneral['CUNE'];
if (empty($cune)) { $errores[] = "CUNE no encontrado"; } elseif (!preg_match('/^[a-f0-9]{64}$/i', $cune)) { $errores[] = "Formato de CUNE inválido"; }
// 5. Verificar proveedor if (!isset($xml->ProveedorXML)) { $errores[] = "Información del proveedor no encontrada"; }
// 6. Verificar coherencia de datos $devengadosTotal = (float)$xml->DevengadosTotal; $deduccionesTotal = (float)$xml->DeduccionesTotal; $comprobanteTotal = (float)$xml->ComprobanteTotal;
$totalCalculado = $devengadosTotal - $deduccionesTotal; $diferencia = abs($totalCalculado - $comprobanteTotal);
if ($diferencia > 2.0) { // Tolerancia de ±2.00 $errores[] = "Totales no cuadran (diferencia: $diferencia)"; }
return [ 'valido' => empty($errores), 'errores' => $errores, 'cune' => $cune ?? null, 'numero' => (string)$xml->NumeroSecuenciaXML['Numero'] ?? null ]; }}Después de descargar el XML firmado:
downloadPDF - Obtener representación gráficadownloadCUNE - Confirmar CUNE