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

AbstractDocumento::getMontoTotal()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 12
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 5
c 1
b 0
f 0
nc 2
nop 0
dl 0
loc 12
ccs 6
cts 6
cp 1
crap 3
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 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