Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
13.45% |
106 / 788 |
|
0.00% |
0 / 14 |
CRAP | n/a |
0 / 0 |
|
| _sii_headers_to_array | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
20 | |||
| _sii_fiscal_region | |
0.00% |
0 / 30 |
|
0.00% |
0 / 1 |
342 | |||
| _extract_wsdl_information | |
0.00% |
0 / 39 |
|
0.00% |
0 / 1 |
156 | |||
| _sii_representation_certificate | |
0.00% |
0 / 27 |
|
0.00% |
0 / 1 |
210 | |||
| _send_sii_request | |
0.00% |
0 / 45 |
|
0.00% |
0 / 1 |
110 | |||
| _parse_sii_reponse_lines | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
56 | |||
| _parse_sii_onerror_lines | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
72 | |||
| _parse_sii_response | |
0.00% |
0 / 38 |
|
0.00% |
0 / 1 |
210 | |||
| update_ticketsii_info | |
86.96% |
60 / 69 |
|
0.00% |
0 / 1 |
25.28 | |||
| _sendsii_substituted_ticket_is_valid | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
42 | |||
| _sendsii_substituted_ticket_expedition_ts | |
0.00% |
0 / 25 |
|
0.00% |
0 / 1 |
182 | |||
| _sendsii_enrich_recap_substituted | |
0.00% |
0 / 61 |
|
0.00% |
0 / 1 |
240 | |||
| _sendsii_omit_recap_without_substituted | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
20 | |||
| sendsii_run | |
10.13% |
39 / 385 |
|
0.00% |
0 / 1 |
9714.35 | |||
| 1 | <?php |
| 2 | require_once __DIR__.'/../legacy/ddbb/conx.php'; |
| 3 | include_once __DIR__.'/../legacy/model/synchloader.php'; |
| 4 | include_once __DIR__.'/../legacy/model/syncchanges.php'; |
| 5 | include_once __DIR__.'/calcsii.php'; |
| 6 | include_once __DIR__.'/validate.php'; |
| 7 | include_once __DIR__.'/../regstr/ticketsii.php'; |
| 8 | include_once __DIR__.'/../legacy/model/spec/ticketdata.php'; |
| 9 | |
| 10 | // https://blog.devgenius.io/how-to-get-the-response-headers-with-curl-in-php-2173b10d4fc5 |
| 11 | function _sii_headers_to_array($str) { |
| 12 | $headers = array(); |
| 13 | |
| 14 | $explode = explode( "\r\n" , $str ); |
| 15 | for ($i=0; $i < count($explode); $i++){ |
| 16 | // we dont care about the two \r\n lines at the end of the headers |
| 17 | if (strlen($explode[$i]) > 0){ |
| 18 | $seppos = strpos($explode[$i], ":"); |
| 19 | |
| 20 | // the headers start with HTTP status codes, which do not contain a colon so we can filter them out too |
| 21 | if ($seppos){ |
| 22 | $key = substr($explode[$i], 0, $seppos); |
| 23 | $val = substr($explode[$i], $seppos + 1); |
| 24 | $headers[strtolower($key)] = $val; |
| 25 | } |
| 26 | } |
| 27 | } |
| 28 | |
| 29 | return $headers; |
| 30 | } |
| 31 | |
| 32 | function _sii_fiscal_region($conn, &$place, &$region){ |
| 33 | $retval = ApplicationError::Success; |
| 34 | |
| 35 | if (success($retval)){ |
| 36 | $results = []; |
| 37 | |
| 38 | $res = $conn->query('ADDRESS', [ 'objid' => $place['fiscal'] ], $results); |
| 39 | if ($res){ |
| 40 | if (count($results) > 0){ |
| 41 | $formatted = $results[0]['formatted']; |
| 42 | } |
| 43 | else { |
| 44 | addlog(__FILE__, LogLevel::Warning, "Place [".$place['objid']."] has not provided fiscal address"); |
| 45 | $retval = ApplicationError::Invalid; |
| 46 | } |
| 47 | } |
| 48 | else { |
| 49 | $retval = ApplicationError::Database; |
| 50 | } |
| 51 | } |
| 52 | |
| 53 | if (success($retval)){ |
| 54 | $parts = explode(',', $formatted); |
| 55 | $country = strtolower(trim($parts[count($parts)-1])); |
| 56 | $region = trim($parts[count($parts)-2]); |
| 57 | if (($country != 'españa') && ($country != 'spain')){ |
| 58 | addlog(__FILE__, LogLevel::Warning, "Place [".$place['objid']."] has its fiscal address out of Spain (".$country.")"); |
| 59 | $retval = ApplicationError::Invalid; |
| 60 | } |
| 61 | else { |
| 62 | $ridx = 0; |
| 63 | |
| 64 | addlog(__FILE__, LogLevel::Verbose, "Place [".$place['objid']."] has its fiscal address in region - '".$region."'"); |
| 65 | if ((stripos($region, 'Bizkaia') !== false) || (stripos($region, 'Vizcaya') !== false)){ |
| 66 | addlog(__FILE__, LogLevel::Information, "Applying 'Bizkaia' fiscal considerations"); |
| 67 | $ridx = AppConstants::SII_Sandbox() ? 0 : 1; |
| 68 | } |
| 69 | |
| 70 | if ((stripos($region, 'Guipúzcoa') !== false) || (stripos($region, 'Guipuzcoa') !== false) || (stripos($region, 'Gipuzkoa') !== false)){ |
| 71 | addlog(__FILE__, LogLevel::Information, "Applying 'Gipuzkoa' fiscal considerations"); |
| 72 | $ridx = AppConstants::SII_Sandbox() ? 0 : 2; |
| 73 | } |
| 74 | |
| 75 | if ((stripos($region, 'Álava') !== false) || (stripos($region, 'Alava') !== false) || (stripos($region, 'Araba') !== false)){ |
| 76 | addlog(__FILE__, LogLevel::Information, "Applying 'Araba' fiscal considerations"); |
| 77 | $ridx = AppConstants::SII_Sandbox() ? 0 : 3;; |
| 78 | } |
| 79 | |
| 80 | $region = [ 'general', 'bizkaia', 'gipuzkoa', 'araba' ][$ridx]; |
| 81 | } |
| 82 | } |
| 83 | |
| 84 | return $retval; |
| 85 | } |
| 86 | |
| 87 | function _extract_wsdl_information($region, $action, &$url, &$nss){ |
| 88 | $retval = ApplicationError::Success; |
| 89 | |
| 90 | if (success($retval)){ |
| 91 | /* |
| 92 | Ficheros WSDL para las diferentes admisitraciones |
| 93 | España: https://sede.agenciatributaria.gob.es/static_files/Sede/Procedimiento_ayuda/G417/FicherosSuministros/V_1_1/WSDL/SuministroFactEmitidas.wsdl |
| 94 | Bizkaia (sólo produccion): https://www.bizkaia.eus/ogasuna/sii/documentos/SuministroFactEmitidas.wsdl |
| 95 | Guipuzkoa (sólo producción): https://egoitza.gipuzkoa.eus/ogasuna/sii/ficheros/v1.1/SuministroFactEmitidas.wsdl |
| 96 | Araba (sólo producción): https://sii.araba.eus/documentos/SuministroFactEmitidas.wsdl |
| 97 | */ |
| 98 | |
| 99 | $wsdl = [ |
| 100 | 'general' => 'https://sede.agenciatributaria.gob.es/static_files/Sede/Procedimiento_ayuda/G417/FicherosSuministros/V_1_1/WSDL/SuministroFactEmitidas.wsdl', |
| 101 | 'bizkaia' => 'https://www.bizkaia.eus/ogasuna/sii/documentos/SuministroFactEmitidas.wsdl', |
| 102 | 'gipuzkoa' => 'https://egoitza.gipuzkoa.eus/ogasuna/sii/ficheros/v1.1/SuministroFactEmitidas.wsdl', |
| 103 | 'araba' => 'https://sii.araba.eus/documentos/SuministroFactEmitidas.wsdl ' |
| 104 | ]; |
| 105 | |
| 106 | $wsdl_url = $wsdl[$region]; |
| 107 | } |
| 108 | |
| 109 | if (success($retval)){ |
| 110 | $wsdl_raw = file_get_contents($wsdl_url); |
| 111 | if ($wsdl_raw === false){ |
| 112 | addlog(__FILE__, LogLevel::Error, "Cannot read WSDL contents from '".$wsdl_url."'"); |
| 113 | $retval = ApplicationError::NotFound; |
| 114 | } |
| 115 | else { |
| 116 | libxml_use_internal_errors(true); |
| 117 | $xml = simplexml_load_string($wsdl_raw); |
| 118 | if ($xml === false){ |
| 119 | print("Failed loading XML from WSDL file at '".$wsdl_url."'"); |
| 120 | foreach(libxml_get_errors() as $error) { |
| 121 | print($error->message); |
| 122 | } |
| 123 | } |
| 124 | else { |
| 125 | $url = null; |
| 126 | $avl = []; |
| 127 | $nss = []; |
| 128 | |
| 129 | // get the target WSDL server URL for the action |
| 130 | $ports = $xml->children('wsdl', true)->service->children('wsdl', true); |
| 131 | foreach($ports as $node => $port){ |
| 132 | $name = $avl[] = (string)$port->attributes()->name; |
| 133 | if ($name == $action){ |
| 134 | $url = (string)$port->children('soap', true)->attributes()->location; |
| 135 | } |
| 136 | } |
| 137 | |
| 138 | if ($url == null){ |
| 139 | addlog(__FILE__, LogLevel::Error, "Action [".$action." not found in WSDL schema. Available actions are [".PHP_EOL.print_r($avl, true).PHP_EOL); |
| 140 | $retval = ApplicationError::NotFound; |
| 141 | } |
| 142 | |
| 143 | // get all the availabe XSD schemas |
| 144 | $namespaces = $xml->getDocNamespaces(); |
| 145 | foreach($namespaces as $namespace => $xsd){ |
| 146 | if (substr($xsd, -4) == '.xsd'){ |
| 147 | $nss[$namespace] = (string)$xsd; |
| 148 | } |
| 149 | } |
| 150 | |
| 151 | if (count(array_keys($nss)) == 0){ |
| 152 | addlog(__FILE__, LogLevel::Error, "No xsd files found within namespaces for '".$wsdl_url."'"); |
| 153 | $retval = ApplicationError::NotFound; |
| 154 | } |
| 155 | } |
| 156 | } |
| 157 | } |
| 158 | |
| 159 | return $retval; |
| 160 | } |
| 161 | |
| 162 | /** |
| 163 | * Certificado PKCS#12 autorizado para presentar en representación (mismo criterio que VeriFactu). |
| 164 | * {@see AppConstants::certPath()} y contraseña {@see BUILD_CERT_PASSWORD} en entorno. |
| 165 | * |
| 166 | * @param-out string|null $certificatePem Certificado en PEM. |
| 167 | * @param-out string|null $privateKeyPem Clave privada en PEM. |
| 168 | */ |
| 169 | function _sii_representation_certificate(&$certificatePem, &$privateKeyPem){ |
| 170 | $retval = ApplicationError::Success; |
| 171 | $certificatePem = null; |
| 172 | $privateKeyPem = null; |
| 173 | |
| 174 | $password = $_ENV['BUILD_CERT_PASSWORD'] ?? getenv('BUILD_CERT_PASSWORD'); |
| 175 | if ($password === false || $password === null || $password === ''){ |
| 176 | addlog(__FILE__, LogLevel::Error, 'BUILD_CERT_PASSWORD is required for SII (certificado de representación).'); |
| 177 | return ApplicationError::Parameters; |
| 178 | } |
| 179 | $password = (string) $password; |
| 180 | |
| 181 | $p12path = AppConstants::certPath(); |
| 182 | if (!file_exists($p12path)){ |
| 183 | addlog(__FILE__, LogLevel::Error, "Configured SII representation certificate file not found at '".$p12path."'"); |
| 184 | return ApplicationError::NotFound; |
| 185 | } |
| 186 | |
| 187 | $pkcs12 = @file_get_contents($p12path); |
| 188 | if (($pkcs12 === false) || ($pkcs12 === '')){ |
| 189 | addlog(__FILE__, LogLevel::Error, "Could not read SII PKCS#12 certificate '".$p12path."'"); |
| 190 | return ApplicationError::Certificate; |
| 191 | } |
| 192 | |
| 193 | $certs = []; |
| 194 | if (!@openssl_pkcs12_read($pkcs12, $certs, $password)){ |
| 195 | addlog(__FILE__, LogLevel::Error, 'Could not parse SII PKCS#12 with configured BUILD_CERT_PASSWORD.'); |
| 196 | return ApplicationError::Certificate; |
| 197 | } |
| 198 | |
| 199 | $certificatePem = array_key_exists('cert', $certs) ? (string) $certs['cert'] : null; |
| 200 | $privateKeyPem = array_key_exists('pkey', $certs) ? (string) $certs['pkey'] : null; |
| 201 | if (($certificatePem === null) || ($certificatePem === '') || ($privateKeyPem === null) || ($privateKeyPem === '')){ |
| 202 | addlog(__FILE__, LogLevel::Error, 'Configured SII PKCS#12 has missing certificate or private key.'); |
| 203 | $retval = ApplicationError::Certificate; |
| 204 | } |
| 205 | else { |
| 206 | addlog(__FILE__, LogLevel::Information, "SII TLS client cert: representation PKCS#12 [file: '".basename($p12path)."']"); |
| 207 | } |
| 208 | |
| 209 | return $retval; |
| 210 | } |
| 211 | |
| 212 | function _send_sii_request($xml, $siiurl, $certval, $certkey, $action, &$result){ |
| 213 | $retval = ApplicationError::Success; |
| 214 | $result = [ |
| 215 | 'headers' => [], |
| 216 | 'content' => '', |
| 217 | ]; |
| 218 | |
| 219 | if (success($retval)){ |
| 220 | $headers = array( |
| 221 | "Content-type: text/xml;charset=\"utf-8\"", |
| 222 | "SOAPAction: ".$action |
| 223 | ); |
| 224 | |
| 225 | $curl = curl_init(); |
| 226 | curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0); |
| 227 | curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); |
| 228 | curl_setopt($curl, CURLOPT_URL, $siiurl); |
| 229 | |
| 230 | if ($certval){ |
| 231 | curl_setopt($curl, CURLOPT_SSLCERT, $certval); |
| 232 | } |
| 233 | |
| 234 | if ($certkey){ |
| 235 | curl_setopt($curl, CURLOPT_SSLKEY, $certkey); |
| 236 | } |
| 237 | |
| 238 | curl_setopt($curl, CURLOPT_HTTPAUTH, CURLAUTH_ANY); |
| 239 | curl_setopt($curl, CURLOPT_TIMEOUT, 60); |
| 240 | curl_setopt($curl, CURLOPT_POST, true); |
| 241 | curl_setopt($curl, CURLOPT_POSTFIELDS, $xml); // the SOAP request |
| 242 | |
| 243 | curl_setopt($curl, CURLOPT_HEADER, true); |
| 244 | curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); |
| 245 | |
| 246 | $hresponse = ''; |
| 247 | $bresponse = ''; |
| 248 | |
| 249 | $response = curl_exec($curl); |
| 250 | if ($response === false){ |
| 251 | addlog(__FILE__, LogLevel::Error, "SII WSDL error: ". curl_error($curl). "(response code: ".curl_getinfo($curl, CURLINFO_RESPONSE_CODE).")"); |
| 252 | $retval = ApplicationError::Generic; |
| 253 | } |
| 254 | else { |
| 255 | $headersze = curl_getinfo($curl, CURLINFO_HEADER_SIZE); |
| 256 | $hresponse = substr($response, 0, $headersze); |
| 257 | $bresponse = substr($response, $headersze); |
| 258 | } |
| 259 | |
| 260 | curl_close($curl); |
| 261 | |
| 262 | if ($response !== false){ |
| 263 | addlog(__FILE__, LogLevel::Debug, "SII XML sent: ".PHP_EOL.$xml.PHP_EOL); |
| 264 | |
| 265 | $result['headers'] = _sii_headers_to_array($hresponse); |
| 266 | $result['content'] = $bresponse; |
| 267 | |
| 268 | addlog(__FILE__, LogLevel::Debug, "SII WSDL response (headers): ".PHP_EOL.print_r($result['headers'], true).PHP_EOL); |
| 269 | if (($result['headers']['content-length'] == 0) || (strpos($result['headers']['content-type'], 'text/xml') === false)) { |
| 270 | $retval = ApplicationError::Generic; |
| 271 | if (isset($result['headers']['location']) && (strpos($result['headers']['location'], "erro401") !== false)){ |
| 272 | $retval = ApplicationError::Certificate; |
| 273 | } |
| 274 | } |
| 275 | else { |
| 276 | addlog(__FILE__, LogLevel::Debug, "SII WSDL response (raw content): ".PHP_EOL.$result['content'].PHP_EOL); |
| 277 | } |
| 278 | } |
| 279 | } |
| 280 | |
| 281 | return $retval; |
| 282 | } |
| 283 | |
| 284 | function _parse_sii_reponse_lines(&$response, &$csv, &$ticketsok, &$ticketsko){ |
| 285 | $retval = ApplicationError::Success; |
| 286 | |
| 287 | if (success($retval)){ |
| 288 | if (isset($response->RespuestaLinea)){ |
| 289 | $csv = (string)$response->CSV; |
| 290 | $lines = $response->RespuestaLinea; |
| 291 | |
| 292 | addlog(__FILE__, LogLevel::Information, "Returned CSV code: [".$response->CSV."] (".count($lines)." lines included)"); |
| 293 | foreach($lines as $line){ |
| 294 | $data = $line->children('siiR', true); |
| 295 | $ivce = (string)$data->IDFactura->children('sii', true)->NumSerieFacturaEmisor; |
| 296 | $isok = (string)$data->EstadoRegistro == 'Correcto'; |
| 297 | |
| 298 | addlog(__FILE__, $isok ? LogLevel::Information : LogLevel::Warning, "Invoice [".$ivce."] - (".($isok ? "OK" : "KO").")"); |
| 299 | if ($isok){ |
| 300 | array_push($ticketsok, $ivce); |
| 301 | } |
| 302 | else { |
| 303 | array_push($ticketsko, $ivce); |
| 304 | } |
| 305 | } |
| 306 | } |
| 307 | } |
| 308 | |
| 309 | return $retval; |
| 310 | } |
| 311 | |
| 312 | function _parse_sii_onerror_lines(&$response, &$ticketsok, &$ticketsko){ |
| 313 | $retval = ApplicationError::Success; |
| 314 | |
| 315 | if (success($retval)){ |
| 316 | if (isset($response->RespuestaLinea)){ |
| 317 | $lines = $response->RespuestaLinea; |
| 318 | |
| 319 | foreach($lines as $line){ |
| 320 | $data = $line->children('siiR', true); |
| 321 | $ivce = (string)$data->IDFactura->children('sii', true)->NumSerieFacturaEmisor; |
| 322 | $isok = (string)$data->EstadoRegistro == 'Correcto'; |
| 323 | |
| 324 | $errormesg = "Correcto"; |
| 325 | if (!$isok){ |
| 326 | $errormesg = "[".(string)$data->CodigoErrorRegistro."]: '".(string)$data->DescripcionErrorRegistro."'"; |
| 327 | } |
| 328 | |
| 329 | addlog(__FILE__, $isok ? LogLevel::Information : LogLevel::Error, "Invoice [".$ivce."] - (".($isok ? "OK" : "KO - ".$errormesg).")"); |
| 330 | if ($isok){ |
| 331 | array_push($ticketsok, [ $ivce, $errormesg ]); |
| 332 | } |
| 333 | else { |
| 334 | array_push($ticketsko, [ $ivce, $errormesg ]); |
| 335 | } |
| 336 | } |
| 337 | } |
| 338 | } |
| 339 | |
| 340 | return $retval; |
| 341 | } |
| 342 | |
| 343 | function _parse_sii_response(&$response, &$csv, &$ticketsok, &$ticketsko){ |
| 344 | $retval = ApplicationError::Success; |
| 345 | |
| 346 | if (success($retval)){ |
| 347 | $xmlstring = preg_replace('(<\?.*?\?>)', '', $response); // remove XML declaration |
| 348 | |
| 349 | libxml_use_internal_errors(true); |
| 350 | $xml = simplexml_load_string($xmlstring); |
| 351 | if ($xml === false){ |
| 352 | addlog(__FILE__, LogLevel::Error, "Failed loading XML from response"); |
| 353 | foreach(libxml_get_errors() as $error) { |
| 354 | addlog(__FILE__, LogLevel::Error, "\t".$error->message); |
| 355 | } |
| 356 | |
| 357 | $retval = ApplicationError::Invalid; |
| 358 | } |
| 359 | else { |
| 360 | $body = $xml->children('env', true)->Body->children('env', true); |
| 361 | if ($body->Fault){ |
| 362 | $fault = $body->Fault->children(); |
| 363 | if ($fault){ // control fault error |
| 364 | addlog(__FILE__, LogLevel::Error, "Error respose received from server:"); |
| 365 | |
| 366 | addlog(__FILE__, LogLevel::Error, "\t[faultcode] ".$fault->faultcode); |
| 367 | addlog(__FILE__, LogLevel::Error, "\t[faultstring] ".$fault->faultstring); |
| 368 | |
| 369 | $retval = ApplicationError::Invalid; |
| 370 | } |
| 371 | } |
| 372 | else { |
| 373 | $body = $xml->children('env', true)->Body->children('siiR', true); |
| 374 | |
| 375 | $response = null; |
| 376 | |
| 377 | // registration or modification of invoices |
| 378 | if (isset($body->RespuestaLRFacturasEmitidas)){ |
| 379 | $response = $body->RespuestaLRFacturasEmitidas->children('siiR', true); |
| 380 | |
| 381 | if ($response){ |
| 382 | $success = in_array((string)$response->EstadoEnvio, ['Correcto', 'ParcialmenteCorrecto']); |
| 383 | addlog(__FILE__, ($success) ? LogLevel::Information : LogLevel::Error, "Sent status: [".(string)$response->EstadoEnvio."]"); |
| 384 | |
| 385 | if ($success){ |
| 386 | $retval = _parse_sii_reponse_lines($response, $csv, $ticketsok, $ticketsko); |
| 387 | } |
| 388 | else { |
| 389 | _parse_sii_onerror_lines($response, $ticketsok, $ticketsko); |
| 390 | $retval = ApplicationError::Generic; |
| 391 | } |
| 392 | } |
| 393 | } |
| 394 | |
| 395 | // invoice cancellations |
| 396 | if (isset($body->RespuestaLRBajaFacturasEmitidas)){ |
| 397 | $response = $body->RespuestaLRBajaFacturasEmitidas->children('siiR', true); |
| 398 | if ($response){ |
| 399 | $success = in_array((string)$response->EstadoEnvio, ['Correcto', 'ParcialmenteCorrecto']); |
| 400 | addlog(__FILE__, ($success) ? LogLevel::Information : LogLevel::Error, "Sent status: [".(string)$response->EstadoEnvio."]"); |
| 401 | |
| 402 | if ($success){ |
| 403 | $retval = _parse_sii_reponse_lines($response, $csv, $ticketsok, $ticketsko); |
| 404 | } |
| 405 | else { |
| 406 | $retval = ApplicationError::Generic; |
| 407 | } |
| 408 | } |
| 409 | } |
| 410 | } |
| 411 | } |
| 412 | } |
| 413 | |
| 414 | return $retval; |
| 415 | } |
| 416 | |
| 417 | function update_ticketsii_info(\ConxHelper $conx, $transaction, $session, $siinfo, &$change, &$updates = null){ |
| 418 | $retval = ApplicationError::Success; |
| 419 | |
| 420 | $conn = $conx->tenant; |
| 421 | if ($conn === null) { |
| 422 | return ApplicationError::Database; |
| 423 | } |
| 424 | |
| 425 | if (success($retval)){ |
| 426 | // update information in the ticket change |
| 427 | if ($change['last']['ticket']){ |
| 428 | if (!array_key_exists('objid', $change['last']) && ($change['last']['objid'] != null)){ |
| 429 | return $retval; // nothing to do: there is no related change |
| 430 | } |
| 431 | |
| 432 | $chgid = $change['last']['objid']; |
| 433 | $iteminfo = [ |
| 434 | 'tckchg' => $chgid, |
| 435 | 'region' => $siinfo['region'], |
| 436 | 'sent' => $siinfo['sent'], |
| 437 | 'error' => null, |
| 438 | 'valid' => null |
| 439 | ]; |
| 440 | |
| 441 | $ivce = $change['last']['series'].$change['last']['invoice']; |
| 442 | if (in_array($ivce, $siinfo['ok'])){ |
| 443 | $iteminfo['valid'] = $siinfo['control']; |
| 444 | } |
| 445 | |
| 446 | if (in_array($ivce, $siinfo['ko'])){ |
| 447 | $iteminfo['error'] = $siinfo['control']; |
| 448 | } |
| 449 | |
| 450 | $results = []; |
| 451 | $res = $conn->query('TICKETSII', [ 'tckchg' => $change['last']['objid']], $results); |
| 452 | if ($res){ |
| 453 | if (count($results) > 0){ |
| 454 | $iteminfo['objid'] = $results[0]['objid']; |
| 455 | $res = $transaction->update('TICKETSII', $iteminfo); |
| 456 | } |
| 457 | else { |
| 458 | $res = $transaction->insert('TICKETSII', $iteminfo, $iteminfo['objid']); |
| 459 | } |
| 460 | } |
| 461 | |
| 462 | if (!$res){ |
| 463 | $retval = ApplicationError::Database; |
| 464 | } |
| 465 | else if ($updates !== null && isset($iteminfo['objid'])){ |
| 466 | $change = [ |
| 467 | 'table' => 'TICKETSII', |
| 468 | 'objid' => $iteminfo['objid'], |
| 469 | ]; |
| 470 | |
| 471 | $retval = push_change($conx, $change, $updates); |
| 472 | } |
| 473 | } |
| 474 | |
| 475 | // update information in the account invoice |
| 476 | else { |
| 477 | $ivcid = $change['last']['invceac']; |
| 478 | $iteminfo = [ |
| 479 | 'invceac' => $ivcid, |
| 480 | 'region' => $siinfo['region'], |
| 481 | 'sent' => $siinfo['sent'], |
| 482 | 'error' => null, |
| 483 | 'valid' => null |
| 484 | ]; |
| 485 | |
| 486 | $ivce = $change['last']['series'].$change['last']['invoice']; |
| 487 | if (in_array($ivce, $siinfo['ok'])){ |
| 488 | $iteminfo['valid'] = $siinfo['control']; |
| 489 | } |
| 490 | |
| 491 | if (in_array($ivce, $siinfo['ko'])){ |
| 492 | $iteminfo['error'] = $siinfo['control']; |
| 493 | } |
| 494 | |
| 495 | $results = []; |
| 496 | $res = $conn->query('TICKETSII', [ 'invceac' => $change['last']['invceac']], $results); |
| 497 | if ($res){ |
| 498 | if (count($results) > 0){ |
| 499 | $iteminfo['objid'] = $results[0]['objid']; |
| 500 | $res = $transaction->update('TICKETSII', $iteminfo); |
| 501 | } |
| 502 | else { |
| 503 | $res = $transaction->insert('TICKETSII', $iteminfo, $iteminfo['objid']); |
| 504 | } |
| 505 | } |
| 506 | |
| 507 | if (!$res){ |
| 508 | $retval = ApplicationError::Database; |
| 509 | } |
| 510 | else if ($updates !== null && isset($iteminfo['objid'])){ |
| 511 | $change = [ |
| 512 | 'table' => 'TICKETSII', |
| 513 | 'objid' => $iteminfo['objid'], |
| 514 | ]; |
| 515 | |
| 516 | $retval = push_change($conx, $change, $updates); |
| 517 | } |
| 518 | } |
| 519 | } |
| 520 | |
| 521 | if (success($retval)){ |
| 522 | $retval = register_ticketsii_activity($conx, $session, $iteminfo); |
| 523 | } |
| 524 | |
| 525 | if (success($retval) && isset($change['last']['ticket']) && ($change['last']['ticket'] != null)){ |
| 526 | $retval = ticketdata_update_fiscal($conx, (int)$change['last']['ticket']); |
| 527 | } |
| 528 | |
| 529 | return $retval; |
| 530 | } |
| 531 | |
| 532 | function _sendsii_substituted_ticket_is_valid(?string $series, ?string $invoice){ |
| 533 | $series = trim((string) $series); |
| 534 | $invoice = trim((string) $invoice); |
| 535 | if (($series === '') || ($invoice === '') || ($series === 'null') || ($invoice === 'null')){ |
| 536 | return false; |
| 537 | } |
| 538 | |
| 539 | $first = strtoupper($invoice[0]); |
| 540 | |
| 541 | return ($first === 'S') || ($first === 'T'); |
| 542 | } |
| 543 | |
| 544 | function _sendsii_substituted_ticket_expedition_ts(\ConxHelper $conx, array $ticketRow){ |
| 545 | $ts = 0; |
| 546 | $paidon = $ticketRow['paidon'] ?? null; |
| 547 | if ($paidon){ |
| 548 | $paidTs = strtotime((string) $paidon); |
| 549 | if ($paidTs){ |
| 550 | $ts = $paidTs; |
| 551 | } |
| 552 | } |
| 553 | |
| 554 | $ticketId = $ticketRow['objid'] ?? null; |
| 555 | if ($ticketId !== null && $conx->tenant !== null){ |
| 556 | $sql = "select TICKETCHANGE.created from TICKETCHANGE "; |
| 557 | $sql .= "where TICKETCHANGE.ticket = ".(int) $ticketId." "; |
| 558 | $sql .= "and TICKETCHANGE.reason in ('S','T','F') "; |
| 559 | $sql .= "order by TICKETCHANGE.objid desc limit 1"; |
| 560 | |
| 561 | $results = []; |
| 562 | $res = $conx->tenant->runsql(['TICKETCHANGE'], $sql, $results); |
| 563 | if ($res && count($results) > 0 && !empty($results[0]['created'])){ |
| 564 | $chgTs = strtotime((string) $results[0]['created']); |
| 565 | if ($chgTs && ($chgTs > $ts)){ |
| 566 | $ts = $chgTs; |
| 567 | } |
| 568 | } |
| 569 | } |
| 570 | |
| 571 | if (!$ts){ |
| 572 | $updated = $ticketRow['updated'] ?? null; |
| 573 | if ($updated){ |
| 574 | $updTs = strtotime((string) $updated); |
| 575 | if ($updTs){ |
| 576 | $ts = $updTs; |
| 577 | } |
| 578 | } |
| 579 | } |
| 580 | |
| 581 | return $ts; |
| 582 | } |
| 583 | |
| 584 | /** |
| 585 | * @param array<int, array<string, mixed>> $results |
| 586 | */ |
| 587 | function _sendsii_enrich_recap_substituted(\ConxHelper $conx, array &$results){ |
| 588 | if ($conx->tenant === null){ |
| 589 | return; |
| 590 | } |
| 591 | |
| 592 | $invceacIds = []; |
| 593 | foreach ($results as $change){ |
| 594 | if (!empty($change['invceac'])){ |
| 595 | $invceacIds[] = (int) $change['invceac']; |
| 596 | } |
| 597 | } |
| 598 | |
| 599 | if (count($invceacIds) === 0){ |
| 600 | return; |
| 601 | } |
| 602 | |
| 603 | $invceacIds = array_values(array_unique($invceacIds)); |
| 604 | $idList = implode(',', $invceacIds); |
| 605 | |
| 606 | $sql = "select TICKET.objid, TICKET.invceac, TICKET.series, TICKET.invceno, TICKET.paidon, TICKET.updated "; |
| 607 | $sql .= "from TICKET "; |
| 608 | $sql .= "where TICKET.invceac in (".$idList.") "; |
| 609 | $sql .= "and TICKET.status not in ('DE', 'CC') "; |
| 610 | $sql .= "order by TICKET.invceac, TICKET.paidon"; |
| 611 | |
| 612 | addlog(__FILE__, LogLevel::Debug, "SII F3 recap substituted tickets query: ".PHP_EOL.$sql.PHP_EOL); |
| 613 | |
| 614 | $tickets = []; |
| 615 | $res = $conx->tenant->runsql(['TICKET'], $sql, $tickets); |
| 616 | if (!$res){ |
| 617 | addlog(__FILE__, LogLevel::Error, "SII F3 recap: failed to load substituted tickets for invceac [".$idList."]"); |
| 618 | return; |
| 619 | } |
| 620 | |
| 621 | $byInvceac = []; |
| 622 | foreach ($tickets as $row){ |
| 623 | $ivcid = $row['invceac']; |
| 624 | if (!_sendsii_substituted_ticket_is_valid($row['series'] ?? null, $row['invceno'] ?? null)){ |
| 625 | addlog(__FILE__, LogLevel::Warning, "SII F3 recap: ticket [".($row['objid'] ?? '?')."] ignored (invalid series/invceno for simplified S/T)"); |
| 626 | continue; |
| 627 | } |
| 628 | |
| 629 | $expeditionTs = _sendsii_substituted_ticket_expedition_ts($conx, $row); |
| 630 | if (!$expeditionTs){ |
| 631 | addlog(__FILE__, LogLevel::Warning, "SII F3 recap: ticket [".($row['objid'] ?? '?')."] ignored (no expedition date)"); |
| 632 | continue; |
| 633 | } |
| 634 | |
| 635 | if (!array_key_exists($ivcid, $byInvceac)){ |
| 636 | $byInvceac[$ivcid] = []; |
| 637 | } |
| 638 | |
| 639 | $byInvceac[$ivcid][] = [ |
| 640 | 'series' => trim((string) $row['series']), |
| 641 | 'invoice' => trim((string) $row['invceno']), |
| 642 | 'created' => date('Y-m-d H:i:s', $expeditionTs), |
| 643 | '_ts' => $expeditionTs, |
| 644 | ]; |
| 645 | } |
| 646 | |
| 647 | foreach ($results as &$change){ |
| 648 | if (empty($change['invceac'])){ |
| 649 | continue; |
| 650 | } |
| 651 | |
| 652 | $ivcid = $change['invceac']; |
| 653 | $entries = $byInvceac[$ivcid] ?? []; |
| 654 | $substituted = []; |
| 655 | $maxTs = 0; |
| 656 | |
| 657 | foreach ($entries as $entry){ |
| 658 | $substituted[] = [ |
| 659 | 'series' => $entry['series'], |
| 660 | 'invoice' => $entry['invoice'], |
| 661 | 'created' => $entry['created'], |
| 662 | ]; |
| 663 | if ($entry['_ts'] > $maxTs){ |
| 664 | $maxTs = $entry['_ts']; |
| 665 | } |
| 666 | } |
| 667 | |
| 668 | $change['substituted'] = $substituted; |
| 669 | if ($maxTs > 0){ |
| 670 | $change['substituted_period'] = [ |
| 671 | 'year' => date('Y', $maxTs), |
| 672 | 'month' => date('m', $maxTs), |
| 673 | ]; |
| 674 | } |
| 675 | } unset($change); |
| 676 | } |
| 677 | |
| 678 | /** |
| 679 | * @param array<int, array<string, mixed>> $to_create |
| 680 | */ |
| 681 | function _sendsii_omit_recap_without_substituted(array &$to_create, array &$ticketsko){ |
| 682 | $valid = []; |
| 683 | |
| 684 | foreach ($to_create as &$change){ |
| 685 | $last = $change['last']; |
| 686 | if (($last['reason'] ?? '') !== 'A'){ |
| 687 | $valid[] = &$change; |
| 688 | continue; |
| 689 | } |
| 690 | |
| 691 | $substituted = $last['substituted'] ?? []; |
| 692 | if (count($substituted) > 0){ |
| 693 | $valid[] = &$change; |
| 694 | continue; |
| 695 | } |
| 696 | |
| 697 | $ivce = $last['series'].$last['invoice']; |
| 698 | addlog(__FILE__, LogLevel::Warning, "SII F3 recap [".$ivce."] omitted from batch: no valid substituted tickets"); |
| 699 | $ticketsko[] = $ivce; |
| 700 | } unset($change); |
| 701 | |
| 702 | $to_create = $valid; |
| 703 | } |
| 704 | |
| 705 | /** |
| 706 | * Envío por lotes SII (AEAT) para un periodo de auditoría cerrado. |
| 707 | * |
| 708 | * @return array{errorcode: int, ticketsok: mixed, ticketsko: mixed} |
| 709 | */ |
| 710 | function sendsii_run(\ConxHelper $conx, string $device, string $sessionToken, string $placeId, string $auditId): array |
| 711 | { |
| 712 | $GLOBALS['__SII_ENRICH_CONX'] = $conx; |
| 713 | |
| 714 | $retval = ApplicationError::Success; |
| 715 | $userid = null; |
| 716 | $sessionObjid = null; |
| 717 | |
| 718 | $ticketsok = []; |
| 719 | $ticketsko = []; |
| 720 | $results = []; |
| 721 | $indexed = []; |
| 722 | $to_create = []; |
| 723 | $to_update = []; |
| 724 | $to_cancel = []; |
| 725 | $region = null; |
| 726 | $place = null; |
| 727 | $siiurl = null; |
| 728 | $siixsd = []; |
| 729 | $xml_post_string = ''; |
| 730 | $result = []; |
| 731 | $certval = null; |
| 732 | $certkey = null; |
| 733 | $tmp_certval = null; |
| 734 | $tmp_certkey = null; |
| 735 | $ini_date = 0; |
| 736 | $end_date = ''; |
| 737 | $csv = null; |
| 738 | /** @var array<string, mixed> $auditRow */ |
| 739 | $auditRow = []; |
| 740 | |
| 741 | if (success($retval)) { |
| 742 | if ($device === '') { |
| 743 | addlog(__FILE__, LogLevel::Error, 'No device provided on URL. Operation cancelled.'); |
| 744 | $retval = ApplicationError::Parameters; |
| 745 | } |
| 746 | } |
| 747 | |
| 748 | if (success($retval)) { |
| 749 | if ($sessionToken === '') { |
| 750 | addlog(__FILE__, LogLevel::Error, 'No session provided on URL. Operation cancelled.'); |
| 751 | $retval = ApplicationError::Parameters; |
| 752 | } else { |
| 753 | $retval = check_session($conx, $sessionToken, $device, $userid, $sessionObjid); |
| 754 | } |
| 755 | } |
| 756 | |
| 757 | if (success($retval)) { |
| 758 | if ($placeId === '' || $placeId === '0') { |
| 759 | addlog(__FILE__, LogLevel::Error, "Mandatory parameter 'place' not provided. Operation cancelled."); |
| 760 | $retval = ApplicationError::Parameters; |
| 761 | } |
| 762 | } |
| 763 | |
| 764 | if (success($retval)) { |
| 765 | if ($auditId === '' || $auditId === '0') { |
| 766 | addlog(__FILE__, LogLevel::Error, "Mandatory parameter 'audit' not provided. Operation cancelled."); |
| 767 | $retval = ApplicationError::Parameters; |
| 768 | } |
| 769 | } |
| 770 | |
| 771 | if (!success($retval)) { |
| 772 | unset($GLOBALS['__SII_ENRICH_CONX']); |
| 773 | |
| 774 | return [ |
| 775 | 'errorcode' => $retval, |
| 776 | 'ticketsok' => $ticketsok, |
| 777 | 'ticketsko' => $ticketsko, |
| 778 | ]; |
| 779 | } |
| 780 | |
| 781 | $conx->tenant = $placeId; |
| 782 | $conn = $conx->getTenant(); |
| 783 | if ($conn === null) { |
| 784 | addlog(__FILE__, LogLevel::Error, 'Tenant connection not available for audit SII.'); |
| 785 | unset($GLOBALS['__SII_ENRICH_CONX']); |
| 786 | return [ |
| 787 | 'errorcode' => ApplicationError::Database, |
| 788 | 'ticketsok' => $ticketsok, |
| 789 | 'ticketsko' => $ticketsko, |
| 790 | ]; |
| 791 | } |
| 792 | |
| 793 | // get the audit information |
| 794 | if (success($retval)){ |
| 795 | $results = []; |
| 796 | $res = $conn->query('AUDIT', [ 'objid' => $auditId ], $results); |
| 797 | if ($res){ |
| 798 | if (count($results) > 0){ |
| 799 | $auditRow = $results[0]; |
| 800 | } |
| 801 | else { |
| 802 | addlog(__FILE__, LogLevel::Error, "No audit entry found with objid '".$auditId."'"); |
| 803 | $retval = ApplicationError::NotFound; |
| 804 | } |
| 805 | } |
| 806 | else { |
| 807 | $retval = ApplicationError::Database; |
| 808 | } |
| 809 | } |
| 810 | |
| 811 | // get the audit period |
| 812 | if (success($retval)){ |
| 813 | $sql = "select * from AUDIT where created < '".$auditRow['created']."' order by created desc limit 1"; |
| 814 | |
| 815 | $results = []; |
| 816 | $res = $conn->runsql([ 'AUDIT'], $sql, $results); |
| 817 | if ($res){ |
| 818 | $ini_date = (count($results) > 0) ? $results[0]['created'] : 0; |
| 819 | $end_date = $auditRow['created']; |
| 820 | |
| 821 | addlog(__FILE__, LogLevel::Information, "Audit period [".$ini_date." - ".$end_date."]"); |
| 822 | } |
| 823 | else { |
| 824 | $retval = ApplicationError::Database; |
| 825 | } |
| 826 | } |
| 827 | |
| 828 | // get the taxrate from the place configuration |
| 829 | if (success($retval)){ |
| 830 | $results = []; |
| 831 | $res = $conn->query('PLACEOPT', [ 'opkey' => 'optionTaxRate'], $results); |
| 832 | if ($res){ |
| 833 | if (count($results) > 0){ |
| 834 | $taxrate = $results[0]['value']; |
| 835 | } |
| 836 | else { |
| 837 | $taxrate = AppConstants::Taxes_Default; |
| 838 | } |
| 839 | } |
| 840 | else { |
| 841 | $retval = ApplicationError::Database; |
| 842 | } |
| 843 | } |
| 844 | |
| 845 | // get all the ticket changes in the audit period |
| 846 | if (success($retval)){ |
| 847 | $sql = "select TICKETCHANGE.*, "; |
| 848 | $sql .= "concat('T', TICKETCHANGE.ticket) as toindex, "; // to index all changes |
| 849 | $sql .= "TICKETSII.objid as ticketsii, "; // relation with the ticketsii |
| 850 | $sql .= "TICKETSII.invceac as invceac "; // this will be null |
| 851 | $sql .= "from TICKETCHANGE "; |
| 852 | $sql .= "inner join TICKET on TICKETCHANGE.ticket = TICKET.objid "; |
| 853 | $sql .= "left join TICKETSII on TICKETSII.tckchg = TICKETCHANGE.objid and TICKETSII.valid is not null "; |
| 854 | $sql .= "where TICKET.invceac is NULL "; // discard paid to account tickets |
| 855 | $sql .= "and TICKET.paidon is not NULL "; // paid tickets |
| 856 | $sql .= "and ("; |
| 857 | |
| 858 | // paid tickets in the audit period or |
| 859 | $sql .= "(TICKET.status not in ('DE', 'CC') and "; |
| 860 | $sql .= "TICKET.paidon > '".$ini_date."' and TICKET.paidon <= '".$end_date."') or "; |
| 861 | |
| 862 | // cancelled tickets in the audit period |
| 863 | $sql .= "(TICKET.status in ('DE', 'CC') and "; |
| 864 | $sql .= "TICKET.updated > '".$ini_date."' and TICKET.updated <= '".$end_date."' and TICKETSII.objid is not null)"; |
| 865 | |
| 866 | $sql .= ") "; |
| 867 | $sql .= "order by TICKETCHANGE.objid desc"; |
| 868 | |
| 869 | addlog(__FILE__, LogLevel::Debug, "Get tickets query (1): ".PHP_EOL.$sql.PHP_EOL); |
| 870 | |
| 871 | $results = []; |
| 872 | $res = $conn->runsql([ 'TICKET', 'TICKETCHANGE', 'TICKETSII' ], $sql, $results); |
| 873 | if (!$res){ |
| 874 | $retval = ApplicationError::Database; |
| 875 | } |
| 876 | } |
| 877 | |
| 878 | // ensure all tickets has al least one change (support for older versions) |
| 879 | if (success($retval)){ |
| 880 | $indexed = []; |
| 881 | |
| 882 | foreach($results as &$change){ |
| 883 | if (!array_key_exists($change['toindex'], $indexed)){ |
| 884 | $indexed[$change['toindex']] = []; |
| 885 | } |
| 886 | $indexed[$change['toindex']][] = &$change; |
| 887 | } unset($change); |
| 888 | |
| 889 | $sql = "select TICKET.*, TICKETINVOICE.client as client "; |
| 890 | $sql .= "from TICKET "; |
| 891 | $sql .= "left join TICKETINVOICE on TICKETINVOICE.ticket = TICKET.objid "; |
| 892 | $sql .= "where TICKET.paidon is not NULL "; // discard paid to account tickets |
| 893 | $sql .= "and TICKET.invceac is NULL "; // paid tickets |
| 894 | $sql .= "and TICKET.status not in ('DE', 'CC') "; |
| 895 | $sql .= "and TICKET.paidon > '".$ini_date."' and TICKET.paidon <= '".$end_date."'"; |
| 896 | |
| 897 | addlog(__FILE__, LogLevel::Debug, "Get all tickets query (2): ".PHP_EOL.$sql.PHP_EOL); |
| 898 | |
| 899 | $tickets = []; |
| 900 | $res = $conn->runsql([ 'TICKET' ], $sql, $tickets); |
| 901 | if ($res){ |
| 902 | foreach($tickets as &$ticket){ |
| 903 | if (!array_key_exists($ticket['objid'], $indexed)){ |
| 904 | $total = $ticket['price']; |
| 905 | $rate = $ticket['taxrate'] ? $ticket['taxrate'] : $taxrate; |
| 906 | $taxes = $total / ((100 + $rate) / $rate); |
| 907 | $base = $total - $taxes; |
| 908 | |
| 909 | $change = [ |
| 910 | 'ticket' => $ticket['objid'], |
| 911 | 'invceac' => null, |
| 912 | 'toindex' => 'T'.$ticket['objid'], |
| 913 | 'created' => $ticket['updated'], |
| 914 | 'prev' => null, |
| 915 | 'reason' => 'F', |
| 916 | 'series' => $ticket['series'], |
| 917 | 'invoice' => $ticket['invceno'], |
| 918 | 'total' => $total, |
| 919 | 'base' => $base, |
| 920 | 'rate' => $rate, |
| 921 | 'taxes' => $taxes, |
| 922 | 'client' => $ticket['client'], |
| 923 | 'ticketsii' => null |
| 924 | ]; |
| 925 | |
| 926 | $results[] = $change; // add to the changes resultset |
| 927 | } |
| 928 | } unset($ticket); |
| 929 | } |
| 930 | else { |
| 931 | $retval = ApplicationError::Database; |
| 932 | } |
| 933 | } |
| 934 | |
| 935 | // get the cummulative invoices as changes to be sent |
| 936 | if (success($retval)){ |
| 937 | $sql = "select ACCOUNTINVOICE.*, PAYACCOUNT.client as client from ACCOUNTINVOICE "; |
| 938 | $sql .=" inner join PAYACCOUNT on ACCOUNTINVOICE.account = PAYACCOUNT.objid "; |
| 939 | $sql .= "where ACCOUNTINVOICE.created > '".$ini_date."' and ACCOUNTINVOICE.created <= '".$end_date."'"; |
| 940 | |
| 941 | addlog(__FILE__, LogLevel::Debug, "Get all account invoices query (3): ".PHP_EOL.$sql.PHP_EOL); |
| 942 | |
| 943 | $invceacs = []; |
| 944 | $res = $conn->runsql([ 'ACCOUNTINVOICE', 'PAYACCOUNT' ], $sql, $invceacs); |
| 945 | if (!$res){ |
| 946 | $retval = ApplicationError::Database; |
| 947 | } |
| 948 | else { |
| 949 | foreach($invceacs as &$invceac){ |
| 950 | $total = $invceac['amount']; |
| 951 | $rate = $taxrate; |
| 952 | $taxes = $total / ((100 + $rate) / $rate); |
| 953 | $base = $total - $taxes; |
| 954 | |
| 955 | $change = [ |
| 956 | 'ticket' => null, |
| 957 | 'invceac' => $invceac['objid'], |
| 958 | 'toindex' => 'A'.$invceac['objid'], |
| 959 | 'created' => $invceac['created'], |
| 960 | 'prev' => null, |
| 961 | 'reason' => 'A', |
| 962 | 'series' => $invceac['series'], |
| 963 | 'invoice' => $invceac['invoice'], |
| 964 | 'total' => $total, |
| 965 | 'base' => $base, |
| 966 | 'rate' => $rate, |
| 967 | 'taxes' => $taxes, |
| 968 | 'client' => $invceac['client'], |
| 969 | 'ticketsii' => null |
| 970 | ]; |
| 971 | |
| 972 | $results[] = $change; // add to the changes resultset |
| 973 | } unset($invceac); |
| 974 | } |
| 975 | } |
| 976 | |
| 977 | // get all the changes client data |
| 978 | if (success($retval)){ |
| 979 | $indexed = []; |
| 980 | |
| 981 | $clntids = []; |
| 982 | foreach($results as &$change){ |
| 983 | if ($change['client']){ |
| 984 | // keep the client objid |
| 985 | if (!array_key_exists($change['client'], $clntids)){ |
| 986 | $clntids[] = $change['client']; |
| 987 | } |
| 988 | |
| 989 | // keep the change for client |
| 990 | if (!array_key_exists($change['client'], $indexed)){ |
| 991 | $indexed[$change['client']] = []; |
| 992 | } |
| 993 | $indexed[$change['client']][] = &$change; |
| 994 | } |
| 995 | } unset($change); |
| 996 | |
| 997 | if (count($clntids) > 0){ |
| 998 | $clients = []; |
| 999 | $res = $conn->query('INVOICEBUSINESS', [ 'objid' => $clntids ], $clients); |
| 1000 | if ($res){ |
| 1001 | foreach($clients as &$client){ |
| 1002 | foreach($indexed[$client['objid']] as &$change){ |
| 1003 | $change['client'] = &$client; |
| 1004 | } unset($change); |
| 1005 | } unset($client); |
| 1006 | } |
| 1007 | else { |
| 1008 | $retval = ApplicationError::Database; |
| 1009 | } |
| 1010 | } |
| 1011 | } |
| 1012 | |
| 1013 | if (success($retval)){ |
| 1014 | _sendsii_enrich_recap_substituted($conx, $results); |
| 1015 | } |
| 1016 | |
| 1017 | // get the last change to be updated and the last change sent (if any) |
| 1018 | if (success($retval)){ |
| 1019 | $indexed = []; |
| 1020 | foreach($results as &$change){ |
| 1021 | if (!array_key_exists($change['toindex'], $indexed)){ |
| 1022 | $indexed[$change['toindex']] = [ |
| 1023 | 'sent' => null, // sent to SII change |
| 1024 | 'last' => null, // last recorded change |
| 1025 | ]; |
| 1026 | } |
| 1027 | |
| 1028 | // keep the last recorded ticket change |
| 1029 | if ($indexed[$change['toindex']]['last'] == null) { |
| 1030 | $indexed[$change['toindex']]['last'] = &$change; |
| 1031 | } |
| 1032 | |
| 1033 | // keep the last sent to sii change |
| 1034 | if ($indexed[$change['toindex']]['sent'] == null) { |
| 1035 | if ($change['ticketsii'] != null){ |
| 1036 | $indexed[$change['toindex']]['sent'] = &$change; |
| 1037 | } |
| 1038 | } |
| 1039 | } unset($change); |
| 1040 | } |
| 1041 | |
| 1042 | // separate the changes in (to create / to modify / to cancel) |
| 1043 | if (success($retval)){ |
| 1044 | $to_create = []; |
| 1045 | $to_update = []; |
| 1046 | $to_cancel = []; |
| 1047 | |
| 1048 | foreach($indexed as &$change){ |
| 1049 | $last = &$change['last']; |
| 1050 | $sent = &$change['sent']; |
| 1051 | |
| 1052 | if ($sent == null){ // not sent to SII |
| 1053 | if ($last['reason'] != 'C'){ |
| 1054 | $to_create[] = &$change; |
| 1055 | } |
| 1056 | } |
| 1057 | else { // already sent to sii |
| 1058 | if ($last['objid'] != $sent['objid']){ |
| 1059 | if ($last['reason'] != 'C'){ |
| 1060 | $to_update[] = &$change; |
| 1061 | } |
| 1062 | else { |
| 1063 | $to_cancel[] = &$change; |
| 1064 | } |
| 1065 | } |
| 1066 | } |
| 1067 | } unset($change); |
| 1068 | } |
| 1069 | |
| 1070 | if (success($retval)){ |
| 1071 | _sendsii_omit_recap_without_substituted($to_create, $ticketsko); |
| 1072 | } |
| 1073 | |
| 1074 | // Log the ticket changes to be updated |
| 1075 | if (success($retval)){ |
| 1076 | addlog(__FILE__, LogLevel::Information, "TICKETS TO CREATE (".count($to_create).")"); |
| 1077 | foreach($to_create as &$change){ |
| 1078 | addlog(__FILE__, LogLevel::Information, "\tTICKET [".$change['last']['series'].$change['last']['invoice']."] - updated: '".$change['last']['created']."'"); |
| 1079 | } unset($change); |
| 1080 | |
| 1081 | addlog(__FILE__, LogLevel::Information, "TICKETS TO UPDATE (".count($to_update).")"); |
| 1082 | foreach($to_update as &$change){ |
| 1083 | addlog(__FILE__, LogLevel::Information, "\tTICKET [".$change['last']['series'].$change['last']['invoice']."] - created: '".$change['sent']['created']."' - updated: '".$change['last']['created']."'"); |
| 1084 | } unset($change); |
| 1085 | |
| 1086 | addlog(__FILE__, LogLevel::Information, "TICKETS TO CANCEL (".count($to_cancel).")"); |
| 1087 | foreach($to_cancel as &$change){ |
| 1088 | addlog(__FILE__, LogLevel::Information, "\tTICKET [".$change['last']['series'].$change['last']['invoice']."] - created: '".$change['sent']['created']."' - updated: '".$change['last']['created']."'"); |
| 1089 | } unset($change); |
| 1090 | } |
| 1091 | |
| 1092 | if (count($results) > 0){ |
| 1093 | $action = "SuministroFactEmitidas"; |
| 1094 | if (AppConstants::SII_Sandbox()){ |
| 1095 | $action = "SuministroFactEmitidasPruebas"; |
| 1096 | } |
| 1097 | |
| 1098 | // obtain the place information |
| 1099 | if (success($retval)){ |
| 1100 | $results = []; |
| 1101 | $res = $conx->global->query('PLACE', [ 'objid' => $placeId ], $results); |
| 1102 | if ($res){ |
| 1103 | if (count($results) > 0){ |
| 1104 | $place = $results[0]; |
| 1105 | } |
| 1106 | else { |
| 1107 | addlog(__FILE__, LogLevel::Error, "Place not found for objid [".$placeId."]"); |
| 1108 | $retval = ApplicationError::NotFound; |
| 1109 | } |
| 1110 | } |
| 1111 | else { |
| 1112 | $retval = ApplicationError::Database; |
| 1113 | } |
| 1114 | } |
| 1115 | |
| 1116 | // Certificado de representación (único .p12 corporativo, igual que VeriFactu — no PEM por local) |
| 1117 | if (success($retval)){ |
| 1118 | $certval = null; |
| 1119 | $certkey = null; |
| 1120 | $retval = _sii_representation_certificate($certval, $certkey); |
| 1121 | } |
| 1122 | |
| 1123 | // obtain the place fiscal region (if located in Spain) |
| 1124 | if (success($retval)){ |
| 1125 | $retval = _sii_fiscal_region($conn, $place, $region); |
| 1126 | } |
| 1127 | |
| 1128 | // obtain server information, certificates and schemas |
| 1129 | if (success($retval)){ |
| 1130 | $retval = _extract_wsdl_information($region, $action, $siiurl, $siixsd); |
| 1131 | if (success($retval)){ |
| 1132 | addlog(__FILE__, LogLevel::Information, "Sending tickets (".$action.") to SII at '".$siiurl."'"); |
| 1133 | foreach($siixsd as $key => $xsd) { |
| 1134 | addlog(__FILE__, LogLevel::Information, "\t".'- xmlns:ns'.$key.'="'.$xsd); |
| 1135 | } |
| 1136 | } |
| 1137 | } |
| 1138 | |
| 1139 | // store the certificates in tempotal files to be used by CURL |
| 1140 | if (success($retval)){ |
| 1141 | $tmp_certval = null; |
| 1142 | $tmp_certkey = null; |
| 1143 | |
| 1144 | if ($certval){ |
| 1145 | $tmp_certval = tempnam(sys_get_temp_dir(), 'c_'); |
| 1146 | chmod($tmp_certval, 0600); |
| 1147 | |
| 1148 | if (!file_put_contents($tmp_certval, $certval)){ |
| 1149 | addlog(__FILE__, LogLevel::Error, "Could not write certificate contents to '".$tmp_certval."' (".error_get_last()['message'].")"); |
| 1150 | $retval = ApplicationError::Generic; |
| 1151 | } |
| 1152 | } |
| 1153 | |
| 1154 | if ($certkey){ |
| 1155 | $tmp_certkey = tempnam(sys_get_temp_dir(), 'c_'); |
| 1156 | chmod($tmp_certkey, 0600); |
| 1157 | |
| 1158 | if (!file_put_contents($tmp_certkey, $certkey)){ |
| 1159 | addlog(__FILE__, LogLevel::Error, "Could not write certificate contents to '".$tmp_certkey."' (".error_get_last()['message'].")"); |
| 1160 | $retval = ApplicationError::Generic; |
| 1161 | } |
| 1162 | } |
| 1163 | } |
| 1164 | |
| 1165 | if (count($to_create) > 0){ |
| 1166 | $offset = 0; |
| 1167 | do { |
| 1168 | $to_send = array_slice($to_create, $offset, 9999); |
| 1169 | $offset += 9999; |
| 1170 | |
| 1171 | if (count($to_send) > 0){ |
| 1172 | // build the SII XML content |
| 1173 | if (success($retval)){ |
| 1174 | $retval = _build_sii_request_AltaFacturas($place, $to_send, $siixsd, $xml_post_string); |
| 1175 | if (!success($retval)){ |
| 1176 | addlog(__FILE__, LogLevel::Error, "Error [".$retval."] received in sii request (AltaFacturas)"); |
| 1177 | } |
| 1178 | } |
| 1179 | |
| 1180 | if (success($retval)){ |
| 1181 | $retval = ticketsii_validate_sii_soap_xml($xml_post_string, $siixsd, null); |
| 1182 | if (!success($retval)){ |
| 1183 | addlog(__FILE__, LogLevel::Error, 'TICKETSII XSD validation failed before SII HTTP send (AltaFacturas)'); |
| 1184 | } |
| 1185 | } |
| 1186 | |
| 1187 | // send the XML to the SII server |
| 1188 | if (success($retval)){ |
| 1189 | $retval = _send_sii_request($xml_post_string, $siiurl, $tmp_certval, $tmp_certkey, $action, $result); |
| 1190 | } |
| 1191 | |
| 1192 | // parse the result XML |
| 1193 | if (success($retval)){ |
| 1194 | $retval = _parse_sii_response($result['content'], $csv, $ticketsok, $ticketsko); |
| 1195 | } |
| 1196 | } |
| 1197 | } while(count($to_send) > 0); |
| 1198 | } |
| 1199 | |
| 1200 | if (count($to_update) > 0){ |
| 1201 | $offset = 0; |
| 1202 | do { |
| 1203 | $to_send = array_slice($to_update, $offset, 9999); |
| 1204 | $offset += 9999; |
| 1205 | |
| 1206 | if (count($to_send) > 0){ |
| 1207 | // build the SII XML content |
| 1208 | if (success($retval)){ |
| 1209 | $retval = _build_sii_request_RectificacionFacturas($place, $to_send, $siixsd, $xml_post_string); |
| 1210 | if (!success($retval)){ |
| 1211 | addlog(__FILE__, LogLevel::Error, "Error [".$retval."] received in sii request (RectificacionFacturas)"); |
| 1212 | } |
| 1213 | } |
| 1214 | |
| 1215 | if (success($retval)){ |
| 1216 | $retval = ticketsii_validate_sii_soap_xml($xml_post_string, $siixsd, null); |
| 1217 | if (!success($retval)){ |
| 1218 | addlog(__FILE__, LogLevel::Error, 'TICKETSII XSD validation failed before SII HTTP send (RectificacionFacturas)'); |
| 1219 | } |
| 1220 | } |
| 1221 | |
| 1222 | // send the XML to the SII server |
| 1223 | if (success($retval)){ |
| 1224 | $retval = _send_sii_request($xml_post_string, $siiurl, $tmp_certval, $tmp_certkey, $action, $result); |
| 1225 | } |
| 1226 | |
| 1227 | // parse the result XML |
| 1228 | if (success($retval)){ |
| 1229 | $retval = _parse_sii_response($result['content'], $csv, $ticketsok, $ticketsko); |
| 1230 | } |
| 1231 | } |
| 1232 | } while(count($to_send) > 0); |
| 1233 | } |
| 1234 | |
| 1235 | if (count($to_cancel) > 0){ |
| 1236 | $offset = 0; |
| 1237 | do { |
| 1238 | $to_send = array_slice($to_cancel, $offset, 9999); |
| 1239 | $offset += 9999; |
| 1240 | |
| 1241 | if (count($to_send) > 0){ |
| 1242 | // build the SII XML content |
| 1243 | if (success($retval)){ |
| 1244 | $retval = _build_sii_request_BajaFacturas($place, $to_send, $siixsd, $xml_post_string); |
| 1245 | if (!success($retval)){ |
| 1246 | addlog(__FILE__, LogLevel::Error, "Error [".$retval."] received in sii request (BajaFacturas)"); |
| 1247 | } |
| 1248 | } |
| 1249 | |
| 1250 | if (success($retval)){ |
| 1251 | $retval = ticketsii_validate_sii_soap_xml($xml_post_string, $siixsd, null); |
| 1252 | if (!success($retval)){ |
| 1253 | addlog(__FILE__, LogLevel::Error, 'TICKETSII XSD validation failed before SII HTTP send (BajaFacturas)'); |
| 1254 | } |
| 1255 | } |
| 1256 | |
| 1257 | // send the XML to the SII server |
| 1258 | if (success($retval)){ |
| 1259 | $retval = _send_sii_request($xml_post_string, $siiurl, $tmp_certval, $tmp_certkey, $action, $result); |
| 1260 | } |
| 1261 | |
| 1262 | // parse the result XML |
| 1263 | if (success($retval)){ |
| 1264 | $retval = _parse_sii_response($result['content'], $csv, $ticketsok, $ticketsko); |
| 1265 | } |
| 1266 | } |
| 1267 | } while(count($to_send) > 0); |
| 1268 | } |
| 1269 | } |
| 1270 | |
| 1271 | // remove the temporal certificate files |
| 1272 | if (isset($tmp_certval) && $tmp_certval){ |
| 1273 | unlink($tmp_certval); |
| 1274 | } |
| 1275 | |
| 1276 | if (isset($tmp_certkey) && $tmp_certkey){ |
| 1277 | unlink($tmp_certkey); |
| 1278 | } |
| 1279 | |
| 1280 | if (success($retval)){ |
| 1281 | addlog(__FILE__, LogLevel::Information, "OK tickets: ".PHP_EOL.print_r($ticketsok, true).PHP_EOL); |
| 1282 | addlog(__FILE__, LogLevel::Information, "KO tickets: ".PHP_EOL.print_r($ticketsko, true).PHP_EOL); |
| 1283 | } |
| 1284 | |
| 1285 | // add SII information to the last ticket change |
| 1286 | if (success($retval) && ((count($ticketsok) > 0) || (count($ticketsko) > 0))) { |
| 1287 | $siiUpdates = []; |
| 1288 | $ok = $conx->transactional(function (\ConxHelper $conx) use ($sessionObjid, $region, $csv, $ticketsok, $ticketsko, $to_create, $to_update, $to_cancel, $placeId, &$retval, &$siiUpdates) { |
| 1289 | $transaction = $conx->trn('TICKETSII'); |
| 1290 | if ($transaction === null) { |
| 1291 | $retval = ApplicationError::Database; |
| 1292 | return false; |
| 1293 | } |
| 1294 | $sentAt = gmdate('Y-m-d H:i:s'); |
| 1295 | $siinfo = [ |
| 1296 | 'region' => $region, |
| 1297 | 'sent' => $sentAt, |
| 1298 | 'control' => $csv, |
| 1299 | 'ok' => $ticketsok, |
| 1300 | 'ko' => $ticketsko, |
| 1301 | ]; |
| 1302 | |
| 1303 | $all_changes = array_merge($to_create, $to_update, $to_cancel); |
| 1304 | foreach ($all_changes as &$change) { |
| 1305 | if (success($retval)) { |
| 1306 | $retval = update_ticketsii_info($conx, $transaction, $sessionObjid, $siinfo, $change, $siiUpdates); |
| 1307 | } |
| 1308 | } |
| 1309 | unset($change); |
| 1310 | |
| 1311 | if (success($retval)) { |
| 1312 | $retval = stage_changes($conx, $placeId, $siiUpdates); |
| 1313 | } |
| 1314 | return success($retval); |
| 1315 | }); |
| 1316 | |
| 1317 | if (success($retval) && !$ok) { |
| 1318 | $retval = ApplicationError::Database; |
| 1319 | addlog(__FILE__, LogLevel::Error, "TICKETSII commit failed at COMMIT time; treating as write failure."); |
| 1320 | } |
| 1321 | |
| 1322 | if (success($retval)) { |
| 1323 | notify_changes($conx, $sessionObjid, $siiUpdates); |
| 1324 | } |
| 1325 | } |
| 1326 | |
| 1327 | unset($GLOBALS['__SII_ENRICH_CONX']); |
| 1328 | |
| 1329 | return [ |
| 1330 | 'errorcode' => $retval, |
| 1331 | 'ticketsok' => $ticketsok, |
| 1332 | 'ticketsko' => $ticketsko, |
| 1333 | ]; |
| 1334 | } |