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

Caf::getIdk()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 5
ccs 3
cts 3
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\AutorizacionFolio;
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\CertificateUtils;
32
use libredte\lib\Core\Signature\SignatureException;
33
use libredte\lib\Core\Signature\SignatureGenerator;
34
use libredte\lib\Core\Sii\Contribuyente\Contribuyente;
35
use libredte\lib\Core\Sii\Dte\Documento\DocumentoTipo;
36
use libredte\lib\Core\Sii\HttpClient\ConnectionConfig;
37
use libredte\lib\Core\Xml\XmlConverter;
38
use libredte\lib\Core\Xml\XmlDocument;
39
40
/**
41
 * Clase para representar un Código de Autorización de Folios (CAF).
42
 *
43
 * Un CAF es un archivo XML que contiene los folios autorizados por el Servicio
44
 * de Impuestos Internos (SII) de Chile para la emisión de Documentos
45
 * Tributarios Electrónicos (DTE).
46
 */
47
class Caf
48
{
49
    /**
50
     * Ambiente de certificación del SII.
51
     *
52
     * Este valor se utiliza para identificar que el CAF pertenece al ambiente
53
     * de pruebas o certificación.
54
     */
55
    private const CERTIFICACION = 100;
56
57
    /**
58
     * Ambiente de producción del SII.
59
     *
60
     * Este valor se utiliza para identificar que el CAF pertenece al ambiente
61
     * de producción.
62
     */
63
    private const PRODUCCION = 300;
64
65
    /**
66
     * Mapa de ambientes disponibles para el CAF.
67
     *
68
     * Asocia los valores de los ambientes con las configuraciones
69
     * correspondientes de conexión al SII (certificación o producción).
70
     *
71
     * @var array<int, int>
72
     */
73
    private const AMBIENTES = [
74
        self::CERTIFICACION => ConnectionConfig::CERTIFICACION,
75
        self::PRODUCCION => ConnectionConfig::PRODUCCION,
76
    ];
77
78
    /**
79
     * Datos del CAF en formato de arreglo.
80
     *
81
     * Este arreglo contiene la estructura completa del XML del CAF convertido
82
     * a un formato de arreglo asociativo.
83
     *
84
     * @var array
85
     */
86
    private array $data;
87
88
    /**
89
     * Documento XML del CAF.
90
     *
91
     * Este objeto representa el XML cargado del CAF, utilizado para
92
     * interactuar con el contenido y extraer los datos necesarios.
93
     *
94
     * @var XmlDocument
95
     */
96
    private XmlDocument $xmlDocument;
97
98
    /**
99
     * Contribuyente emisor del CAF.
100
     *
101
     * Este objeto representa al contribuyente que está autorizado para
102
     * utilizar los folios del CAF.
103
     *
104
     * @var Contribuyente
105
     */
106
    private Contribuyente $emisor;
107
108
    /**
109
     * Tipo de documento tributario del CAF.
110
     *
111
     * Este objeto representa el tipo de documento tributario (DTE) asociado al
112
     * CAF, como facturas, boletas, notas de crédito, etc.
113
     *
114
     * @var DocumentoTipo
115
     */
116
    private DocumentoTipo $tipoDocumento;
117
118
    /**
119
     * Proveedor de datos.
120
     *
121
     * @var DataProviderInterface
122
     */
123
    protected DataProviderInterface $dataProvider;
124
125
    /**
126
     * Constructor de la clase.
127
     *
128
     * @param DataProviderInterface|null $dataProvider Proveedor de datos.
129
     */
130 62
    public function __construct(?DataProviderInterface $dataProvider = null)
131
    {
132 62
        $this->dataProvider = $dataProvider ?? new ArrayDataProvider();
133
    }
134
135
    /**
136
     * Carga un string XML de un CAF en la clase.
137
     *
138
     * @param string $xml Contenido del archivo XML del CAF.
139
     */
140 62
    public function loadXML(string $xml): void
141
    {
142 62
        $this->xmlDocument = new XmlDocument();
143 62
        $this->xmlDocument->loadXML($xml);
144
    }
145
146
    /**
147
     * Retorna el XML cargado como string.
148
     *
149
     * @return string Contenido del XML.
150
     */
151
    public function getXML(): string
152
    {
153
        return $this->xmlDocument->saveXML();
154
    }
155
156
    /**
157
     * Método que valida el código de autorización de folios (CAF).
158
     *
159
     * Valida la firma y las claves públicas y privadas asociadas al CAF.
160
     */
161 1
    public function validate(): void
162
    {
163
        // Verificar firma del CAF con la clave pública del SII.
164 1
        $public_key_sii = $this->getCertificateSII();
165 1
        if ($public_key_sii !== null) {
166
            $firma = $this->getFirma();
167
            $signed_da = $this->xmlDocument->C14NWithIsoEncodingFlattened('/AUTORIZACION/CAF/DA');
168
            if (openssl_verify($signed_da, base64_decode($firma), $public_key_sii) !== 1) {
169
                throw new CafException(sprintf(
170
                    'La firma del CAF %s no es válida (no está autorizado por el SII).',
171
                    $this->getID()
172
                ));
173
            }
174
        }
175
176
        // Verificar que la clave pública y privada sean válidas. Esto se hace
177
        // encriptando un texto random y desencriptándolo.
178 1
        $private_key = $this->getPrivateKey();
179 1
        $test_plain = md5(date('U'));
180 1
        if (!openssl_private_encrypt($test_plain, $test_encrypted, $private_key)) {
181
            throw new CafException(sprintf(
182
                'El CAF %s no pasó la validación de su clave privada (posible archivo CAF corrupto).',
183
                $this->getID()
184
            ));
185
        }
186 1
        $public_key = $this->getPublicKey();
187 1
        if (!openssl_public_decrypt($test_encrypted, $test_decrypted, $public_key)) {
188
            throw new CafException(sprintf(
189
                'El CAF %s no pasó la validación de su clave pública (posible archivo CAF corrupto).',
190
                $this->getID()
191
            ));
192
        }
193 1
        if ($test_plain !== $test_decrypted) {
194
            throw new CafException(sprintf(
195
                'El CAF %s no logró encriptar y desencriptar correctamente un texto de prueba (posible archivo CAF corrupto).',
196
                $this->getID()
197
            ));
198
        }
199
    }
200
201
    /**
202
     * Entrega un ID para el CAF generado a partir de los datos del mismo.
203
     *
204
     * @return string
205
     */
206 1
    public function getID(): string
207
    {
208 1
        return sprintf(
209 1
            'CAF%dD%dH%d',
210 1
            $this->getTipoDocumento()->getCodigo(),
211 1
            $this->getFolioDesde(),
212 1
            $this->getFolioHasta()
213 1
        );
214
    }
215
216
    /**
217
     * Obtiene el contribuyente emisor del CAF.
218
     *
219
     * @return Contribuyente Instancia de Contribuyente que representa al emisor.
220
     */
221 1
    public function getEmisor(): Contribuyente
222
    {
223 1
        if (!isset($this->emisor)) {
224 1
            $data = $this->getData();
225
226 1
            $this->emisor = new Contribuyente(
227 1
                rut: $data['AUTORIZACION']['CAF']['DA']['RE'],
228 1
                razon_social: $data['AUTORIZACION']['CAF']['DA']['RS'],
229 1
                dataProvider: $this->dataProvider
230 1
            );
231
        }
232
233 1
        return $this->emisor;
234
    }
235
236
    /**
237
     * Obtiene el tipo de documento tributario del CAF.
238
     *
239
     * @return DocumentoTipo Instancia de DocumentoTipo.
240
     */
241 59
    public function getTipoDocumento(): DocumentoTipo
242
    {
243 59
        if (!isset($this->tipoDocumento)) {
244 59
            $data = $this->getData();
245 59
            $this->tipoDocumento = new DocumentoTipo(
246 59
                codigo: (int) $data['AUTORIZACION']['CAF']['DA']['TD'],
247 59
                dataProvider: $this->dataProvider
248 59
            );
249
        }
250
251 59
        return $this->tipoDocumento;
252
    }
253
254
    /**
255
     * Obtiene el folio inicial autorizado en el CAF.
256
     *
257
     * @return int Folio inicial.
258
     */
259 59
    public function getFolioDesde(): int
260
    {
261 59
        $data = $this->getData();
262
263 59
        return (int) $data['AUTORIZACION']['CAF']['DA']['RNG']['D'];
264
    }
265
266
    /**
267
     * Obtiene el folio final autorizado en el CAF.
268
     *
269
     * @return int Folio final.
270
     */
271 59
    public function getFolioHasta(): int
272
    {
273 59
        $data = $this->getData();
274
275 59
        return (int) $data['AUTORIZACION']['CAF']['DA']['RNG']['H'];
276
    }
277
278
    /**
279
     * Obtiene la cantidad de folios autorizados en el CAF.
280
     *
281
     * @return int Cantidad de folios.
282
     */
283
    public function getCantidadFolios(): int
284
    {
285
        $desde = $this->getFolioDesde();
286
        $hasta = $this->getFolioHasta();
287
288
        return $hasta - $desde + 1;
289
    }
290
291
    /**
292
     * Determina si el folio pasado como argumento está o no dentro del rango
293
     * del CAF.
294
     *
295
     * NOTE: Esta validación NO verifica si el folio ya fue usado, solo si está
296
     * dentro del rango de folios disponibles en el CAF.
297
     *
298
     * @param integer $folio
299
     * @return boolean
300
     */
301 56
    public function enRango(int $folio): bool
302
    {
303 56
        return $folio >= $this->getFolioDesde() && $folio <= $this->getFolioHasta();
304
    }
305
306
    /**
307
     * Obtiene la fecha de autorización del CAF.
308
     *
309
     * @return string Fecha de autorización en formato YYYY-MM-DD.
310
     */
311 34
    public function getFechaAutorizacion(): string
312
    {
313 34
        $data = $this->getData();
314
315 34
        return $data['AUTORIZACION']['CAF']['DA']['FA'];
316
    }
317
318
    /**
319
     * Obtiene la fecha de vencimiento del CAF.
320
     *
321
     * @return string|null Fecha de vencimiento en formato YYYY-MM-DD o `null`
322
     * si no aplica.
323
     */
324 33
    public function getFechaVencimiento(): ?string
325
    {
326 33
        if (!$this->vence()) {
327
            return null;
328
        }
329
330 33
        $fecha_autorizacion = $this->getFechaAutorizacion();
331 33
        if (!$fecha_autorizacion) {
332
            throw new CafException(sprintf(
333
                'No fue posible obtener la fecha de autorización del CAF %s.',
334
                $this->getID()
335
            ));
336
        }
337
338
        // Los folios vencen en 6 meses (6 * 30 días).
339 33
        return date('Y-m-d', strtotime($fecha_autorizacion. ' + 180 days'));
340
    }
341
342
    /**
343
     * Entrega la cantidad de meses que han pasado desde la solicitud del CAF.
344
     *
345
     * @return float Cantidad de meses transcurridos.
346
     */
347
    public function getMesesAutorizacion(): float
348
    {
349
        $d1 = new DateTime($this->getFechaAutorizacion());
350
        $d2 = new DateTime(date('Y-m-d'));
351
        $diff = $d1->diff($d2);
352
        $meses = $diff->m + ($diff->y * 12);
353
354
        if ($diff->d) {
355
            $meses += round($diff->d / 30, 2);
356
        }
357
358
        return $meses;
359
    }
360
361
    /**
362
     * Indica si el CAF está o no vigente.
363
     *
364
     * @param string $timestamp Marca de tiempo para consultar vigencia en un
365
     * momento específico. Si no se indica, por defecto es la fecha y hora
366
     * actual.
367
     * @return bool `true` si el CAF está vigente, `false` si no está vigente.
368
     */
369 56
    public function vigente(?string $timestamp = null): bool
370
    {
371 56
        if (!$this->vence()) {
372 24
            return true;
373
        }
374
375 33
        if ($timestamp === null) {
376
            $timestamp = date('Y-m-d\TH:i:s');
377
        }
378
379 33
        if (!isset($timestamp[10])) {
380
            $timestamp .= 'T00:00:00';
381
        }
382
383 33
        return $timestamp < ($this->getFechaVencimiento() . 'T00:00:00');
384
    }
385
386
    /**
387
     * Indica si el CAF de este tipo de documento vence o no.
388
     *
389
     * @return bool `true` si los folios de este tipo de documento vencen,
390
     * `false` si no vencen.
391
     */
392 56
    public function vence(): bool
393
    {
394 56
        $vencen = [33, 43, 46, 56, 61];
395
396 56
        return in_array($this->getTipoDocumento()->getCodigo(), $vencen);
397
    }
398
399
    /**
400
     * Entrega el ambiente del SII asociado al CAF.
401
     *
402
     * El resultado puede ser:
403
     *
404
     *   - ConnectionConfig::CERTIFICACION el ambiente del CAF es certificación.
405
     *   - ConnectionConfig::PRODUCCION el ambiente del CAF es producción.
406
     *   - `null`: no hay ambiente, pues el Caf es falso y tiene IDK CafFaker::IDK
407
     *
408
     * @return int|null
409
     */
410 1
    public function getAmbiente(): ?int
411
    {
412 1
        $idk = $this->getIDK();
413
414 1
        return $idk === CafFaker::IDK ? null : self::AMBIENTES[$idk];
415
    }
416
417
    /**
418
     * Indica si el CAF es de certificación o producción.
419
     *
420
     * El resultado puede ser:
421
     *
422
     *   - ConnectionConfig::CERTIFICACION es CAF de certificación.
423
     *   - ConnectionConfig::PRODUCCION es CAF de producción.
424
     *   - `null`: indicando que el Caf es falso y tiene IDK CafFaker::IDK
425
     *
426
     * @return int|null
427
     */
428
    public function getCertificacion(): ?int
429
    {
430
        return $this->getAmbiente();
431
    }
432
433
    /**
434
     * Entrega los datos del código de autorización de folios (CAF).
435
     *
436
     * @return array
437
     */
438 56
    public function getAutorizacion(): array
439
    {
440 56
        $data = $this->getData();
441
442 56
        return $data['AUTORIZACION']['CAF'];
443
    }
444
445
    /**
446
     * Timbra los datos con la clave privada del CAF.
447
     *
448
     * En estricto rigor, esto es una firma electrónica. Por lo que se usa
449
     * directamente el método SignatureGenerator::sign().
450
     *
451
     * @param string $data String con el nodo DD a timbrar.
452
     * @return string Timbre (firma) codificado en base64.
453
     */
454 56
    public function timbrar(string $data): string
455
    {
456 56
        $privateKey = $this->getPrivateKey();
457 56
        $signatureAlgorithm = OPENSSL_ALGO_SHA1;
458
459
        try {
460 56
            return SignatureGenerator::sign(
461 56
                $data,
462 56
                $privateKey,
463 56
                $signatureAlgorithm
464 56
            );
465
        } catch (SignatureException) {
466
            throw new CafException('No fue posible timbrar los datos.');
467
        }
468
    }
469
470
    /**
471
     * Obtiene la clave privada proporcionada en el CAF.
472
     *
473
     * @return string Clave privada.
474
     */
475 59
    public function getPrivateKey(): string
476
    {
477 59
        $data = $this->getData();
478
479 59
        return $data['AUTORIZACION']['RSASK'];
480
    }
481
482
    /**
483
     * Obtiene la clave pública proporcionada en el CAF.
484
     *
485
     * @return string Clave pública.
486
     */
487 3
    public function getPublicKey(): string
488
    {
489 3
        $data = $this->getData();
490
491 3
        return $data['AUTORIZACION']['RSAPUBK'];
492
    }
493
494
    /**
495
     * Obtiene el identificador del certificado utilizado en el CAF.
496
     *
497
     * @return int ID del certificado.
498
     */
499 2
    private function getIdk(): int
500
    {
501 2
        $data = $this->getData();
502
503 2
        return (int) $data['AUTORIZACION']['CAF']['DA']['IDK'];
504
    }
505
506
    /**
507
     * Obtiene la firma del SII sobre el nodo DA del CAF.
508
     *
509
     * @return string Firma en base64.
510
     */
511
    private function getFirma(): string
512
    {
513
        $data = $this->getData();
514
515
        return $data['AUTORIZACION']['CAF']['FRMA']['@value'];
516
    }
517
518
    /**
519
     * Obtiene los datos del CAF en formato de arreglo.
520
     *
521
     * @return array Datos del CAF.
522
     */
523 62
    private function getData(): array
524
    {
525 62
        if (!isset($this->data)) {
526 62
            $this->data = XmlConverter::xmlToArray($this->xmlDocument);
527
        }
528
529 62
        return $this->data;
530
    }
531
532
    /**
533
     * Método para obtener la clave pública del CAF a partir del módulo y el
534
     * exponente.
535
     *
536
     * @return string|null Contenido de la clave pública o `null` si es un CAF
537
     * falso.
538
     */
539
    // private function getPublicKeyFromModulusExponent(): ?string
540
    // {
541
    //     $idk = $this->getIDK();
542
    //     if ($idk === CafFaker::IDK) {
543
    //         return null;
544
    //     }
545
546
    //     $data = $this->getData();
547
548
    //     $modulus = $data['AUTORIZACION']['CAF']['DA']['RSAPK']['M'];
549
    //     $exponent = $data['AUTORIZACION']['CAF']['DA']['RSAPK']['E'];
550
551
    //     $public_key = CertificateUtils::generatePublicKeyFromModulusExponent(
552
    //         $modulus,
553
    //         $exponent
554
    //     );
555
556
    //     return $public_key;
557
    // }
558
559
    /**
560
     * Método para obtener el certificado X.509 del SII para la validación del
561
     * XML del CAF.
562
     *
563
     * @return string|null Contenido del certificado o `null` si es un CAF
564
     * falso.
565
     * @throws CafException Si no es posible obtener el certificado del SII.
566
     */
567 1
    private function getCertificateSII(): ?string
568
    {
569 1
        $idk = $this->getIDK();
570 1
        if ($idk === CafFaker::IDK) {
571 1
            return null;
572
        }
573
574
        $filename = $idk . '.cer';
575
        $filepath = PathManager::getCertificatesPath($filename);
576
577
        if ($filepath === null) {
578
            throw new CafException(sprintf(
579
                'No fue posible obtener el certificado del SII %s para validar el CAF.',
580
                $filename
581
            ));
582
        }
583
584
        return file_get_contents($filepath);
585
    }
586
}
587