Test Failed
Push — master ( 59e870...659f9a )
by Esteban De La Fuente
04:33
created

validateUploadXmlResponse()   C

Complexity

Conditions 13
Paths 22

Size

Total Lines 72
Code Lines 47

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 182

Importance

Changes 0
Metric Value
cc 13
eloc 47
nc 22
nop 2
dl 0
loc 72
ccs 0
cts 50
cp 0
crap 182
rs 6.6166
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * LibreDTE: Biblioteca PHP (Núcleo).
7
 * Copyright (C) LibreDTE <https://www.libredte.cl>
8
 *
9
 * Este programa es software libre: usted puede redistribuirlo y/o modificarlo
10
 * bajo los términos de la Licencia Pública General Affero de GNU publicada por
11
 * la Fundación para el Software Libre, ya sea la versión 3 de la Licencia, o
12
 * (a su elección) cualquier versión posterior de la misma.
13
 *
14
 * Este programa se distribuye con la esperanza de que sea útil, pero SIN
15
 * GARANTÍA ALGUNA; ni siquiera la garantía implícita MERCANTIL o de APTITUD
16
 * PARA UN PROPÓSITO DETERMINADO. Consulte los detalles de la Licencia Pública
17
 * General Affero de GNU para obtener una información más detallada.
18
 *
19
 * Debería haber recibido una copia de la Licencia Pública General Affero de
20
 * GNU junto a este programa.
21
 *
22
 * En caso contrario, consulte <http://www.gnu.org/licenses/agpl.html>.
23
 */
24
25
namespace libredte\lib\Core\Package\Billing\Component\Integration\Worker;
26
27
use Derafu\Lib\Core\Foundation\Abstract\AbstractWorker;
28
use Derafu\Lib\Core\Helper\Rut;
29
use Derafu\Lib\Core\Package\Prime\Component\Certificate\Contract\CertificateInterface;
30
use Derafu\Lib\Core\Package\Prime\Component\Xml\Contract\XmlComponentInterface;
31
use Derafu\Lib\Core\Package\Prime\Component\Xml\Entity\Xml as XmlDocument;
32
use libredte\lib\Core\Package\Billing\Component\Integration\Contract\SiiDocumentSenderWorkerInterface;
33
use libredte\lib\Core\Package\Billing\Component\Integration\Contract\SiiLazyWorkerInterface;
34
use libredte\lib\Core\Package\Billing\Component\Integration\Contract\SiiTokenManagerWorkerInterface;
35
use libredte\lib\Core\Package\Billing\Component\Integration\Exception\SiiDocumentSenderException;
36
use libredte\lib\Core\Package\Billing\Component\Integration\Support\SiiConnectionOptions;
37
use libredte\lib\Core\Sii\HttpClient\SiiLazyException;
0 ignored issues
show
Bug introduced by
The type libredte\lib\Core\Sii\HttpClient\SiiLazyException was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
38
39
/**
40
 * Clase para el envío de documentos al SII.
41
 */
42
class SiiDocumentSenderWorker extends AbstractWorker implements SiiDocumentSenderWorkerInterface
43
{
44
    /**
45
     * Constructor del worker.
46
     *
47
     * @param SiiLazyWorkerInterface $lazyWorker
48
     * @param SiiTokenManagerWorkerInterface $tokenManager
49
     * @param XmlComponentInterface $xmlComponent
50
     */
51
    public function __construct(
52
        private SiiLazyWorkerInterface $lazyWorker,
53
        private SiiTokenManagerWorkerInterface $tokenManager,
54
        private XmlComponentInterface $xmlComponent
55
    ) {
56
    }
57
58
    /**
59
     * {@inheritdoc}
60
     */
61
    public function sendXml(
62
        CertificateInterface $certificate,
63
        XmlDocument $doc,
64
        string $company,
65
        bool $compress = false,
66
        ?int $retry = null
67
    ): int {
68
        // Crear string del documento XML.
69
        $xml = $doc->saveXml();
70
        if (empty($xml) || $xml == '<?xml version="1.0" encoding="ISO-8859-1"?>'."\n") {
71
            throw new SiiDocumentSenderException(
72
                'El XML que se desea enviar al SII no puede ser vacío.'
73
            );
74
        }
75
76
        // Validar los RUT que se utilizarán para el envío y descomponerlos.
77
        $sender = $certificate->getId();
78
        Rut::validate($sender);
79
        Rut::validate($company);
80
        [$rutSender, $dvSender] = Rut::toArray($sender);
81
        [$rutCompany, $dvCompany] = Rut::toArray($company);
82
83
        // Crear el archivo que se enviará en el sistema de archivos para poder
84
        // adjuntarlo en la solicitud mediante curl al SII.
85
        [$filepath, $mimetype] = $this->createXmlFile($xml, $compress);
86
        $filename = $company . '_' . basename($filepath);
87
88
        // Preparar los datos que se enviarán mediante POST al SII.
89
        $data = [
90
            'rutSender' => $rutSender,
91
            'dvSender' => $dvSender,
92
            'rutCompany' => $rutCompany,
93
            'dvCompany' => $dvCompany,
94
            'archivo' => curl_file_create($filepath, $mimetype, $filename),
95
        ];
96
97
        // Si no se especificó $retry se obtiene el valor por defecto.
98
        $retry = max(0, min(
99
            $retry ?? $this->lazyWorker->getConnectionOptions()->getReintentos(),
100
            SiiConnectionOptions::REINTENTOS
101
        ));
102
103
        // Realizar la solicitud mediante POST al SII para subir el archivo.
104
        $xmlResponse = $this->uploadXml($certificate, $data, $retry);
105
106
        // Eliminar el archivo temporal con el XML.
107
        unlink($filepath);
108
109
        // Procesar respuesta recibida desde el SII.
110
        $response = $this->xmlComponent->getDecoderWorker()->decode($xmlResponse);
111
        $this->validateUploadXmlResponse($certificate, $response);
112
113
        // Entregar el número de seguimiendo (Track ID) del envío al SII.
114
        $trackId = $response['RECEPCIONDTE']['TRACKID'] ?? 0;
115
        return (int) $trackId;
116
    }
117
118
    /**
119
     * Valida la respuesta recibida desde el SII al enviar un XML.
120
     *
121
     * @param CertificateInterface $certificate Certificado digital.
122
     * @param array $response Arreglo con los datos del XML de la respuesta.
123
     * @return void
124
     * @throws SiiLazyException Si el envío tuvo algún problema.
125
     */
126
    private function validateUploadXmlResponse(
127
        CertificateInterface $certificate,
128
        array $response
129
    ): void {
130
        $status = $response['RECEPCIONDTE']['STATUS'] ?? null;
131
132
        // Si el estado es `null` la respuesta del SII no es válida. Lo cual
133
        // indicaría que el SII no contestó correctamente a la solicitud o bien
134
        // la misma solicitud se hizo de manera incorrecta produciendo que el
135
        // SII no contestase adecuadamente.
136
        if ($status === null) {
137
            throw new SiiDocumentSenderException(
138
                'La respuesta del envío del XML al SII no trae un código de estado válido.'
139
            );
140
        }
141
142
        // Si el estado es 0, el envío fue OK.
143
        if ($status == 0) {
144
            return;
145
        }
146
147
        // Se define un mensaje de error según el código de estado.
148
        switch ($status) {
149
            case 1:
150
                $message = sprintf(
151
                    'El usuario %s no tiene permisos para enviar XML al SII.',
152
                    $certificate->getId()
153
                );
154
                break;
155
            case 2:
156
                $message = 'Error en el tamaño del archivo enviado con el XML, muy grande o muy chico.';
157
                break;
158
            case 3:
159
                $message = 'El archivo enviado está cortado, el tamaño es diferente al parámetro "size".';
160
                break;
161
            case 5:
162
                $message = sprintf(
163
                    'El usuario %s no está autenticado (posible token expirado).',
164
                    $certificate->getId()
165
                );
166
                break;
167
            case 6:
168
                $message = 'La empresa no está autorizada a enviar archivos XML al SII.';
169
                break;
170
            case 7:
171
                $message = 'El esquema del XML es inválido.';
172
                break;
173
            case 8:
174
                $message = 'Existe un error en la firma del documento XML.';
175
                break;
176
            case 9:
177
                $message = 'Los servidores del SII están con problemas internos.';
178
                break;
179
            case 99:
180
                $message = 'El XML enviado ya fue previamente recibido por el SII.';
181
                break;
182
            default:
183
                $message = sprintf(
184
                    'Ocurrió un error con código de estado "%s", que es desconocido por LibreDTE.',
185
                    $status
186
                );
187
                break;
188
        }
189
190
        // Ver si vienen detalles del error.
191
        $error = $response['DETAIL']['ERROR'] ?? null;
192
        if ($error !== null) {
193
            $message .= ' ' . implode(' ', $error);
194
        }
195
196
        // Lanzar una excepción con el mensaje de error determinado.
197
        throw new SiiDocumentSenderException($message);
198
    }
199
200
    /**
201
     * Sube el archivo XML al SII y retorna la respuesta de este.
202
     *
203
     * Este método emula la subida mendiante los siguientes formularios:
204
     *
205
     *   - Producción: https://palena.sii.cl/cgi_dte/UPL/DTEauth?1
206
     *   - Certificación: https://maullin.sii.cl/cgi_dte/UPL/DTEauth?1
207
     *
208
     * @param CertificateInterface $certificate Certificado digital.
209
     * @param array $data Arreglo con los datos del formulario del SII,
210
     * incluyendo el archivo XML que se subirá.
211
     * @param integer $retry
212
     * @return XmlDocument Respuesta del SII al enviar el XML.
213
     * @throws SiiLazyException Si no se puede obtener el token para enviar
214
     * el XML al SII o si hubo un problema (error) al enviar el XML al SII.
215
     */
216
    private function uploadXml(
217
        CertificateInterface $certificate,
218
        array $data,
219
        int $retry
220
    ): XmlDocument {
221
        // URL que se utilizará para subir el XML al SII.
222
        $url = $this->lazyWorker->getConnectionOptions()->getUrl('/cgi_dte/UPL/DTEUpload');
223
224
        // Obtener el token asociado al certificado digital.
225
        $token = $this->tokenManager->getToken($certificate);
226
227
        // Cabeceras HTTP de la solicitud que se hará al SII.
228
        $headers = [
229
            'User-Agent: Mozilla/5.0 (compatible; PROG 1.0; +https://www.libredte.cl)',
230
            'Cookie: TOKEN=' . $token,
231
        ];
232
233
        // Inicializar curl.
234
        $curl = curl_init();
235
        curl_setopt($curl, CURLOPT_POST, true);
236
        curl_setopt($curl, CURLOPT_URL, $url);
237
        curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
238
        curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
239
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
240
241
        // Si no se debe verificar el certificado SSL del servidor del SII se
242
        // agrega la opción a curl.
243
        if (!$this->lazyWorker->getConnectionOptions()->getVerificarSsl()) {
244
            curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0);
245
            curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0);
246
        }
247
248
        // Realizar el envío del XML al SII con $retry intentos.
249
        $responseBody = null;
250
        for ($i = 0; $i < $retry; $i++) {
251
            // Realizar consulta al SII enviando el XML.
252
            $responseBody = curl_exec($curl);
253
254
            // Si se logró obtener una respuesta, y no es un "Error 500",
255
            // entonces se logró enviar el XML al SII y se rompe el ciclo para
256
            // parar los reintentos.
257
            if ($responseBody && $responseBody !== 'Error 500') {
258
                break;
259
            }
260
261
            // El reitento será con "exponential backoff", por lo que se hace
262
            // una pausa de 0.2 * $retry segundos antes de volver a intentar el
263
            // envio del XML al SII.
264
            usleep(200000 * $retry);
265
        }
266
267
        // Validar si hubo un error en la respuesta.
268
        if (!$responseBody || $responseBody === 'Error 500') {
269
            $message = 'Falló el envío del XML al SII. ';
270
            $message .= !$responseBody
271
                ? curl_error($curl)
272
                : 'El SII tiene problemas en sus servidores (Error 500).'
273
            ;
274
            curl_close($curl); // Se cierra conexión curl acá por error.
275
            throw new SiiDocumentSenderException($message);
276
        }
277
278
        // Cerrar conexión curl.
279
        curl_close($curl);
280
281
        // Entregar el resultado como un documento XML.
282
        $xmlDocument = new XmlDocument();
283
        $xmlDocument->loadXml((string) $responseBody);
284
        return $xmlDocument;
285
    }
286
287
    /**
288
     * Guarda el XML en un archivo temporal y, si es necesario, lo comprime.
289
     *
290
     * @param string $xml Documento XML que se guardará en el archivo..
291
     * @param bool $compress Indica si se debe crear un archivo comprimido.
292
     * @return array Arreglo con la ruta al archivo creado y su mimetype.
293
     */
294
    private function createXmlFile(string $xml, bool $compress): array
295
    {
296
        // Normalizar el XML agregando el encabezado si no viene en el
297
        // documento. El SII recibe los documentos en ISO-8859-1 por lo que se
298
        // asume (y no valida) que el contenido del XML en $xml viene ya
299
        // codificado en ISO-8859-1.
300
        if (!str_contains($xml, '<?xml')) {
301
            $xml = '<?xml version="1.0" encoding="ISO-8859-1"?>' . "\n" . $xml;
302
        }
303
304
        // Comprimir el XML si es necesario. En caso de error al comprimir se
305
        // creará el archivo XML igualmente, pero sin comprimir. Ya que no
306
        // debería fallar la creación del XML para el envío si falló la
307
        // compresión al ser una funcionalidad opcional del SII.
308
        if ($compress) {
309
            $xmlGzEncoded = gzencode($xml);
310
            if ($xmlGzEncoded !== false) {
311
                $xml = $xmlGzEncoded;
312
            } else {
313
                $compress = false;
314
            }
315
        }
316
317
        // Crear archivo temporal y guardar los datos del XML en el archivo.
318
        $filepath = $this->getXmlFilePath($compress);
319
        file_put_contents($filepath, $xml);
320
321
        // Determinar mimetype que tiene el archivo.
322
        $mimetype = $compress ? 'application/gzip' : 'application/xml';
323
324
        // Entregar la ruta al archivo creado con el contenido del XML y su
325
        // mimetype final (pues no necesariamente será gzip aunque se haya así
326
        // solicitado).
327
        return [$filepath, $mimetype];
328
    }
329
330
    /**
331
     * Obtiene un nombre único para el archivo del XML que se desea crear.
332
     *
333
     * @param bool $compress Indica si se debe crear un archivo comprimido.
334
     * @return string Arreglo con la ruta al archivo.
335
     */
336
    private function getXmlFilePath(bool $compress): string
337
    {
338
        // Genera un archivo temporal para el XML.
339
        $tempDir = sys_get_temp_dir();
340
        $prefix = 'libredte_xml_document_for_upload_to_sii_';
341
        $filepath = tempnam($tempDir, $prefix);
342
343
        // Renombrar la ruta asignando la extensión al archivo.
344
        $realFilepath = $filepath . ($compress ? '.xml.gz' : '.xml');
345
        rename($filepath, $realFilepath);
346
347
        // Entregar la ruta que se determinó para el archivo.
348
        return $realFilepath;
349
    }
350
}
351