Passed
Push — master ( 9d79d3...4bb85e )
by Esteban De La Fuente
08:41
created

SendXmlDocumentJob::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
eloc 47
dl 0
loc 72
c 0
b 0
f 0
ccs 0
cts 50
cp 0
rs 6.6166
cc 13
nc 22
nop 2
crap 182

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