Passed
Push — master ( c02ff8...8cf6c5 )
by Esteban De La Fuente
06:05
created

AbstractDocumento::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
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\Xml\XmlConverter;
39
use libredte\lib\Core\Xml\XmlDocument;
40
use libredte\lib\Core\Xml\XmlException;
41
use libredte\lib\Core\Xml\XmlUtils;
42
use libredte\lib\Core\Xml\XmlValidator;
43
44
/**
45
 * Clase abstracta (base) de la representación de un documento.
46
 */
47
abstract class AbstractDocumento
48
{
49
    /**
50
     * Código del tipo de documento tributario al que está asociada esta
51
     * instancia de un documento.
52
     */
53
    protected int $codigo;
54
55
    /**
56
     * Instancia del tipo de documento tributario, según el código, asociado a
57
     * esta instancia de un documento.
58
     *
59
     * @var DocumentoTipo
60
     */
61
    private DocumentoTipo $tipo;
62
63
    /**
64
     * Arreglo con los datos del documento tributario.
65
     *
66
     * Estos datos podrían o no haber sido normalizados. Sin embargo, si no
67
     * fueron normalizados, se espera que se hayan asignados según lo que el
68
     * SII requiere (o sea, como si se hubiesen "normalizado").
69
     *
70
     * @var array
71
     */
72
    protected array $data;
73
74
    /**
75
     * Instancia del documento XML asociado a los datos.
76
     *
77
     * @var XmlDocument
78
     */
79
    protected XmlDocument $xmlDocument;
80
81
    /**
82
     * Contribuyente emisor del documento.
83
     *
84
     * Este objeto representa al contribuyente que emitió el documento.
85
     *
86
     * @var Contribuyente
87
     */
88
    private Contribuyente $emisor;
89
90
    /**
91
     * Contribuyente receptor del documento.
92
     *
93
     * Este objeto representa al contribuyente que recibió el documento.
94
     *
95
     * @var Contribuyente
96
     */
97
    private Contribuyente $receptor;
98
99
    /**
100
     * Proveedor de datos.
101
     *
102
     * @var DataProviderInterface
103
     */
104
    protected DataProviderInterface $dataProvider;
105
106
    /**
107
     * Constructor de la clase.
108
     *
109
     * @param DataProviderInterface|null $dataProvider Proveedor de datos.
110
     */
111 116
    public function __construct(?DataProviderInterface $dataProvider = null)
112
    {
113 116
        $this->dataProvider = $dataProvider ?? new ArrayDataProvider();
114
    }
115
116
    /**
117
     * Entrega la instancia del tipo de documento asociado a este documento.
118
     *
119
     * @return DocumentoTipo
120
     */
121 116
    public function getTipo(): DocumentoTipo
122
    {
123 116
        if (!isset($this->tipo)) {
124 116
            $this->tipo = new DocumentoTipo(
125 116
                $this->codigo,
126 116
                $this->dataProvider
127 116
            );
128
        }
129
130 116
        return $this->tipo;
131
    }
132
133
    /**
134
     * Asigna los datos del documento.
135
     *
136
     * @param array $data
137
     * @return self
138
     */
139 114
    public function setData(array $data): self
140
    {
141 114
        $this->data = $data;
142 114
        unset($this->xmlDocument);
143
144 114
        return $this;
145
    }
146
147
    /**
148
     * Obtiene los datos del documento.
149
     *
150
     * @return array
151
     */
152 114
    public function getData(): array
153
    {
154 114
        return $this->data;
155
    }
156
157
    /**
158
     * Carga el XML completo de un documento para crear la instancia del
159
     * documento XML asociada a este documento.
160
     *
161
     * @param string $xml
162
     * @return self
163
     */
164 2
    public function loadXML(string $xml): self
165
    {
166 2
        $this->xmlDocument = new XmlDocument();
167 2
        $this->xmlDocument->loadXML($xml);
168
169 2
        return $this;
170
    }
171
172
    /**
173
     * Entrega el string XML del documento XML.
174
     *
175
     * Es un wrapper de XmlDocument::getXML();
176
     *
177
     * @return string
178
     */
179 2
    public function getXml(): string
180
    {
181 2
        return $this->getXmlDocument()->getXML();
182
    }
183
184
    /**
185
     * Entrega el string XML del documento XML.
186
     *
187
     * Es un wrapper de XmlDocument::saveXML();
188
     *
189
     * @return string
190
     */
191 27
    public function saveXml(): string
192
    {
193 27
        return $this->getXmlDocument()->saveXML();
194
    }
195
196
    /**
197
     * Entrega el ID que LibreDTE asigna al documento.
198
     *
199
     * @return string
200
     */
201 56
    public function getId(): string
202
    {
203 56
        return sprintf(
204 56
            'LibreDTE_T%dF%d',
205 56
            $this->getCodigo(),
206 56
            $this->getFolio()
207 56
        );
208
    }
209
210
    /**
211
     * Entrega el código del tipo de documento tributario.
212
     *
213
     * @return int
214
     */
215 56
    public function getCodigo(): int
216
    {
217 56
        return $this->codigo;
218
    }
219
220
    /**
221
     * Entrega el folio del documento tributario.
222
     *
223
     * @return int
224
     */
225 83
    public function getFolio(): int
226
    {
227 83
        $data = $this->getData();
228
229 83
        return (int) $data['Encabezado']['IdDoc']['Folio'];
230
    }
231
232
    /**
233
     * Entrega la fecha de emisión asignada al documento tributario.
234
     *
235
     * Esta es la fecha de emisión informada al SII del documento, no es la
236
     * fecha de creación real del documento en LibreDTE.
237
     *
238
     * @return string
239
     */
240 56
    public function getFechaEmision(): string
241
    {
242 56
        $data = $this->getData();
243
244 56
        return $data['Encabezado']['IdDoc']['FchEmis'];
245
    }
246
247
    /**
248
     * Obtiene el contribuyente emisor del documento.
249
     *
250
     * @return Contribuyente Instancia de Contribuyente que representa al
251
     * emisor.
252
     */
253 56
    public function getEmisor(): Contribuyente
254
    {
255 56
        if (!isset($this->emisor)) {
256 56
            $data = $this->getData();
257
258 56
            $this->emisor = new Contribuyente(
259 56
                data: $data['Encabezado']['Emisor'],
260 56
                dataProvider: $this->dataProvider
261 56
            );
262
        }
263
264 56
        return $this->emisor;
265
    }
266
267
    /**
268
     * Obtiene el contribuyente receptor del documento.
269
     *
270
     * @return Contribuyente Instancia de Contribuyente que representa al
271
     * receptor.
272
     */
273 56
    public function getReceptor(): Contribuyente
274
    {
275 56
        if (!isset($this->receptor)) {
276 56
            $data = $this->getData();
277
278 56
            $this->receptor = new Contribuyente(
279 56
                data: $data['Encabezado']['Receptor'],
280 56
                dataProvider: $this->dataProvider
281 56
            );
282
        }
283
284 56
        return $this->receptor;
285
    }
286
287
    /**
288
     * Entrega todos los valores del tag "Totales".
289
     *
290
     * @return array
291
     */
292
    public function getTotales(): array
293
    {
294
        $data = $this->getData();
295
296
        return $data['Encabezado']['Totales'];
297
    }
298
299
    /**
300
     * Entrega el monto total del documento.
301
     *
302
     * El monto estará en la moneda del documento.
303
     *
304
     * @return int|float Monto total del documento.
305
     */
306 60
    public function getMontoTotal(): int|float
307
    {
308 60
        $data = $this->getData();
309 60
        $monto = $data['Encabezado']['Totales']['MntTotal'];
310
311
        // Verificar si el monto es equivalente a un entero.
312 60
        if (is_float($monto) && floor($monto) == $monto) {
313 9
            return (int) $monto;
314
        }
315
316
        // Entregar como flotante.
317 52
        return $monto;
318
    }
319
320
    /**
321
     * Entrega el detalle del documento.
322
     *
323
     * Se puede solicitar todo el detalle o el detalle de una línea en
324
     * específico.
325
     *
326
     * @param integer|null $index Índice de la línea de detalle solicitada o
327
     * `null` (por defecto) para obtener todas las líneas.
328
     * @return array
329
     */
330 56
    public function getDetalle(?int $index = null): array
331
    {
332 56
        $data = $this->getData();
333
334 56
        return $index !== null
335 56
            ? $data['Detalle'][$index] ?? []
336 56
            : $data['Detalle'] ?? []
337 56
        ;
338
    }
339
340
    /**
341
     * Obtiene la instancia del documento XML asociada al documento tributario.
342
     *
343
     * @return XmlDocument
344
     */
345 57
    public function getXmlDocument(): XmlDocument
346
    {
347 57
        if (!isset($this->xmlDocument)) {
348 56
            $xmlDocumentData = [
349 56
                'DTE' => [
350 56
                    '@attributes' => [
351 56
                        'version' => '1.0',
352 56
                        'xmlns' => 'http://www.sii.cl/SiiDte',
353 56
                    ],
354 56
                    $this->getTipo()->getTagXML() => array_merge([
355 56
                        '@attributes' => [
356 56
                            'ID' => $this->getId(),
357 56
                        ],
358 56
                    ], $this->getData()),
359 56
                ],
360 56
            ];
361 56
            $this->xmlDocument = XmlConverter::arrayToXml($xmlDocumentData);
0 ignored issues
show
Documentation Bug introduced by
It seems like libredte\lib\Core\Xml\Xm...ToXml($xmlDocumentData) can also be of type DOMElement. However, the property $xmlDocument is declared as type libredte\lib\Core\Xml\XmlDocument. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
362
        }
363
364 57
        return $this->xmlDocument;
365
    }
366
367
    /**
368
     * Realiza el timbrado del documento.
369
     *
370
     * @param Caf $caf Instancia del CAF con el que se desea timbrar.
371
     * @param string $timestamp Marca de tiempo a utilizar en el timbre.
372
     * @throws CafException Si existe algún problema al timbrar el documento.
373
     */
374 56
    public function timbrar(Caf $caf, ?string $timestamp = null): void
375
    {
376
        // Verificar que el folio del documento esté dentro del rango del CAF.
377 56
        if (!$caf->enRango($this->getFolio())) {
378
            throw new CafException(sprintf(
379
                'El folio %d del documento %s no está disponible en el rango del CAF %s.',
380
                $this->getFolio(),
381
                $this->getID(),
382
                $caf->getID()
383
            ));
384
        }
385
386
        // Asignar marca de tiempo si no se pasó una.
387 56
        if ($timestamp === null) {
388 56
            $timestamp = date('Y-m-d\TH:i:s');
389
        }
390
391
        // Corroborar que el CAF esté vigente según el timestamp usado.
392 56
        if (!$caf->vigente($timestamp)) {
393
            throw new CafException(sprintf(
394
                'El CAF %s que contiene el folio %d del documento %s no está vigente, venció el día %s.',
395
                $caf->getID(),
396
                $this->getFolio(),
397
                $this->getID(),
398
                (new DateTime($caf->getFechaVencimiento()))->format('d/m/Y'),
399
            ));
400
        }
401
402
        // Preparar datos del timbre.
403 56
        $tedData = [
404 56
            'TED' => [
405 56
                '@attributes' => [
406 56
                    'version' => '1.0',
407 56
                ],
408 56
                'DD' => [
409 56
                    'RE' => $this->getEmisor()->getRut(),
410 56
                    'TD' => $this->getTipo()->getCodigo(),
411 56
                    'F' => $this->getFolio(),
412 56
                    'FE' => $this->getFechaEmision(),
413 56
                    'RR' => $this->getReceptor()->getRut(),
414 56
                    'RSR' => $this->getReceptor()->getRazonSocial(),
415 56
                    'MNT' => $this->getMontoTotal(),
416 56
                    'IT1' => $this->getDetalle(0)['NmbItem'] ?? '',
417 56
                    'CAF' => $caf->getAutorizacion(),
418 56
                    'TSTED' => $timestamp,
419 56
                ],
420 56
                'FRMT' => [
421 56
                    '@attributes' => [
422 56
                        'algoritmo' => 'SHA1withRSA',
423 56
                    ],
424 56
                    '@value' => '', // Se agregará luego.
425 56
                ],
426 56
            ],
427 56
        ];
428
429
        // Armar XML del timbre y obtener los datos a timbrar (tag DD: datos
430
        // del documento).
431 56
        $tedXmlDocument = XmlConverter::arrayToXml($tedData);
432 56
        $ddToStamp = $tedXmlDocument->C14NWithIsoEncodingFlattened('/TED/DD');
0 ignored issues
show
Bug introduced by
The method C14NWithIsoEncodingFlattened() does not exist on DOMElement. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

432
        /** @scrutinizer ignore-call */ 
433
        $ddToStamp = $tedXmlDocument->C14NWithIsoEncodingFlattened('/TED/DD');

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
433
434
        // Timbrar los "datos a timbrar" $ddToStamp.
435 56
        $timbre = $caf->timbrar($ddToStamp);
436 56
        $tedData['TED']['FRMT']['@value'] = $timbre;
437
438
        // Actualizar los datos del documento incorporando el timbre calculado.
439 56
        $newData = array_merge($this->getData(), $tedData);
440 56
        $this->setData($newData);
441
    }
442
443
    /**
444
     * Realiza la firma del documento.
445
     *
446
     * @param Certificate $certificate Instancia que representa la firma
447
     * electrónica.
448
     * @param string $timestamp Marca de tiempo a utilizar en la firma.
449
     * @return string String con el XML firmado.
450
     * @throws SignatureException Si existe algún problema al firmar el documento.
451
     */
452 29
    public function firmar(Certificate $certificate, ?string $timestamp = null): string
453
    {
454
        // Asignar marca de tiempo si no se pasó una.
455 29
        if ($timestamp === null) {
456 29
            $timestamp = date('Y-m-d\TH:i:s');
457
        }
458
459
        // Corroborar que el certificado esté vigente según el timestamp usado.
460 29
        if (!$certificate->isActive($timestamp)) {
461
            throw new SignatureException(sprintf(
462
                'El certificado digital de %s no está vigente en el tiempo %s, su rango de vigencia es del %s al %s.',
463
                $certificate->getID(),
464
                (new DateTime($timestamp))->format('d/m/Y H:i'),
465
                (new DateTime($certificate->getFrom()))->format('d/m/Y H:i'),
466
                (new DateTime($certificate->getTo()))->format('d/m/Y H:i'),
467
            ));
468
        }
469
470
        // Agregar timestamp.
471 29
        $newData = array_merge($this->getData(), ['TmstFirma' => $timestamp]);
472 29
        $this->setData($newData);
473
474
        // Firmar el tag que contiene el documento y retornar el XML firmado.
475 29
        $xmlSigned = SignatureGenerator::signXml(
476 29
            $this->getXmlDocument(),
477 29
            $certificate,
478 29
            $this->getId()
479 29
        );
480
481
        // Cargar XML en el documento.
482 29
        $this->xmlDocument->loadXML($xmlSigned);
483
484
        // Entregar XML firmado.
485 29
        return $xmlSigned;
486
    }
487
488
    /**
489
     * Obtiene una instancia del nodo de la firma.
490
     *
491
     * @return XmlSignatureNode
492
     * @throws SignatureException Si el documento XML no está firmado.
493
     */
494 29
    public function getXmlSignatureNode(): XmlSignatureNode
495
    {
496 29
        $tag = $this->getXmlDocument()->documentElement->tagName;
497 29
        $xpath = '/*[local-name()="' . $tag . '"]/*[local-name()="Signature"]';
498 29
        $signatureElement = XmlUtils::xpath($this->getXmlDocument(), $xpath)->item(0);
499 29
        if ($signatureElement === null) {
500
            throw new SignatureException('El documento XML del DTE no se encuentra firmado (no se encontró el nodo "Signature").');
501
        }
502
503 29
        $xmlSignatureNode = new XmlSignatureNode();
504 29
        $xmlSignatureNode->loadXML($signatureElement->C14N());
505
506 29
        return $xmlSignatureNode;
507
    }
508
509
    /**
510
     * Valida la firma electrónica del documento XML del DTE.
511
     *
512
     * @return void
513
     * @throws SignatureException Si la validación del esquema falla.
514
     */
515 29
    public function validateSignature()
516
    {
517 29
        $xmlSignatureNode = $this->getXmlSignatureNode();
518 29
        $xmlSignatureNode->validate($this->getXmlDocument());
519
    }
520
521
    /**
522
     * Valida el esquema del XML del DTE.
523
     *
524
     * @return void
525
     * @throws XmlException Si la validación del esquema falla.
526
     */
527 30
    public function validateSchema(): void
528
    {
529
        // Las boletas no se validan de manera individual (el DTE). Se validan
530
        // a través del EnvioBOLETA.
531 30
        if ($this->getTipo()->esBoleta()) {
532 5
            return;
533
        }
534
535
        // Validar esquema de otros DTE (no boletas).
536 25
        $schema = 'DTE_v10.xsd';
537 25
        $schemaPath = PathManager::getSchemasPath($schema);
538 25
        XmlValidator::validateSchema($this->getXmlDocument(), $schemaPath);
539
    }
540
}
541