Passed
Push — master ( a2f57d...fc3866 )
by Esteban De La Fuente
09:35
created

AbstractDocumento::timbrar()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 67
Code Lines 40

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 36
CRAP Score 4.2987

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 40
c 1
b 0
f 0
nc 5
nop 2
dl 0
loc 67
ccs 36
cts 49
cp 0.7347
crap 4.2987
rs 9.28

How to fix   Long Method   

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
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 DateTime;
28
use libredte\lib\Core\Service\ArrayDataProvider;
29
use libredte\lib\Core\Service\DataProviderInterface;
30
use libredte\lib\Core\Service\PathManager;
31
use libredte\lib\Core\Signature\Certificate;
32
use libredte\lib\Core\Signature\SignatureException;
33
use libredte\lib\Core\Signature\SignatureGenerator;
34
use libredte\lib\Core\Signature\XmlSignatureNode;
35
use libredte\lib\Core\Sii\Contribuyente\Contribuyente;
36
use libredte\lib\Core\Sii\Dte\AutorizacionFolio\Caf;
37
use libredte\lib\Core\Sii\Dte\AutorizacionFolio\CafException;
38
use libredte\lib\Core\Sii\Dte\Documento\Renderer\DocumentoRenderer;
39
use libredte\lib\Core\Xml\XmlConverter;
40
use libredte\lib\Core\Xml\XmlDocument;
41
use libredte\lib\Core\Xml\XmlException;
42
use libredte\lib\Core\Xml\XmlUtils;
43
use libredte\lib\Core\Xml\XmlValidator;
44
45
/**
46
 * Clase abstracta (base) de la representación de un documento.
47
 */
48
abstract class AbstractDocumento
49
{
50
    /**
51
     * Código del tipo de documento tributario al que está asociada esta
52
     * instancia de un documento.
53
     */
54
    protected int $codigo;
55
56
    /**
57
     * Instancia del tipo de documento tributario, según el código, asociado a
58
     * esta instancia de un documento.
59
     *
60
     * @var DocumentoTipo
61
     */
62
    private DocumentoTipo $tipo;
63
64
    /**
65
     * Arreglo con los datos del documento tributario.
66
     *
67
     * Estos datos podrían o no haber sido normalizados. Sin embargo, si no
68
     * fueron normalizados, se espera que se hayan asignados según lo que el
69
     * SII requiere (o sea, como si se hubiesen "normalizado").
70
     *
71
     * @var array
72
     */
73
    protected array $data;
74
75
    /**
76
     * Instancia del documento XML asociado a los datos.
77
     *
78
     * @var XmlDocument
79
     */
80
    protected XmlDocument $xmlDocument;
81
82
    /**
83
     * Contribuyente emisor del documento.
84
     *
85
     * Este objeto representa al contribuyente que emitió el documento.
86
     *
87
     * @var Contribuyente
88
     */
89
    private Contribuyente $emisor;
90
91
    /**
92
     * Contribuyente receptor del documento.
93
     *
94
     * Este objeto representa al contribuyente que recibió el documento.
95
     *
96
     * @var Contribuyente
97
     */
98
    private Contribuyente $receptor;
99
100
    /**
101
     * Proveedor de datos.
102
     *
103
     * @var DataProviderInterface
104
     */
105
    protected DataProviderInterface $dataProvider;
106
107
    /**
108
     * Renderizador del documento tributario.
109
     *
110
     * @var DocumentoRenderer
111
     */
112
    protected DocumentoRenderer $renderer;
113
114
    /**
115
     * Constructor de la clase.
116
     *
117
     * @param DataProviderInterface|null $dataProvider Proveedor de datos.
118
     */
119 305
    public function __construct(?DataProviderInterface $dataProvider = null)
120
    {
121 305
        $this->dataProvider = $dataProvider ?? new ArrayDataProvider();
122
    }
123
124
    /**
125
     * Entrega la instancia del tipo de documento asociado a este documento.
126
     *
127
     * @return DocumentoTipo
128
     */
129 305
    public function getTipo(): DocumentoTipo
130
    {
131 305
        if (!isset($this->tipo)) {
132 305
            $this->tipo = new DocumentoTipo(
133 305
                $this->codigo,
134 305
                $this->dataProvider
135 305
            );
136
        }
137
138 305
        return $this->tipo;
139
    }
140
141
    /**
142
     * Asigna los datos del documento.
143
     *
144
     * @param array $data
145
     * @return self
146
     */
147 303
    public function setData(array $data): self
148
    {
149 303
        $this->data = $data;
150 303
        unset($this->xmlDocument);
151
152 303
        return $this;
153
    }
154
155
    /**
156
     * Obtiene los datos del documento.
157
     *
158
     * @return array
159
     */
160 303
    public function getData(): array
161
    {
162 303
        return $this->data;
163
    }
164
165
    /**
166
     * Carga el XML completo de un documento para crear la instancia del
167
     * documento XML asociada a este documento.
168
     *
169
     * @param string $xml
170
     * @return self
171
     */
172 2
    public function loadXML(string $xml): self
173
    {
174 2
        $this->xmlDocument = new XmlDocument();
175 2
        $this->xmlDocument->loadXML($xml);
176
177 2
        return $this;
178
    }
179
180
    /**
181
     * Entrega el string XML del documento XML.
182
     *
183
     * Es un wrapper de XmlDocument::getXML();
184
     *
185
     * @return string
186
     */
187 2
    public function getXml(): string
188
    {
189 2
        return $this->getXmlDocument()->getXML();
190
    }
191
192
    /**
193
     * Entrega el string XML del documento XML.
194
     *
195
     * Es un wrapper de XmlDocument::saveXML();
196
     *
197
     * @return string
198
     */
199 49
    public function saveXml(): string
200
    {
201 49
        return $this->getXmlDocument()->saveXML();
202
    }
203
204
    /**
205
     * Entrega el ID que LibreDTE asigna al documento.
206
     *
207
     * @return string
208
     */
209 103
    public function getId(): string
210
    {
211 103
        return sprintf(
212 103
            'LibreDTE_T%dF%d',
213 103
            $this->getCodigo(),
214 103
            $this->getFolio()
215 103
        );
216
    }
217
218
    /**
219
     * Entrega el código del tipo de documento tributario.
220
     *
221
     * @return int
222
     */
223 103
    public function getCodigo(): int
224
    {
225 103
        return $this->codigo;
226
    }
227
228
    /**
229
     * Entrega el folio del documento tributario.
230
     *
231
     * @return int
232
     */
233 152
    public function getFolio(): int
234
    {
235 152
        $data = $this->getData();
236
237 152
        return (int) $data['Encabezado']['IdDoc']['Folio'];
238
    }
239
240
    /**
241
     * Entrega la fecha de emisión asignada al documento tributario.
242
     *
243
     * Esta es la fecha de emisión informada al SII del documento, no es la
244
     * fecha de creación real del documento en LibreDTE.
245
     *
246
     * @return string
247
     */
248 103
    public function getFechaEmision(): string
249
    {
250 103
        $data = $this->getData();
251
252 103
        return $data['Encabezado']['IdDoc']['FchEmis'];
253
    }
254
255
    /**
256
     * Obtiene el contribuyente emisor del documento.
257
     *
258
     * @return Contribuyente Instancia de Contribuyente que representa al
259
     * emisor.
260
     */
261 103
    public function getEmisor(): Contribuyente
262
    {
263 103
        if (!isset($this->emisor)) {
264 103
            $data = $this->getData();
265
266 103
            $this->emisor = new Contribuyente(
267 103
                data: $data['Encabezado']['Emisor'],
268 103
                dataProvider: $this->dataProvider
269 103
            );
270
        }
271
272 103
        return $this->emisor;
273
    }
274
275
    /**
276
     * Obtiene el contribuyente receptor del documento.
277
     *
278
     * @return Contribuyente Instancia de Contribuyente que representa al
279
     * receptor.
280
     */
281 103
    public function getReceptor(): Contribuyente
282
    {
283 103
        if (!isset($this->receptor)) {
284 103
            $data = $this->getData();
285
286 103
            $this->receptor = new Contribuyente(
287 103
                data: $data['Encabezado']['Receptor'],
288 103
                dataProvider: $this->dataProvider
289 103
            );
290
        }
291
292 103
        return $this->receptor;
293
    }
294
295
    /**
296
     * Entrega todos los valores del tag "Totales".
297
     *
298
     * @return array
299
     */
300
    public function getTotales(): array
301
    {
302
        $data = $this->getData();
303
304
        return $data['Encabezado']['Totales'];
305
    }
306
307
    /**
308
     * Entrega el monto total del documento.
309
     *
310
     * El monto estará en la moneda del documento.
311
     *
312
     * @return int|float Monto total del documento.
313
     */
314 107
    public function getMontoTotal(): int|float
315
    {
316 107
        $data = $this->getData();
317 107
        $monto = $data['Encabezado']['Totales']['MntTotal'];
318
319
        // Verificar si el monto es equivalente a un entero.
320 107
        if (is_float($monto) && floor($monto) == $monto) {
321 19
            return (int) $monto;
322
        }
323
324
        // Entregar como flotante.
325 89
        return $monto;
326
    }
327
328
    /**
329
     * Entrega el detalle del documento.
330
     *
331
     * Se puede solicitar todo el detalle o el detalle de una línea en
332
     * específico.
333
     *
334
     * @param integer|null $index Índice de la línea de detalle solicitada o
335
     * `null` (por defecto) para obtener todas las líneas.
336
     * @return array
337
     */
338 103
    public function getDetalle(?int $index = null): array
339
    {
340 103
        $data = $this->getData();
341
342 103
        return $index !== null
343 103
            ? $data['Detalle'][$index] ?? []
344 103
            : $data['Detalle'] ?? []
345 103
        ;
346
    }
347
348
    /**
349
     * Obtiene la instancia del documento XML asociada al documento tributario.
350
     *
351
     * @return XmlDocument
352
     */
353 104
    public function getXmlDocument(): XmlDocument
354
    {
355 104
        if (!isset($this->xmlDocument)) {
356 103
            $xmlDocumentData = [
357 103
                'DTE' => [
358 103
                    '@attributes' => [
359 103
                        'version' => '1.0',
360 103
                        'xmlns' => 'http://www.sii.cl/SiiDte',
361 103
                    ],
362 103
                    $this->getTipo()->getTagXML() => array_merge([
363 103
                        '@attributes' => [
364 103
                            'ID' => $this->getId(),
365 103
                        ],
366 103
                    ], $this->getData()),
367 103
                ],
368 103
            ];
369 103
            $this->xmlDocument = XmlConverter::arrayToXml($xmlDocumentData);
370
        }
371
372 104
        return $this->xmlDocument;
373
    }
374
375
    /**
376
     * Realiza el timbrado del documento.
377
     *
378
     * @param Caf $caf Instancia del CAF con el que se desea timbrar.
379
     * @param string $timestamp Marca de tiempo a utilizar en el timbre.
380
     * @throws CafException Si existe algún problema al timbrar el documento.
381
     */
382 103
    public function timbrar(Caf $caf, ?string $timestamp = null): void
383
    {
384
        // Verificar que el folio del documento esté dentro del rango del CAF.
385 103
        if (!$caf->enRango($this->getFolio())) {
386
            throw new CafException(sprintf(
387
                'El folio %d del documento %s no está disponible en el rango del CAF %s.',
388
                $this->getFolio(),
389
                $this->getID(),
390
                $caf->getID()
391
            ));
392
        }
393
394
        // Asignar marca de tiempo si no se pasó una.
395 103
        if ($timestamp === null) {
396 103
            $timestamp = date('Y-m-d\TH:i:s');
397
        }
398
399
        // Corroborar que el CAF esté vigente según el timestamp usado.
400 103
        if (!$caf->vigente($timestamp)) {
401
            throw new CafException(sprintf(
402
                'El CAF %s que contiene el folio %d del documento %s no está vigente, venció el día %s.',
403
                $caf->getID(),
404
                $this->getFolio(),
405
                $this->getID(),
406
                (new DateTime($caf->getFechaVencimiento()))->format('d/m/Y'),
407
            ));
408
        }
409
410
        // Preparar datos del timbre.
411 103
        $tedData = [
412 103
            'TED' => [
413 103
                '@attributes' => [
414 103
                    'version' => '1.0',
415 103
                ],
416 103
                'DD' => [
417 103
                    'RE' => $this->getEmisor()->getRut(),
418 103
                    'TD' => $this->getTipo()->getCodigo(),
419 103
                    'F' => $this->getFolio(),
420 103
                    'FE' => $this->getFechaEmision(),
421 103
                    'RR' => $this->getReceptor()->getRut(),
422 103
                    'RSR' => $this->getReceptor()->getRazonSocial(),
423 103
                    'MNT' => $this->getMontoTotal(),
424 103
                    'IT1' => $this->getDetalle(0)['NmbItem'] ?? '',
425 103
                    'CAF' => $caf->getAutorizacion(),
426 103
                    'TSTED' => $timestamp,
427 103
                ],
428 103
                'FRMT' => [
429 103
                    '@attributes' => [
430 103
                        'algoritmo' => 'SHA1withRSA',
431 103
                    ],
432 103
                    '@value' => '', // Se agregará luego.
433 103
                ],
434 103
            ],
435 103
        ];
436
437
        // Armar XML del timbre y obtener los datos a timbrar (tag DD: datos
438
        // del documento).
439 103
        $tedXmlDocument = XmlConverter::arrayToXml($tedData);
440 103
        $ddToStamp = $tedXmlDocument->C14NWithIsoEncodingFlattened('/TED/DD');
441
442
        // Timbrar los "datos a timbrar" $ddToStamp.
443 103
        $timbre = $caf->timbrar($ddToStamp);
444 103
        $tedData['TED']['FRMT']['@value'] = $timbre;
445
446
        // Actualizar los datos del documento incorporando el timbre calculado.
447 103
        $newData = array_merge($this->getData(), $tedData);
448 103
        $this->setData($newData);
449
    }
450
451
    /**
452
     * Realiza la firma del documento.
453
     *
454
     * @param Certificate $certificate Instancia que representa la firma
455
     * electrónica.
456
     * @param string $timestamp Marca de tiempo a utilizar en la firma.
457
     * @return string String con el XML firmado.
458
     * @throws SignatureException Si existe algún problema al firmar el documento.
459
     */
460 54
    public function firmar(Certificate $certificate, ?string $timestamp = null): string
461
    {
462
        // Asignar marca de tiempo si no se pasó una.
463 54
        if ($timestamp === null) {
464 54
            $timestamp = date('Y-m-d\TH:i:s');
465
        }
466
467
        // Corroborar que el certificado esté vigente según el timestamp usado.
468 54
        if (!$certificate->isActive($timestamp)) {
469
            throw new SignatureException(sprintf(
470
                'El certificado digital de %s no está vigente en el tiempo %s, su rango de vigencia es del %s al %s.',
471
                $certificate->getID(),
472
                (new DateTime($timestamp))->format('d/m/Y H:i'),
473
                (new DateTime($certificate->getFrom()))->format('d/m/Y H:i'),
474
                (new DateTime($certificate->getTo()))->format('d/m/Y H:i'),
475
            ));
476
        }
477
478
        // Agregar timestamp.
479 54
        $newData = array_merge($this->getData(), ['TmstFirma' => $timestamp]);
480 54
        $this->setData($newData);
481
482
        // Firmar el tag que contiene el documento y retornar el XML firmado.
483 54
        $xmlSigned = SignatureGenerator::signXml(
484 54
            $this->getXmlDocument(),
485 54
            $certificate,
486 54
            $this->getId()
487 54
        );
488
489
        // Cargar XML en el documento.
490 54
        $this->xmlDocument->loadXML($xmlSigned);
491
492
        // Entregar XML firmado.
493 54
        return $xmlSigned;
494
    }
495
496
    /**
497
     * Obtiene una instancia del nodo de la firma.
498
     *
499
     * @return XmlSignatureNode
500
     * @throws SignatureException Si el documento XML no está firmado.
501
     */
502 54
    public function getXmlSignatureNode(): XmlSignatureNode
503
    {
504 54
        $tag = $this->getXmlDocument()->documentElement->tagName;
505 54
        $xpath = '/*[local-name()="' . $tag . '"]/*[local-name()="Signature"]';
506 54
        $signatureElement = XmlUtils::xpath($this->getXmlDocument(), $xpath)->item(0);
507 54
        if ($signatureElement === null) {
508
            throw new SignatureException('El documento XML del DTE no se encuentra firmado (no se encontró el nodo "Signature").');
509
        }
510
511 54
        $xmlSignatureNode = new XmlSignatureNode();
512 54
        $xmlSignatureNode->loadXML($signatureElement->C14N());
513
514 54
        return $xmlSignatureNode;
515
    }
516
517
    /**
518
     * Valida la firma electrónica del documento XML del DTE.
519
     *
520
     * @return void
521
     * @throws SignatureException Si la validación del esquema falla.
522
     */
523 54
    public function validateSignature()
524
    {
525 54
        $xmlSignatureNode = $this->getXmlSignatureNode();
526 54
        $xmlSignatureNode->validate($this->getXmlDocument());
527
    }
528
529
    /**
530
     * Valida el esquema del XML del DTE.
531
     *
532
     * @return void
533
     * @throws XmlException Si la validación del esquema falla.
534
     */
535 55
    public function validateSchema(): void
536
    {
537
        // Las boletas no se validan de manera individual (el DTE). Se validan
538
        // a través del EnvioBOLETA.
539 55
        if ($this->getTipo()->esBoleta()) {
540 5
            return;
541
        }
542
543
        // Validar esquema de otros DTE (no boletas).
544 50
        $schema = 'DTE_v10.xsd';
545 50
        $schemaPath = PathManager::getSchemasPath($schema);
546 50
        XmlValidator::validateSchema($this->getXmlDocument(), $schemaPath);
547
    }
548
549
    /**
550
     * Método general para el renderizado del documento tributario electrónico.
551
     *
552
     * @param array $options Opciones para el renderizado del documento.
553
     * @return string Datos del documento renderizado según las opciones.
554
     */
555 98
    public function render(array $options = []): string
556
    {
557
        // Si el renderer no está creado se crea.
558 98
        if (!isset($this->renderer)) {
559 98
            $this->renderer = new DocumentoRenderer($this->dataProvider);
560
        }
561
562
        // Renderizar el documento.
563 98
        return $this->renderer->renderFromDocumento($this, $options);
564
    }
565
566
    /**
567
     * Genera el HTML del documento tributario electrónico.
568
     *
569
     * @param array $options Opciones para generar el HTML.
570
     * @return string Código HTML generado.
571
     */
572 49
    public function getHtml(array $options = []): string
573
    {
574 49
        $options['format'] = 'html';
575
576 49
        return $this->render($options);
577
    }
578
579
    /**
580
     * Genera el PDF del documento tributario electrónico.
581
     *
582
     * @param array $options Opciones para generar el PDF.
583
     * @return string Datos binarios del PDF generado.
584
     */
585 49
    public function getPdf(array $options = []): string
586
    {
587 49
        $options['format'] = 'pdf';
588
589 49
        return $this->render($options);
590
    }
591
592
    /**
593
     * Genera el ESCPOS del documento tributario electrónico.
594
     *
595
     * @param array $options Opciones para generar el ESCPOS.
596
     * @return string Datos binarios del ESCPOS generado.
597
     */
598
    public function getEscpos(array $options = []): string
599
    {
600
        $options['format'] = 'escpos';
601
602
        return $this->render($options);
603
    }
604
}
605