<?php
    /**
     *  wsInvoiceSFJ_VP_v40_RR_1.PHP
     * 
     *  Autor: Felipe Faccinetto
     *  Fecha: Enero/2023
     * 
     *  DESCRIPCIÓN:
     * 
     *  Valida, crea y timbra un CFDI . 
     * 
     *  PARAMETROS:
     * 
     *  pDatPAC         Cadena codificada en base64 que contiene un arreglo JSON con los datos del PAC
     *  pDatGen         Cadena codificada en base64 que contiene un arreglo JSON con los datos generales del CFDI 
     *  pDatEmi         Cadena codificada en base64 que contiene un arreglo JSON con los datos del emisor del CFDI
     *  pDatRec         Cadena codificada en base64 que contiene un arreglo JSON con los datos del receptor del CFDI
     *  pDatArt         Cadena codificada en base64 que contiene un arreglo JSON con los datos de los articulos del CFDI
	 *  pRecordId		Identidicador del registro a actualizar en Zoho Creator
	 *  pFormatoPDF		Nombre del formato que se utilizara para generar el PDF
     *  pDatRel         Cadena codificada en base64 que contiene un arreglo JSON con los datos de los CFDI Relacionados
     *  pTipPDFGen      Especifica si se utiliza un formato en especial o no, 0 = No, 1 = Arochi
     *  pAppOwner       Especifica el propietario de la aplicación que se actualizara.
     * 
    */
    
    header('Content-Type: text/html; charset=UTF-8');
    require('oauth.php');
    $path = __DIR__ . '/../aptus_core/services/oauth_v2.service.php';

if (!file_exists($path)) {
    die("Archivo no encontrado: $path");
}

require_once $path;
echo "Archivo cargado correctamente.";
    include("qrlib/qrlib.php");

    $bDatPAC 	 = "";
    $bDatGen 	 = "";
    $bDatEmi 	 = "";
    $bDatRec 	 = "";
    $bDatArt 	 = "";
    $bDatRel 	 = "";
 	 $bRecId  	 = "";
	 $FormatoPDF = "wsInvoicePDF_v40";
    $applnkname = 'cfdi'; 
    $appOwner   = "";
    $TipPDFGen  = 0;

    ## Cargamos los parametros enviados al WebService
    if (isset($_REQUEST['pDatPAC']))
    {
        $bDatPAC = $_REQUEST['pDatPAC'];        // Parametros para la conexión con el PAC
    }

    if (isset($_REQUEST['pDatGen']))
    {
        $bDatGen = $_REQUEST['pDatGen'];        // Datos Generales del CFDI
    }

    if (isset($_REQUEST['pDatEmi']))
    {
        $bDatEmi = $_REQUEST['pDatEmi'];        // Datos del Emisor del CFDI
    }

    if (isset($_REQUEST['pDatRec']))
    {
        $bDatRec = $_REQUEST['pDatRec'];        // Datos del Receptor del CFDI
    }

    if (isset($_REQUEST['pRecordId']))
    {
        $bRecId = $_REQUEST['pRecordId'];        // Identidicador del registro
    }

    if (isset($_REQUEST['pFormatoPDF']))
    {
        $FormatoPDF = $_REQUEST['pFormatoPDF']; // Nombre del formato para el PDF
    }

    if (isset($_REQUEST['pDatRel']))
    {
        $bDatRel = $_REQUEST['pDatRel'];     	// Datos de los CFDI ReElacionados
    }

    if (isset($_REQUEST['pAppLnkNam']))
    {
        $applnkname = $_REQUEST['pAppLnkNam'];     	// Identificador de la aplicación en Zoho Creator
    }
	 
    if (isset($_REQUEST['pTipPDFGen']))
    {
        $TipPDFGen = $_REQUEST['pTipPDFGen'];        // Parametros para el tipo de formato
    }

    if (isset($_REQUEST['pAppOwner']))
    {
        $appOwner = $_REQUEST['pAppOwner'];        // Parametros para el propietario de la aplicación
    }

    if($appOwner == "")
    {
		$j_array = array('code' => "300", "message" => "ERROR - [wsInvoiceSF] Parametros incompletos para el Servicio Web, faltan el Propoietario de la Aplicación");
		$Resultado = json_encode($j_array);
		echo $Resultado;
        return;
    }

    if($bRecId == "")
    {
		$j_array = array('code' => "300", "message" => "ERROR - [wsInvoiceSF] Parametros incompletos para el Servicio Web, faltan el ID del CFDI a actualizar");
		$Resultado = json_encode($j_array);
		echo $Resultado;
        return;
    }

    if($bDatPAC == "")
    {
		$j_array = array('code' => "300", "message" => "ERROR - [wsInvoiceSF] Parametros incompletos para el Servicio Web, faltan los Datos del PAC");
		$Resultado = json_encode($j_array);
		echo $Resultado;
        return;
    }

    if($bDatGen == "")
    {
		$j_array = array('code' => "300", "message" => "ERROR - [wsInvoiceSF] Parametros incompletos para el Servicio Web, faltan los Datos Generales");
		$Resultado = json_encode($j_array);
		echo $Resultado;
        return;
    }

    if($bDatEmi == "")
    {
		$j_array = array('code' => "300", "message" => "ERROR - [wsInvoiceSF] Parametros incompletos para el Servicio Web, faltan los Datos del Emisor");
		$Resultado = json_encode($j_array);
		echo $Resultado;
        return;
    }

    if($bDatRec == "")
    {
		$j_array = array('code' => "300", "message" => "ERROR - [wsInvoiceSF] Parametros incompletos para el Servicio Web, faltan los Datos del Receptor");
		$Resultado = json_encode($j_array);
		echo $Resultado;
        return;
    }

    ### 0. EXTRACCION DE PARAMETROS PARA EL CFDI ######################################################
    #== Primero, extraemos el JSON del string en base 64
    $bdDatPAC = base64_decode($bDatPAC);
    $bdDatGen = base64_decode($bDatGen);
    $bdDatEmi = base64_decode($bDatEmi);
    $bdDatRec = base64_decode($bDatRec);

	if($bDatRel <> "")
	{
		$bdDatRel = base64_decode($bDatRel);
	}

    #== Segundo, decodificamos el JSON a un arreglo
    $abdDatPAC = json_decode($bdDatPAC,true);
    $abdDatGen = json_decode($bdDatGen,true);
    $abdDatEmi = json_decode($bdDatEmi,true);
    $abdDatRec = json_decode($bdDatRec,true);

	if($bDatRel <> "")
	{
		$abdDatRel = json_decode($bdDatRel,true);
	}
	
	$dirBase = realpath("../");

    ### CÓDIGO FUENTE, FACTURACIÓN ELECTRÓNICA CFDI VERSIÓN 3.2 ACORDE A LOS REQUIRIMIENTOS DEL SAT, ANEXO 20.

    ### 1. CONFIGURACIÓN INICIAL ######################################################

    # 1.1 Configuración de zona horaria
    date_default_timezone_set('America/Mexico_City'); // 

    # 1.2 Muestra la zona horaria predeterminada del servidor (opcional a mostrar)

    ### 2. DEFINICIÓN DE CONSTANTES ###################################################
    $SendaPEMS  = "archs_pem/";   
    $SendaCFDI  = "archs_cfdi/";  
    $SendaGRAFS = "archs_graf/";  
    $SendaXSD   = "archs_xsd/";   
    
    // 2.1 Datos de acceso del usuario (proporcionados por el PAC).
    if($abdDatPAC["tipoTim"] == 1){
        ## Timbrado en producción
        $urlPAC   = "https://solucionfactible.com/ws/services/Timbrado";
    }
    else
    {
        ## Timbrado en pruebas
        $urlPAC   = "https://testing.solucionfactible.com/ws/services/Timbrado?wsdl";
    }
    
    $username = $abdDatPAC["username"];
    $password = $abdDatPAC["password"];

    $valorNod      = '';
    $metodoDePago  = "";
    
    ### 3. DEFINICIÓN DE VARIABLES INICIALES ##########################################

   $noCertificado = $abdDatGen["Certificado_SAT"];
   $file_cer      = $abdDatGen["Archivo_CER"];
   $file_key      = $abdDatGen["Archivo_KEY"];
	
	$organi_id_ZB  = $abdDatGen["organization_id_ZB"];
	$authtoken_ZB  = $abdDatGen["authtoken_ZB"];
	$authtoken_ZC  = $abdDatGen["authtoken_ZC"];


    #== Datos y Variables para OAuth Token
	$coa_ClientId 		= $abdDatGen["C_OAuth_client_id"];
	$coa_ClientSecret = $abdDatGen["C_OAuth_client_secret"];
	$coa_RefreshToken = $abdDatGen["C_OAuth_refresh_token"];
    $coa_GrantType    = $abdDatGen["C_OAuth_grant_type"];
    $coa_RedirectUri  = $abdDatGen["C_OAuth_redirect_uri"];
    $coa_AuthUrl 		= "https://accounts.zoho.com/oauth/v2/token";

    #----------------------------------------------------------------
  	# JFA: 2021-06-12
	# Obtenemos el access_token
  	#----------------------------------------------------------------
    #----------------------------------------------------------------
        #== Llamado para generar el token para donatello
        #----------------------------------------------------------------
        $curl = curl_init();

        curl_setopt_array($curl, array(
            CURLOPT_URL => 'https://donatello.aptuslegal.app/oauth/token/'.$appOwner,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_ENCODING => '',
            CURLOPT_MAXREDIRS => 10,
            CURLOPT_TIMEOUT => 0,
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
            CURLOPT_CUSTOMREQUEST => 'GET',
            CURLOPT_HTTPHEADER => array(
            'Authorization: Token e68d5a079f937ea29ad2ec5a5b105b75491a0e0c'
            ),
        ));

        $response = curl_exec($curl);

        curl_close($curl);

        // Decodificamos la respuesta JSON
        $data = json_decode($response, true);

        // Accedemos al valor de 'access_token de donatello'
        $access_token = $data['access_token'];
    

    #== Conexión a Zoho Creator para el detalle del CFDI
    $request_url =  'https://creator.zoho.com/api/v2/'.$appOwner.'/'.$applnkname.'/report/cfdi_I_query/'.$bRecId;
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $request_url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 
    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); 
    curl_setopt($ch, CURLOPT_HTTPHEADER, array('Authorization: Zoho-oauthtoken ' . $access_token));

    $r = curl_exec($ch);

    print_r($r);

    $array = json_decode($r);
	$bdDatArt =  $array->data->jsonInvoiceDetail;
	$abdDatArt = json_decode($bdDatArt,true);

 

    curl_close ($ch);

    ### 4. DESTINATARIOS DE E-MAILS, PERSONAS A QUIENES LES LLEGARÁ DE MANERA ADJUNTA LA FACTURA ELECTRÓNICA EN ARCHIVOS .XML Y .PDF
    $CadenaEmails = $abdDatGen["CadenaEmails"];
    $CadenaEmails = "ffaccinetto@aptus-legal.com";
    
    ### 5. DATOS GENERALES DE LA FACTURA ##############################################
    $fact_serie        = $abdDatGen["fact_serie"];
    $fact_folio        = $abdDatGen["fact_folio"];
    $NoFac             = $fact_serie.$fact_folio;
    $fact_tipcompr     = $abdDatGen["fact_tipcompr"];
    $fact_exportacion  = $abdDatGen["fact_exportacion"];
    $tasa_iva          = $abdDatGen["tasa_iva"];
    $subTotal          = number_format($abdDatGen["subTotal"],2,'.','');
    $descuento         = number_format($abdDatGen["descuento"],2,'.','');
    $IVA               = number_format($abdDatGen["IVA"],2,'.','');
    $total             = number_format($abdDatGen["total"],2,'.','');
    $fecha_fact        = $abdDatGen["fecha_fact"];
    $condicionesDePago = $abdDatGen["condicionesDePago"];
    $formaDePago       = utf8_decode($abdDatGen["formaDePago"]);
    $metodoDePago      = utf8_decode($abdDatGen["metodoDePago"]);
    $TipoCambio        = $abdDatGen["TipoCambio"];
    $LugarExpedicion   = utf8_decode($abdDatGen["LugarExpedicion"]);
    $moneda            = $abdDatGen["moneda"];
	$invoiceID		   = $abdDatGen["invoiceID"];
	$invoiceNumber	   = $abdDatGen["invoiceNumber"];
    $TipoRelacion	   = $abdDatGen["TipoRelacion"];
    
    if($TipPDFGen == 1)
    {
        $sObservaciones	= base64_encode(utf8_decode($abdDatGen["Observaciones"]));
        $sOrdenCobro	   = $abdDatGen["OrdenCobro"];
    }

    $subTotal 				   = 0;
    $totalImpuestosRetenidos   = 0; // 5.24 Total de impuestos retenidos.
    $totalImpuestosTrasladados = 0; // 5.25 Total de impuestos trasladados.
    
    // Calculando subTotal, Impuestos Trasladados y Retenidos.
    for ($i = 0; $i<count($abdDatArt); $i++){
        $subTotal                  = $subTotal + $abdDatArt[$i]["importe"];
        $totalImpuestosTrasladados = $totalImpuestosTrasladados + $abdDatArt[$i]["ivaImporte"];
        $totalImpuestosRetenidos   = $totalImpuestosRetenidos + $abdDatArt[$i]["isrImporte"];
    }
    
    // Calculando el SubTotal.
    $subTotal                  = number_format($subTotal,2,'.',''); 
    $totalImpuestosTrasladados = number_format($totalImpuestosTrasladados,2,'.',''); 
    $totalImpuestosRetenidos   = number_format($totalImpuestosRetenidos,2,'.',''); 
	
    // Calculando Total.
   $total = $subTotal - $descuento + $totalImpuestosTrasladados - $totalImpuestosRetenidos;
	$total = number_format($total,2,'.','');    
	
	### 6. MUESTRA LA ZONA HORARIA PREDETERMINADA DEL SERVIDOR (OPCIONAL A MOSTRAR) ######
    ### 7. ARRAYS QUE CONTIENEN LOS ARTICULOS QUE FORMAN PARTE DE LA VENTA ############
                            
    ### 8. DATOS GENERALES DEL EMISOR #################################################    
    $emisor_rs       = utf8_decode($abdDatEmi["emisor_rs"]);
    $emisor_rfc      = utf8_decode($abdDatEmi["emisor_rfc"]);
    $emisor_regfis   = utf8_decode($abdDatEmi["emisor_regfis"]);

    ### 9. DATOS GENERALES DEL RECEPTOR (CLIENTE) #####################################
    $RFC_Recep    = utf8_decode($abdDatRec["RFC_Recep"]);
    if (strlen($RFC_Recep) == 12)
      {$RFC_Recep = " ".$RFC_Recep; }
    else
      {$RFC_Recep = $RFC_Recep;}
    $receptor_rfc    = $RFC_Recep;
    $receptor_rs     = utf8_decode($abdDatRec["receptor_rs"]);
	$receptor_uso    = utf8_decode($abdDatGen["usoCFDI"]);
    $receptor_regfis = utf8_decode($abdDatRec["receptor_regfis"]);
    $receptor_cp     = utf8_decode($abdDatRec["receptor_cp"]);

    if($TipPDFGen == 1)
    {
        $receptor_cal = utf8_decode($abdDatRec["receptor_cal"]);
        $receptor_ne  = utf8_decode($abdDatRec["receptor_ne"]);
        $receptor_ni  = utf8_decode($abdDatRec["receptor_ni"]);
        $receptor_col = utf8_decode($abdDatRec["receptor_col"]);
        $receptos_loc = utf8_decode($abdDatRec["receptor_loc"]);
        $receptor_del = utf8_decode($abdDatRec["receptor_del"]);
        $receptor_edo = utf8_decode($abdDatRec["receptor_edo"]);
        $receptor_pai = utf8_decode($abdDatRec["receptor_pai"]);

        $sDomicilio = $receptor_cal;
        if($receptor_ne != "")
        {
            $sDomicilio = $sDomicilio." ".$receptor_ne;
        } 
        if($receptor_ni != "")
        {
            $sDomicilio = $sDomicilio." ".$receptor_ni;
        } 
        if($receptor_col != "")
        {
            $sDomicilio = $sDomicilio.", ".$receptor_col;
        } 
        if($receptos_loc != "")
        {
            $sDomicilio = $sDomicilio.", ".$receptos_loc;
        } 
        if($receptor_del != "")
        {
            $sDomicilio = $sDomicilio.", ".$receptor_del;
        } 
        if($receptor_edo != "")
        {
            $sDomicilio = $sDomicilio.", ".$receptor_edo;
        } 
        if($receptor_pai != "")
        {
            $sDomicilio = $sDomicilio.", ".$receptor_pai;
        } 
        if($receptor_cp != "")
        {
            $sDomicilio = $sDomicilio." ".$receptor_cp;
        } 
        $sDireRecep = base64_encode(utf8_encode($sDomicilio));
    }
    

    ### 10. CREACIÓN Y ALMACENAMIENTO DEL ARCHIVO .XML (CFDI) ANTES DE SER TIMBRADO ###################
    
    #== 10.1 Creación de la variable de tipo DOM, aquí se conforma el XML a timbrar posteriormente.
    $xml = new DOMdocument('1.0', 'UTF-8'); 
    $root = $xml->createElement("cfdi:Comprobante");
    $root = $xml->appendChild($root); 
    
    $cadena_original='||';
    $noatt=  array();
    
    #== 10.2 Se crea e inserta el primer nodo donde se declaran los namespaces ======
    cargaAtt($root, array("xsi:schemaLocation"=>"http://www.sat.gob.mx/cfd/4 http://www.sat.gob.mx/sitio_internet/cfd/4/cfdv40.xsd",
            "xmlns:cfdi"=>"http://www.sat.gob.mx/cfd/4",
            "xmlns:xsi"=>"http://www.w3.org/2001/XMLSchema-instance"
        )
    );
    
    #== 10.3 Rutina de integración de nodos =========================================
    cargaAtt($root, array(
             "Version"=>"4.0", 
             "Serie"=>$fact_serie,
             "Folio"=>$fact_folio,
             "Fecha"=>date("Y-m-d")."T".date("H:i:s"),
             "FormaPago"=>$formaDePago,
             "NoCertificado"=>$noCertificado,
             "CondicionesDePago"=>$condicionesDePago,
             "SubTotal"=>$subTotal,
             "Descuento"=>$descuento,
             "Moneda"=>$moneda,
             "TipoCambio"=>$TipoCambio,
             "Total"=>$total,
             "TipoDeComprobante"=>$fact_tipcompr,
             "Exportacion"=>$fact_exportacion,
             "MetodoPago"=>$metodoDePago,
             "LugarExpedicion"=>$LugarExpedicion
          )
       );

    $emisor = $xml->createElement("cfdi:Emisor");
    $emisor = $root->appendChild($emisor);
    cargaAtt($emisor, array("Rfc"=>$emisor_rfc,
                            "Nombre"=>$emisor_rs,
                            "RegimenFiscal"=>$emisor_regfis
                             )
                        );

    $receptor = $xml->createElement("cfdi:Receptor");
    $receptor = $root->appendChild($receptor);
    cargaAtt($receptor, array("Rfc"=>$receptor_rfc,
                    "Nombre"=>$receptor_rs,
                    "DomicilioFiscalReceptor"=>$receptor_cp,
                    "RegimenFiscalReceptor"=>$receptor_regfis,
                    "UsoCFDI"=>$receptor_uso
                )
            );

	// Adicionado por FFR en Marzo 30, 2019
	// Para el manejo de los CFDI Relacionados 
	if($bDatRel <> "")
	{
		$cfdiRelacionados = $xml->createElement("cfdi:CfdiRelacionados");
		$cfdiRelacionados = $root->appendChild($cfdiRelacionados);
        cargaAtt($cfdiRelacionados, array("TipoRelacion"=>utf8_decode($TipoRelacion)));
		for ($i=0; $i < count($abdDatRel); $i++)
		{
			$cfdiRelacionado = $xml->createElement("cfdi:cfdiRelacionado");
			$cfdiRelacionado = $cfdiRelacionados->appendChild($cfdiRelacionado);
			cargaAtt($cfdiRelacionado, array("UUID"=>utf8_decode($abdDatRel[$i]["UUID"])));
		}
	}

    $conceptos = $xml->createElement("cfdi:Conceptos");
    $conceptos = $root->appendChild($conceptos);

	$isrImpuesto = "001";
	$ivaImpuesto = "002";
    $lIvaTraslad = false;
    $lIsrRetenid = false;
    
    #== 10.4 Ciclo "for", recopilación de datos de artículos e integración de sus respectivos nodos =
    $ivaTasaOCuota = 0.160000;
    for ($i=0; $i<count($abdDatArt); $i++){
       	
        $concepto = $xml->createElement("cfdi:Concepto");
        $concepto = $conceptos->appendChild($concepto);
        cargaAtt($concepto, array(
               "ClaveProdServ"=>utf8_decode($abdDatArt[$i]["claveProd"]),
               "Cantidad"=>$abdDatArt[$i]["cantidad"],
               "ClaveUnidad"=>utf8_decode($abdDatArt[$i]["unidad"]),
               "Unidad"=>utf8_decode($abdDatArt[$i]["descUnidad"]),
               "Descripcion"=>utf8_decode($abdDatArt[$i]["descripcion"]),
               "ValorUnitario"=>number_format($abdDatArt[$i]["valorUnitario"],2,'.',''),
               "Importe"=>number_format($abdDatArt[$i]["importe"],2,'.',''),
               "Descuento"=>number_format($abdDatArt[$i]["descuento"],2,'.',''),
               "ObjetoImp"=>utf8_decode($abdDatArt[$i]["ObjetoImp"])
            )
        );
    
		$impuestos = $xml->createElement("cfdi:Impuestos");
		$impuestos = $concepto->appendChild($impuestos);

		//if ($abdDatArt[$i]["ivaImporte"] > 0)
      if($abdDatArt[$i]["ivaImpuesto"] == "002")
		{
            $lIvaTraslad = true;

			$Traslados = $xml->createElement("cfdi:Traslados");
			$Traslados = $impuestos->appendChild($Traslados);
			
			$Traslado = $xml->createElement("cfdi:Traslado");
			$Traslado = $Traslados->appendChild($Traslado);

			$ivaImpuesto = $abdDatArt[$i]["ivaImpuesto"];
			
			cargaAtt($Traslado, array(
				   "Base"=>number_format($abdDatArt[$i]["ivaBase"],2,'.',''),
				   "Impuesto"=>$abdDatArt[$i]["ivaImpuesto"],
				   "TipoFactor"=>$abdDatArt[$i]["ivaTipoFactor"],
				   "TasaOCuota"=>number_format($abdDatArt[$i]["ivaTasaOCuota"],6,'.',''),
				   "Importe"=>number_format($abdDatArt[$i]["ivaImporte"],2,'.','')
				) 
			);
			$ivaTasaOCuota = $abdDatArt[$i]["ivaTasaOCuota"];
		}
		
		//if ($abdDatArt[$i]["isrImporte"] > 0)
      if($abdDatArt[$i]["isrImpuesto"] == "001")
		{
            $lIsrRetenid = true;
			$Retenciones = $xml->createElement("cfdi:Retenciones");
			$Retenciones = $impuestos->appendChild($Retenciones);
			
			$Retencion = $xml->createElement("cfdi:Retencion");
			$Retencion = $Retenciones->appendChild($Retencion);
			
			$isrImpuesto = $abdDatArt[$i]["isrImpuesto"];

			cargaAtt($Retencion, array(
				   "Base"=>number_format($abdDatArt[$i]["isrBase"],2,'.',''),
				   "Impuesto"=>$abdDatArt[$i]["isrImpuesto"],
				   "TipoFactor"=>$abdDatArt[$i]["isrTipoFactor"],
				   "TasaOCuota"=>number_format($abdDatArt[$i]["isrTasaOCuota"],6,'.',''),
				   "Importe"=>number_format($abdDatArt[$i]["isrImporte"],2,'.','')
				) 
			);
		}
	}
	
	$Impuestos = $xml->createElement("cfdi:Impuestos");
	$Impuestos = $root->appendChild($Impuestos);

	//if ($totalImpuestosTrasladados > 0)
	if ($lIvaTraslad == true)
	{
		$Traslados = $xml->createElement("cfdi:Traslados");
		$Traslados = $Impuestos->appendChild($Traslados);

			$Traslado = $xml->createElement("cfdi:Traslado");
			$Traslado = $Traslados->appendChild($Traslado);

				cargaAtt($Traslado, array(
					   "Impuesto"=>$ivaImpuesto,
					   "TipoFactor"=>"Tasa",
					   "TasaOCuota"=>number_format($ivaTasaOCuota,6,'.',''),
					   "Importe"=>number_format($totalImpuestosTrasladados,2,'.','')
					) 
				);    
				
				cargaAtt($Impuestos, array(
								"TotalImpuestosTrasladados"=>number_format($totalImpuestosTrasladados,2,'.','')
							)
						);
	}
	
	//if ($totalImpuestosRetenidos > 0)
	if ($lIsrRetenid == true)
	{
		$Retenciones = $xml->createElement("cfdi:Retenciones");
		$Retenciones = $Impuestos->appendChild($Retenciones);    

			$Retencion = $xml->createElement("cfdi:Retencion");
			$Retencion = $Retenciones->appendChild($Retencion);

				cargaAtt($Retencion, array(
					   "Impuesto"=>$isrImpuesto,
					   "Importe"=>number_format($totalImpuestosRetenidos,2,'.','')
					) 
				);

				cargaAtt($Impuestos, array(
						"TotalImpuestosRetenidos"=>number_format($totalImpuestosRetenidos,2,'.','')
					)
				);
    }        
	
    $complemento = $xml->createElement("cfdi:Complemento");
    $complemento = $root->appendChild($complemento);
    
    #== 10.7 Termina de conformarse la "Cadena original" con doble ||
    $cadena_original .= "|";   
	
    #=== Muestra la cadena original (opcional a mostrar) ======================= 
    
    #== 10.8 Proceso para obtener el sello digital del archivo .pem.key =========
    $keyid = openssl_get_privatekey(file_get_contents($SendaPEMS.$file_key));
    openssl_sign($cadena_original, $crypttext, $keyid, OPENSSL_ALGO_SHA256);
    openssl_free_key($keyid);

    #== 10.9 Se convierte la cadena digital a Base 64 ===========================
    $sello = base64_encode($crypttext);    
    
    #=== Muestra el sello (opcional a mostrar) =================================
    
    #== 10.10 Proceso para extraer el certificado del sello digital ==================
    $file = $SendaPEMS.$file_cer;      // Ruta al archivo
    $datos = file($file);
    $certificado = ""; 
    $carga=false;  
    for ($i=0; $i<sizeof($datos); $i++){
        if (strstr($datos[$i],"END CERTIFICATE")) $carga=false;
        if ($carga) $certificado .= trim($datos[$i]);

        if (strstr($datos[$i],"BEGIN CERTIFICATE")) $carga=true;
    } 
    
    #=== Muestra el certificado del sello digital (opcional a mostrar) =========
    
    #== 10.11 Se continua con la integración de nodos ===========================   
    $root->setAttribute("sello",$sello);
    $root->setAttribute("certificado",$certificado);   # Certificado.
	
    //$root->setAttribute("motivoDescuento",$MotivDesc); # Motivo de descuento. 
    //$root->setAttribute("folio",$fact_folio);          # Folio de la factura (número entero).
    //$root->setAttribute("serie",$fact_serie);          # Serie de la factura (letra o letras).
    
    #== Fin de la integración de nodos =========================================
    
    
    #=== 10.12 Se guarda el archivo .XML antes de ser timbrado =======================
	
    $NomArchCFDI = $SendaCFDI."PreCFDI-33_".$NoFac.".xml";
    $NomArchPDF1 = $SendaCFDI."VP_CFDI_".$fact_serie.str_pad($fact_folio, 6, "0", STR_PAD_LEFT)."_".$invoiceNumber.".pdf";
	
    $NomArchXML = "PreCFDI-33_".$NoFac.".xml";
    $NomArchPDF = "VP_CFDI_".$fact_serie.str_pad($fact_folio, 6, "0", STR_PAD_LEFT)."_".$invoiceNumber.".pdf";


//    $cfdi = $xml->saveXML();
    $xml->formatOutput = true;             
    $xml->save($NomArchCFDI); // Guarda el archivo .XML (sin timbrar) en el directorio predeterminado.
    unset($xml);
    
    #=== 10.13 Se dan permisos de escritura al archivo .xml. =========================
    chmod($NomArchCFDI, 0777); 

    ### 13. CREACION DE PDF Y ENVIO DE CORREO CON XML Y PDF ###########################
    # 13.1 Creación del Archivo PDF
    //$url = 'http://localhost/aptusCFDIRF/wsInvoicePDF_VP_v33.php?NomArchXML='.$NomArchXML.'&NomArchPDF='.$NomArchPDF;
    if($TipPDFGen == 1)
    {
        $url = 'https://aptuslegal.app/aptusCFDIRF/'.$FormatoPDF.'.php?NomArchXML='.$NomArchXML.'&NomArchPDF='.$NomArchPDF.'&pInvNumb='.$invoiceNumber.'&pOrdCob='.$sOrdenCobro.'&pObserva='.$sObservaciones.'&pDirRecep='.$sDireRecep;
    }
    else
    {
        $url = 'https://aptuslegal.app/aptusCFDIRF/'.$FormatoPDF.'.php?NomArchXML='.$NomArchXML.'&NomArchPDF='.$NomArchPDF;
    }
    // echo $url;
    // echo '<br>';

    $objCurl = curl_init();
    curl_setopt($objCurl, CURLOPT_URL, $url);
    curl_setopt($objCurl, CURLOPT_HEADER, 0);
    curl_setopt($objCurl, CURLOPT_RETURNTRANSFER, true);
    curl_exec($objCurl);
    //curl_close($objCurl);

		# Usamos la funcion curl_errno() para atrapar cualquier error relacionado con la llamada.
		if (curl_errno($objCurl))
		{
    		$error_msg = curl_error($objCurl);
		}

		curl_close ($objCurl);

		if (isset($error_msg))
		{
			$j_array = array('code' => '666', 'message' => "Error en la llamada curl " . $error_msg);
			$Resultado = json_encode($j_array);
			echo $Resultado;
			return;
		}
    
    # 13.3 Envio del Archivos a Invoice de Zoho Creator
	# Primero el PDF
    $file_name_with_full_path = '/var/www/html/aptusCFDIRF/archs_cfdi/'.$NomArchPDF;
    //$request_url = 'https://creator.zoho.com/api/xml/fileupload/scope=creatorapi';
    # Actualización a API V2 y OAuth JFA y FFR 2021-06-18
    $request_url = 'https://creator.zoho.com/api/v2/'.$appOwner.'/'.$applnkname.'/report/cfdi_I_query/'.$bRecId.'/File_PDF_VP/upload';

    if (function_exists('curl_file_create')) { // php 5.6+
      $cFile = curl_file_create($file_name_with_full_path);
    } 
    else 
	{  
      $cFile = '@' . realpath($file_name_with_full_path);
    }

/*
	echo $bRecId;
	echo "<br>";
	echo $authtoken_ZC;
	echo "<br>";
	echo $NomArchPDF;
	echo "<br>";
	echo $file_name_with_full_path;
	echo "<br>";
*/	

//    'applinkname' => 'cfdi',


    $post = array(
    //'authtoken' => $authtoken_ZC,							
    //'applinkname' => $applnkname,
    //'formname' => 'CreacionCFDIv33',
	//'fieldname' => 'File_PDF_VP',
    //'recordId' => $bRecId,
    'filename' => $NomArchPDF,
    'file'=> $cFile);


    #----------------------------------------------------------------
        #== Llamado para generar el token para donatello
        #----------------------------------------------------------------
        $curl = curl_init();

        curl_setopt_array($curl, array(
            CURLOPT_URL => 'https://donatello.aptuslegal.app/oauth/token/'.$appOwner,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_ENCODING => '',
            CURLOPT_MAXREDIRS => 10,
            CURLOPT_TIMEOUT => 0,
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
            CURLOPT_CUSTOMREQUEST => 'GET',
            CURLOPT_HTTPHEADER => array(
            'Authorization: Token e68d5a079f937ea29ad2ec5a5b105b75491a0e0c'
            ),
        ));

        $response = curl_exec($curl);

        curl_close($curl);

        // Decodificamos la respuesta JSON
        $data = json_decode($response, true);

        // Accedemos al valor de 'access_token de donatello'
        $access_token = $data['access_token'];
    // $ch = curl_init();
    // curl_setopt($ch, CURLOPT_URL, $request_url);
    // //curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
    // curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 
    // curl_setopt($ch, CURLOPT_POST, true); 
    // curl_setopt($ch, CURLOPT_POSTFIELDS, $post); 
    // curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); 
	// #JFA: Cambiamos el método de Autenticación a OAuth
    // curl_setopt($ch, CURLOPT_HTTPHEADER, array('Authorization: Zoho-oauthtoken ' . $access_token));

    // $r = curl_exec($ch);
    // curl_close ($ch);

    try {
        $result = uploadPdfWithRetries($request_url, $post, $access_token);
        $j_array = array('code' => "200", "message" => "Proceso de creacion de CFDI fue exitoso", 'file_upload' => $result);
        $Resultado = json_encode($j_array);
        echo $Resultado;
        return;
    } catch (Exception $e) {
        error_log($e->getMessage());
        $j_array = array('code' => "500", "message" => "Error no pudo añadirse el PDF", 'Error' => $e->getMessage());
        $Resultado = json_encode($j_array);
        echo $Resultado;
        return;
    }

    #== Se elimina los archivos de trabajo
    //unlink($NomArchCFDI);
    //unlink($NomArchPDF1);
	
    $j_array = array('code' => "200", "message" => "Proceso de creacion de CFDI fue exitoso", 'file_upload' => $NomArchPDF);
    $Resultado = json_encode($j_array);
	echo $Resultado;
	return;
	
    
    ### 14. FUNCIONES DEL MÓDULO #########################################################
    # 14.1 Función que integra los nodos al archivo .XML y forma la "Cadena original".
    function cargaAtt(&$nodo, $attr){
        global $xml, $cadena_original;
        $quitar = array('sello'=>1,'noCertificado'=>1,'certificado'=>1);
        foreach ($attr as $key => $val){
            $val = preg_replace('/\s\s+/', ' ', $val);
            $val = trim($val);
            if (strlen($val)>0){
                 $val = utf8_encode(str_replace("|","/",$val));
                 $nodo->setAttribute($key,$val);
                 if (!isset($quitar[$key])) 
                   if (substr($key,0,3) != "xml" &&
                       substr($key,0,4) != "xsi:")
                    $cadena_original .= $val . "|";
            }
         }
     }

    
    # 14.2 Funciónes que da formato al "Importe total" como lo requiere el SAT para ser integrado al código QR.
     
    function ProcesImpTot($ImpTot){
        $ArrayImpTot = explode(".", $ImpTot);
        $NumEnt = $ArrayImpTot[0];
        $NumDec = ProcesDecFac($ArrayImpTot[1]);
        return $NumEnt.".".$NumDec;
    }
    
    function ProcesDecFac($Num){
        $FolDec = "";
        if ($Num < 10){$FolDec = "00000".$Num;}
        if ($Num > 9 and $Num < 100){$FolDec = $Num."0000";}
        if ($Num > 99 and $Num < 1000){$FolDec = $Num."000";}
        if ($Num > 999 and $Num < 10000){$FolDec = $Num."00";}
        if ($Num > 9999 and $Num < 100000){$FolDec = $Num."0";}
        return $FolDec;
    }        
    function getZohoCreatorRecord($appOwner, $appLinkName, $reportLinkName, $recordId, $access_token, $maxRetries = 3)
{
    $base_url = 'https://www.zohoapis.com/creator/v2.1';
    $request_url = "$base_url/data/$appOwner/$appLinkName/report/$reportLinkName/$recordId";

    $headers = [
        "Authorization: Zoho-oauthtoken $access_token",
        "Accept: application/json"
    ];

    $retryCount = 0;
    $waitSeconds = 1;

    while ($retryCount < $maxRetries) {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $request_url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        $data = json_decode($response);

        if ($httpCode === 200 && isset($data->data)) {
            return $data->data;
        }

        $errorCode = $data->code ?? 'N/A';
        $errorMsg  = $data->message ?? 'Respuesta inválida';

        if ($httpCode === 401 || $errorCode === 1030) {
            return json_encode([
                'code' => '401',
                'message' => 'ERROR - Token de acceso expirado o inválido para Zoho Creator'
            ]);
        }

        if ($errorCode === 3190) {
            return json_encode([
                'code' => '404',
                'message' => "ERROR - No se encontró el registro con ID $recordId en el reporte $reportLinkName"
            ]);
        }

        if ($httpCode === 429 || $errorCode === 2955) {
            sleep($waitSeconds);
            $waitSeconds *= 2;
            $retryCount++;
            continue;
        }

        if (!isset($data->data)) {
            sleep($waitSeconds);
            $retryCount++;
            continue;
        }

        return json_encode([
            'code' => '500',
            'message' => "ERROR - Fallo inesperado en Zoho Creator. HTTP $httpCode - Código $errorCode - $errorMsg"
        ]);
    }

    return json_encode([
        'code' => '408',
        'message' => "ERROR - Tiempo de espera agotado tras $maxRetries intentos para obtener el registro $recordId"
    ]);
}


/**
 * Sube un PDF a Zoho Creator con reintentos y back-off exponencial.
 *
 * @param string $request_url    URL de la API de Creator para el upload.
 * @param array  $post           Array con 'filename' y 'file'.
 * @param string $access_token   Token OAuth válido.
 * @param int    $maxRetries     Número máximo de intentos (por defecto 5).
 * @param int    $baseDelay      Retardo base en segundos (por defecto 1).
 *
 * @return string                Respuesta cruda del servidor en caso de éxito.
 * @throws Exception             Si se agotan los reintentos o hay errores de cURL/HTTP.
 */
function uploadPdfWithRetries($request_url, array $post, $access_token, $maxRetries = 5, $baseDelay = 1)
{
    $errors = [];

    for ($attempt = 1; $attempt <= $maxRetries; $attempt++) {
        $ch = curl_init();
        curl_setopt_array($ch, [
            CURLOPT_URL            => $request_url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST           => true,
            CURLOPT_POSTFIELDS     => $post,
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_HTTPHEADER     => [
                'Authorization: Zoho-oauthtoken ' . $access_token
            ],
            // CURLOPT_SSL_VERIFYPEER => false,
        ]);

        $response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$errNo    = curl_errno($ch);
$errMsg   = curl_error($ch);
curl_close($ch);

// Intento: siempre parseamos la respuesta para ver si hay error de API
$apiError = false;
$body     = $response;

// 1) Si hubo fallo de cURL o HTTP fuera de 2xx, es error
if ($response === false || $httpCode < 200 || $httpCode >= 300) {
    $apiError = true;
    $errors[] = sprintf(
        'Intento %d/%d: HTTP %d, cURL[%d]: %s',
        $attempt, $maxRetries, $httpCode, $errNo, $errMsg
    );
} else {
    // 2) HTTP OK, pero comprobamos Zoho-level
    $json = json_decode($body, true);
    if (json_last_error() === JSON_ERROR_NONE && isset($json['code']) && $json['code'] != 3000) {
        $apiError = true;
        $errors[] = sprintf(
            'Intento %d/%d: Zoho API error code %d: %s',
            $attempt, $maxRetries,
            $json['code'],
            $json['message'] ?? '(sin mensaje)'
        );
    }
}

// 3) Si no hubo **ningún** error, devolvemos (respuesta + todos los errores previos)
if (!$apiError) {
    return [
        'response' => $body,
        'errors'   => $errors
    ];
}

// 4) Si hubo error, esperamos back-off y reintentamos
$delay = $baseDelay * (2 ** ($attempt - 1));
sleep($delay);
    }

    // Si llegamos aquí, todos los intentos fallaron
    $errorText = implode("\n", $errors);
    throw new Exception(
        "No se pudo subir el PDF tras {$maxRetries} intentos. Errores:\n{$errorText}"
    );
}