Passed
Push — master ( 6ab367...147f6f )
by Esteban De La Fuente
09:19
created

AbstractDocumento::timbrar()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 75
Code Lines 44

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 44
CRAP Score 4.1898

Importance

Changes 2
Bugs 1 Features 0
Metric Value
cc 4
eloc 44
c 2
b 1
f 0
nc 5
nop 2
dl 0
loc 75
ccs 44
cts 57
cp 0.7719
crap 4.1898
rs 9.216

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' => mb_substr(
423 103
                        $this->getReceptor()->getRazonSocial(),
424 103
                        0,
425 103
                        40
426 103
                    ),
427 103
                    'MNT' => $this->getMontoTotal(),
428 103
                    'IT1' => mb_substr(
429 103
                        $this->getDetalle(0)['NmbItem'] ?? '',
430 103
                        0,
431 103
                        40
432 103
                    ),
433 103
                    'CAF' => $caf->getAutorizacion(),
434 103
                    'TSTED' => $timestamp,
435 103
                ],
436 103
                'FRMT' => [
437 103
                    '@attributes' => [
438 103
                        'algoritmo' => 'SHA1withRSA',
439 103
                    ],
440 103
                    '@value' => '', // Se agregará luego.
441 103
                ],
442 103
            ],
443 103
        ];
444
445
        // Armar XML del timbre y obtener los datos a timbrar (tag DD: datos
446
        // del documento).
447 103
        $tedXmlDocument = XmlConverter::arrayToXml($tedData);
448 103
        $ddToStamp = $tedXmlDocument->C14NWithIsoEncodingFlattened('/TED/DD');
449
450
        // Timbrar los "datos a timbrar" $ddToStamp.
451 103
        $timbre = $caf->timbrar($ddToStamp);
452 103
        $tedData['TED']['FRMT']['@value'] = $timbre;
453
454
        // Actualizar los datos del documento incorporando el timbre calculado.
455 103
        $newData = array_merge($this->getData(), $tedData);
456 103
        $this->setData($newData);
457
    }
458
459
    /**
460
     * Realiza la firma del documento.
461
     *
462
     * @param Certificate $certificate Instancia que representa la firma
463
     * electrónica.
464
     * @param string $timestamp Marca de tiempo a utilizar en la firma.
465
     * @return string String con el XML firmado.
466
     * @throws SignatureException Si existe algún problema al firmar el documento.
467
     */
468 54
    public function firmar(Certificate $certificate, ?string $timestamp = null): string
469
    {
470
        // Asignar marca de tiempo si no se pasó una.
471 54
        if ($timestamp === null) {
472 54
            $timestamp = date('Y-m-d\TH:i:s');
473
        }
474
475
        // Corroborar que el certificado esté vigente según el timestamp usado.
476 54
        if (!$certificate->isActive($timestamp)) {
477
            throw new SignatureException(sprintf(
478
                'El certificado digital de %s no está vigente en el tiempo %s, su rango de vigencia es del %s al %s.',
479
                $certificate->getID(),
480
                (new DateTime($timestamp))->format('d/m/Y H:i'),
481
                (new DateTime($certificate->getFrom()))->format('d/m/Y H:i'),
482
                (new DateTime($certificate->getTo()))->format('d/m/Y H:i'),
483
            ));
484
        }
485
486
        // Agregar timestamp.
487 54
        $newData = array_merge($this->getData(), ['TmstFirma' => $timestamp]);
488 54
        $this->setData($newData);
489
490
        // Firmar el tag que contiene el documento y retornar el XML firmado.
491 54
        $xmlSigned = SignatureGenerator::signXml(
492 54
            $this->getXmlDocument(),
493 54
            $certificate,
494 54
            $this->getId()
495 54
        );
496
497
        // Cargar XML en el documento.
498 54
        $this->xmlDocument->loadXML($xmlSigned);
499
500
        // Entregar XML firmado.
501 54
        return $xmlSigned;
502
    }
503
504
    /**
505
     * Obtiene una instancia del nodo de la firma.
506
     *
507
     * @return XmlSignatureNode
508
     * @throws SignatureException Si el documento XML no está firmado.
509
     */
510 54
    public function getXmlSignatureNode(): XmlSignatureNode
511
    {
512 54
        $tag = $this->getXmlDocument()->documentElement->tagName;
513 54
        $xpath = '/*[local-name()="' . $tag . '"]/*[local-name()="Signature"]';
514 54
        $signatureElement = XmlUtils::xpath($this->getXmlDocument(), $xpath)->item(0);
515 54
        if ($signatureElement === null) {
516
            throw new SignatureException('El documento XML del DTE no se encuentra firmado (no se encontró el nodo "Signature").');
517
        }
518
519 54
        $xmlSignatureNode = new XmlSignatureNode();
520 54
        $xmlSignatureNode->loadXML($signatureElement->C14N());
521
522 54
        return $xmlSignatureNode;
523
    }
524
525
    /**
526
     * Valida la firma electrónica del documento XML del DTE.
527
     *
528
     * @return void
529
     * @throws SignatureException Si la validación del esquema falla.
530
     */
531 54
    public function validateSignature()
532
    {
533 54
        $xmlSignatureNode = $this->getXmlSignatureNode();
534 54
        $xmlSignatureNode->validate($this->getXmlDocument());
535
    }
536
537
    /**
538
     * Valida el esquema del XML del DTE.
539
     *
540
     * @return void
541
     * @throws XmlException Si la validación del esquema falla.
542
     */
543 55
    public function validateSchema(): void
544
    {
545
        // Las boletas no se validan de manera individual (el DTE). Se validan
546
        // a través del EnvioBOLETA.
547 55
        if ($this->getTipo()->esBoleta()) {
548 5
            return;
549
        }
550
551
        // Validar esquema de otros DTE (no boletas).
552 50
        $schema = 'DTE_v10.xsd';
553 50
        $schemaPath = PathManager::getSchemasPath($schema);
554 50
        XmlValidator::validateSchema($this->getXmlDocument(), $schemaPath);
555
    }
556
557
    /**
558
     * Método general para el renderizado del documento tributario electrónico.
559
     *
560
     * @param array $options Opciones para el renderizado del documento.
561
     * @return string Datos del documento renderizado según las opciones.
562
     */
563 98
    public function render(array $options = []): string
564
    {
565
        // Si el renderer no está creado se crea.
566 98
        if (!isset($this->renderer)) {
567 98
            $this->renderer = new DocumentoRenderer($this->dataProvider);
568
        }
569
570
        // Renderizar el documento.
571 98
        return $this->renderer->renderFromDocumento($this, $options);
572
    }
573
574
    /**
575
     * Genera el HTML del documento tributario electrónico.
576
     *
577
     * @param array $options Opciones para generar el HTML.
578
     * @return string Código HTML generado.
579
     */
580 49
    public function getHtml(array $options = []): string
581
    {
582 49
        $options['format'] = 'html';
583
584 49
        return $this->render($options);
585
    }
586
587
    /**
588
     * Genera el PDF del documento tributario electrónico.
589
     *
590
     * @param array $options Opciones para generar el PDF.
591
     * @return string Datos binarios del PDF generado.
592
     */
593 49
    public function getPdf(array $options = []): string
594
    {
595 49
        $options['format'] = 'pdf';
596
597 49
        return $this->render($options);
598
    }
599
600
    /**
601
     * Genera el ESCPOS del documento tributario electrónico.
602
     *
603
     * @param array $options Opciones para generar el ESCPOS.
604
     * @return string Datos binarios del ESCPOS generado.
605
     */
606
    public function getEscpos(array $options = []): string
607
    {
608
        $options['format'] = 'escpos';
609
610
        return $this->render($options);
611
    }
612
}
613