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