Passed
Push — master ( cbeb6d...052d75 )
by Esteban De La Fuente
11:37 queued 03:06
created

AbstractDocumento   A

Complexity

Total Complexity 38

Size/Duplication

Total Lines 520
Duplicated Lines 0 %

Test Coverage

Coverage 86.67%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 145
dl 0
loc 520
ccs 156
cts 180
cp 0.8667
rs 9.36
c 1
b 0
f 0
wmc 38

24 Methods

Rating   Name   Duplication   Size   Complexity  
A getEmisor() 0 12 2
A getId() 0 6 1
A getCodigo() 0 3 1
A validateSignature() 0 4 1
A getMontoTotal() 0 12 3
A getPdf() 0 6 1
A validateSchema() 0 12 2
A getReceptor() 0 12 2
A getXml() 0 3 1
A getDetalle() 0 7 2
A firmar() 0 34 3
A __construct() 0 3 1
A getXmlSignatureNode() 0 13 2
A getTotales() 0 5 1
A loadXML() 0 6 1
A getData() 0 3 1
A getTipo() 0 10 2
A timbrar() 0 67 4
A getXmlDocument() 0 20 2
A getHtml() 0 6 1
A getFolio() 0 5 1
A setData() 0 6 1
A saveXml() 0 3 1
A getFechaEmision() 0 5 1
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