Passed
Push — master ( e584ed...a08a48 )
by Esteban De La Fuente
05:48
created

SobreEnvio::getXmlSignatureNode()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 2.0054

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 8
c 1
b 0
f 0
nc 2
nop 0
dl 0
loc 13
ccs 8
cts 9
cp 0.8889
crap 2.0054
rs 10
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
11
 * por la Fundación para el Software Libre, ya sea la versión 3 de la Licencia,
12
 * o (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\Sii\Dte\Documento;
26
27
use libredte\lib\Core\Service\ArrayDataProvider;
28
use libredte\lib\Core\Service\DataProviderInterface;
29
use libredte\lib\Core\Signature\Certificate;
30
use libredte\lib\Core\Signature\SignatureException;
31
use libredte\lib\Core\Signature\SignatureGenerator;
32
use libredte\lib\Core\Signature\XmlSignatureNode;
33
use libredte\lib\Core\Sii\Dte\Documento\Builder\DocumentoFactory;
34
use libredte\lib\Core\Xml\XmlConverter;
35
use libredte\lib\Core\Xml\XmlDocument;
36
use libredte\lib\Core\Xml\XmlException;
37
use libredte\lib\Core\Xml\XmlUtils;
38
use libredte\lib\Core\Xml\XmlValidator;
39
40
/**
41
 * Clase que representa un sobre para el envío de documentos al SII.
42
 *
43
 * Este sobre permite enviar facturas (EnvioDTE) y boletas (EnvioBOLETA).
44
 */
45
class SobreEnvio
46
{
47
    /**
48
     * Constante que representa que el envío es de DTE.
49
     *
50
     * Este sobre se usa para todo menos boletas.
51
     */
52
    private const SOBRE_DTE = 0;
53
54
    /**
55
     * Constante que representa que el envío es de boletas.
56
     */
57
    private const SOBRE_BOLETA = 1;
58
59
    /**
60
     * Configuración (reglas) para el documento XML del envío.
61
     */
62
    private const CONFIG = [
63
        self::SOBRE_DTE => [
64
            // Máxima cantidad de tipos de documentos en el envío.
65
            'SubTotDTE_max' => 20,
66
            // Máxima cantidad de documentos en un envío.
67
            'DTE_max' => 2000,
68
            // Tag XML para el envío.
69
            'tag' => 'EnvioDTE',
70
            // Schema principal del XML del envío.
71
            'schema' => 'EnvioDTE_v10',
72
        ],
73
        self::SOBRE_BOLETA => [
74
            // Máxima cantidad de tipos de documentos en el envío.
75
            'SubTotDTE_max' => 2,
76
            // Máxima cantidad de documentos en un envío.
77
            'DTE_max' => 1000,
78
            // Tag XML para el envío.
79
            'tag' => 'EnvioBOLETA',
80
            // Schema principal del XML del envío.
81
            'schema' => 'EnvioBOLETA_v11',
82
        ],
83
    ];
84
85
    /**
86
     * Instancia del documento XML asociado al sobre.
87
     *
88
     * @var XmlDocument
89
     */
90
    protected XmlDocument $xmlDocument;
91
92
    /**
93
     * Tipo de sobre que se está generando.
94
     *
95
     * Posibles valores:
96
     *
97
     *   - SobreEnvio::SOBRE_DTE
98
     *   - SobreEnvio::SOBRE_BOLETA
99
     *
100
     * @var int
101
     */
102
    private int $tipo;
103
104
    /**
105
     * Datos de la carátula del envío
106
     *
107
     * @var array
108
     */
109
    private array $caratula;
110
111
    /**
112
     * Arreglo con las instancias de documentos que se enviarán.
113
     *
114
     * @var array<int, AbstractDocumento>
115
     */
116
    private array $documentos;
117
118
    /**
119
     * Proveedor de datos.
120
     *
121
     * @var DataProviderInterface
122
     */
123
    protected DataProviderInterface $dataProvider;
124
125
    /**
126
     * Constructor del sobre del envío de DTE al SII.
127
     *
128
     * @param DataProviderInterface|null $dataProvider
129
     */
130 6
    public function __construct(?DataProviderInterface $dataProvider = null)
131
    {
132 6
        $this->dataProvider = $dataProvider ?? new ArrayDataProvider();
133
    }
134
135
    /**
136
     * Permite crear el documento XML del sobre a partir de un string XML.
137
     *
138
     * @param string $xml
139
     * @return void
140
     */
141 4
    public function loadXML(string $xml)
142
    {
143 4
        $this->xmlDocument = new XmlDocument();
144 4
        $this->xmlDocument->loadXML($xml);
145
    }
146
147
    /**
148
     * Obtiene el string XML del sobre con el formato de XmlDocument::getXML().
149
     *
150
     * @return string
151
     */
152
    public function getXml(): string
153
    {
154
        return $this->getXmlDocument()->getXML();
155
    }
156
157
    /**
158
     * Obtiene el string XML del sobre en el formato de XmlDocument::saveXML().
159
     *
160
     * @return string
161
     */
162
    public function saveXml(): string
163
    {
164
        return $this->getXmlDocument()->saveXML();
165
    }
166
167
    /**
168
     * Realiza la firma del sobre del envío.
169
     *
170
     * @param Certificate $certificate Instancia que representa la firma
171
     * electrónica.
172
     * @return string String con el XML firmado.
173
     * @throws SignatureException Si existe algún problema al firmar el sobre.
174
     */
175 2
    public function firmar(Certificate $certificate): string
176
    {
177 2
        $this->getXmlDocument();
178
179 2
        $xmlSigned = SignatureGenerator::signXml(
180 2
            $this->xmlDocument,
181 2
            $certificate,
182 2
            'LibreDTE_SetDoc'
183 2
        );
184
185 2
        $this->xmlDocument->loadXML($xmlSigned);
186
187 2
        return $xmlSigned;
188
    }
189
190
    /**
191
     * Obtiene una instancia del nodo de la firma.
192
     *
193
     * @return XmlSignatureNode
194
     * @throws SignatureException Si el documento XML no está firmado.
195
     */
196 3
    public function getXmlSignatureNode(): XmlSignatureNode
197
    {
198 3
        $tag = $this->getXmlDocument()->documentElement->tagName;
199 3
        $xpath = '/*[local-name()="' . $tag . '"]/*[local-name()="Signature"]';
200 3
        $signatureElement = XmlUtils::xpath($this->getXmlDocument(), $xpath)->item(0);
201 3
        if ($signatureElement === null) {
202
            throw new SignatureException('El sobre del envío del XML no se encuentra firmado (no se encontró el nodo "Signature").');
203
        }
204
205 3
        $xmlSignatureNode = new XmlSignatureNode();
206 3
        $xmlSignatureNode->loadXML($signatureElement->C14N());
207
208 3
        return $xmlSignatureNode;
209
    }
210
211
    /**
212
     * Valida la firma electrónica del documento XML del sobre.
213
     *
214
     * @return void
215
     * @throws SignatureException Si la validación de la firma falla.
216
     */
217 2
    public function validateSignature()
218
    {
219 2
        $xmlSignatureNode = $this->getXmlSignatureNode();
220 2
        $xmlSignatureNode->validate($this->getXmlDocument());
221
    }
222
223
    /**
224
     * Valida el esquema del XML del sobre del envío.
225
     *
226
     * Este método valida tanto los esquemas de EnvioDTE como el EnvioBOLETA.
227
     *
228
     * @return void
229
     * @throws XmlException Si la validación del esquema falla.
230
     */
231 3
    public function validateSchema(): void
232
    {
233 3
        XmlValidator::validateSchema($this->getXmlDocument());
234
    }
235
236
    /**
237
     * Agrega un documento al listado que se enviará al SII.
238
     *
239
     * @param AbstractDocumento $documento Instancia del documento que se desea
240
     * agregar al listado del envío.
241
     */
242 4
    public function agregar(AbstractDocumento $documento): void
243
    {
244
        // Si ya se generó la carátula no se permite agregar nuevos documentos.
245 4
        if (isset($this->caratula)) {
246
            throw new DocumentoException(
247
                'No es posible agregar documentos cuando la carátula ya fue generada.'
248
            );
249
        }
250
251
        // Determinar el tipo del envío (DTE o BOLETA).
252 4
        $esBoleta = $documento->getTipo()->esBoleta();
253 4
        if (!isset($this->tipo)) {
254 4
            $this->tipo = $esBoleta
255 1
                ? self::SOBRE_BOLETA
256 4
                : self::SOBRE_DTE
257 4
            ;
258
        }
259
260
        // Validar que el tipo de documento sea del tipo que se espera.
261 2
        elseif ($esBoleta !== (bool) $this->tipo) {
262
            throw new DocumentoException(
263
                'No es posible mezclar DTE con BOLETA en el envío al SII.'
264
            );
265
        }
266
267
        // Si no está definido como arreglo se crea el arreglo de documentos.
268 4
        if (!isset($this->documentos)) {
269 4
            $this->documentos = [];
270
        }
271
272
        // Validar que no se haya llenado la lista.
273 4
        if (isset($this->documentos[self::CONFIG[$this->tipo]['DTE_max'] - 1])) {
274
            throw new DocumentoException(sprintf(
275
                'No es posible agregar nuevos documentos al envío al SII, límite de %d.',
276
                self::CONFIG[$this->tipo]['DTE_max']
277
            ));
278
        }
279
280
        // Agregar documento al listado.
281 4
        $this->documentos[] = $documento;
282
    }
283
284
    /**
285
     * Asignar la carátula del sobre del envío.
286
     *
287
     * @param array $caratula Arreglo con datos: RutEnvia, FchResol y NroResol.
288
     * @return array Arreglo con la carátula normalizada.
289
     */
290 2
    public function setCaratula(array $caratula): array
291
    {
292
        // Si no hay documentos para enviar error.
293 2
        if (!isset($this->documentos[0])) {
294
            throw new DocumentoException(
295
                'No existen documentos en el sobre para poder generar la carátula.'
296
            );
297
        }
298
299
        // Si se agregaron más tipos de documentos que los permitidos error.
300 2
        $SubTotDTE = $this->getSubTotDTE();
301 2
        if (isset($SubTotDTE[self::CONFIG[$this->tipo]['SubTotDTE_max']])) {
302
            throw new DocumentoException(
303
                'Se agregaron más tipos de documento de los que son permitidos (%d).',
304
                self::CONFIG[$this->tipo]['SubTotDTE_max']
305
            );
306
        }
307
308
        // Generar carátula.
309 2
        $this->caratula = array_merge([
310 2
            '@attributes' => [
311 2
                'version' => '1.0',
312 2
            ],
313 2
            'RutEmisor' => $this->documentos[0]->getEmisor()->getRut(),
314 2
            'RutEnvia' => false,
315 2
            'RutReceptor' => $this->documentos[0]->getReceptor()->getRut(),
316 2
            'FchResol' => '',
317 2
            'NroResol' => '',
318 2
            'TmstFirmaEnv' => date('Y-m-d\TH:i:s'),
319 2
            'SubTotDTE' => $SubTotDTE,
320 2
        ], $caratula);
321
322
        // Retornar la misma carátula pero normalizada.
323 2
        return $this->caratula;
324
    }
325
326
    /**
327
     * Obtiene los datos para generar los tags SubTotDTE.
328
     *
329
     * @return array Arreglo con los datos para generar los tags SubTotDTE.
330
     */
331 2
    private function getSubTotDTE(): array
332
    {
333 2
        $SubTotDTE = [];
334 2
        $subtotales = [];
335
336 2
        foreach ($this->documentos as $documento) {
337 2
            if (!isset($subtotales[$documento->getTipo()->getCodigo()])) {
338 2
                $subtotales[$documento->getTipo()->getCodigo()] = 0;
339
            }
340 2
            $subtotales[$documento->getTipo()->getCodigo()]++;
341
        }
342
343 2
        foreach ($subtotales as $tipo => $subtotal) {
344 2
            $SubTotDTE[] = [
345 2
                'TpoDTE' => $tipo,
346 2
                'NroDTE' => $subtotal,
347 2
            ];
348
        }
349
350 2
        return $SubTotDTE;
351
    }
352
353
    /**
354
     * Genera el documento XML.
355
     *
356
     * @return XmlDocument
357
     */
358 6
    public function getXmlDocument(): XmlDocument
359
    {
360 6
        if (!isset($this->xmlDocument)) {
361
            // Generar estructura base del XML del sobre (envío).
362 2
            $xmlDocumentData = [
363 2
                self::CONFIG[$this->tipo]['tag'] => [
364 2
                    '@attributes' => [
365 2
                        'xmlns' => 'http://www.sii.cl/SiiDte',
366 2
                        'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance',
367 2
                        'xsi:schemaLocation' => 'http://www.sii.cl/SiiDte '
368 2
                            . self::CONFIG[$this->tipo]['schema'] . '.xsd'
369 2
                        ,
370 2
                        'version' => '1.0',
371 2
                    ],
372 2
                    'SetDTE' => [
373 2
                        '@attributes' => [
374 2
                            'ID' => 'LibreDTE_SetDoc',
375 2
                        ],
376 2
                        'Caratula' => $this->caratula,
377 2
                        'DTE' => '',
378 2
                    ],
379 2
                ],
380 2
            ];
381 2
            $this->xmlDocument = XmlConverter::arrayToXml($xmlDocumentData);
382
383
            // Generar XML de los documentos que se deberán incorporar.
384 2
            $documentos = [];
385 2
            foreach ($this->documentos as $doc) {
386 2
                $documentos[] = trim(str_replace(
387 2
                    [
388 2
                        '<?xml version="1.0" encoding="ISO-8859-1"?>',
389 2
                        '<?xml version="1.0"?>',
390 2
                    ],
391 2
                    '',
392 2
                    $doc->getXML()
393 2
                ));
394
            }
395
396
            // Agregar los DTE dentro de SetDTE reemplazando el tag vacio DTE.
397 2
            $xmlEnvio = $this->xmlDocument->saveXML();
398 2
            $xml = str_replace('<DTE/>', implode("\n", $documentos), $xmlEnvio);
399
400
            // Reemplazar el documento XML del sobre del envío.
401 2
            $this->xmlDocument->loadXML($xml);
402
        }
403
404
        // Entregar el documento XML.
405 6
        return $this->xmlDocument;
406
    }
407
408
    /**
409
     * Entrega el listado de documentos incluídos en el sobre.
410
     *
411
     * @return array<AbstractDocumento>
412
     */
413 2
    public function getDocumentos(): array
414
    {
415 2
        if (!isset($this->documentos)) {
416 2
            $factory = new DocumentoFactory($this->dataProvider);
417 2
            $documentosNodeList = $this
418 2
                ->getXmlDocument()
419 2
                ->getElementsByTagName('DTE')
420 2
            ;
421 2
            foreach ($documentosNodeList as $documentoNode) {
422 2
                $xml = $documentoNode->C14N();
423 2
                $documento = $factory->loadFromXml($xml);
424 2
                $this->agregar($documento);
425
            }
426
        }
427
428 2
        return $this->documentos;
429
    }
430
}
431