EstandarParserStrategy::setInitialDTE()   F
last analyzed

Complexity

Conditions 33
Paths > 20000

Size

Total Lines 143
Code Lines 108

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 91
CRAP Score 36.6387

Importance

Changes 0
Metric Value
cc 33
eloc 108
c 0
b 0
f 0
nc 1073741824
nop 2
dl 0
loc 143
ccs 91
cts 107
cp 0.8505
crap 36.6387
rs 0

How to fix   Long Method    Complexity   

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 por
11
 * la Fundación para el Software Libre, ya sea la versión 3 de la Licencia, o
12
 * (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\Package\Billing\Component\Document\Worker\Parser\Strategy\Form;
26
27
use Derafu\Backbone\Abstract\AbstractStrategy;
28
use Derafu\Backbone\Attribute\Strategy;
29
use Derafu\Repository\Contract\RepositoryManagerInterface;
30
use libredte\lib\Core\Package\Billing\Component\Document\Contract\ParserStrategyInterface;
31
use libredte\lib\Core\Package\Billing\Component\Document\Contract\TipoDocumentoInterface;
32
use libredte\lib\Core\Package\Billing\Component\Document\Exception\ParserException;
33
use Symfony\Component\Yaml\Exception\ParseException;
34
use Symfony\Component\Yaml\Yaml;
35
36
/**
37
 * Estrategia "billing.document.parser#strategy:form.estandar".
38
 *
39
 * Transforma los datos recibidos a través de un formulario de la vista estándar
40
 * de emisión de DTE a un arreglo PHP con la estructura oficial del SII.
41
 */
42
#[Strategy(name: 'form.estandar', worker: 'parser', component: 'document', package: 'billing')]
43
class EstandarParserStrategy extends AbstractStrategy implements ParserStrategyInterface
44
{
45
    /**
46
     * Constructor de la estrategia con sus dependencias.
47
     *
48
     * @param RepositoryManagerInterface $repositoryManager
49
     */
50 49
    public function __construct(
51
        private RepositoryManagerInterface $repositoryManager
52
    ) {
53 49
    }
54
55
    /**
56
     * Realiza la transformación de los datos del documento.
57
     *
58
     * @param string|array $data Datos de entrada del formulario.
59
     * @return array Arreglo transformado a la estructura oficial del SII.
60
     */
61 49
    public function parse(string|array $data): array
62
    {
63
        // Decodificar datos si vienen en formato YAML.
64 49
        $data = $this->decodeData($data);
65
66
        // Validar datos mínimos requeridos.
67 49
        $this->validateMinimalData($data);
68
69
        // Inicializar los datos del DTE.
70 49
        $dte = [];
71 49
        $this->setInitialDTE($data, $dte);
72
73
        // Procesar los datos adicionales del DTE.
74 49
        $this->processDTEData($data, $dte);
75
76
        // Retornar el DTE procesado.
77 49
        return $dte;
78
    }
79
80
    /**
81
     * Decodifica datos YAML o los retorna directamente si ya son un arreglo.
82
     *
83
     * Verifica si los datos son una cadena YAML y los decodifica a un arreglo
84
     * asociativo. Si los datos ya son un arreglo, los retorna sin cambios.
85
     *
86
     * @param string|array $data Datos a procesar. Puede ser YAML o un arreglo.
87
     * @return array Datos decodificados como arreglo asociativo.
88
     * @throws ParserException Si la cadena YAML proporcionada no es válida.
89
     */
90 49
    private function decodeData(string|array $data): array
91
    {
92
        // Retornar los datos directamente si ya son un arreglo.
93 49
        if (is_array($data)) {
0 ignored issues
show
introduced by
The condition is_array($data) is always true.
Loading history...
94
            return $data;
95
        }
96
97
        // Parsar los datos en string como una cadena YAML.
98
        try {
99 49
            $decoded = Yaml::parse($data);
100
        } catch (ParseException $e) {
101
            throw new ParserException(sprintf(
102
                'El YAML proporcionado es inválido: %s',
103
                $e->getMessage()
104
            ));
105
        }
106
107 49
        return $decoded;
108
    }
109
110
    /**
111
     * Procesa los datos de un DTE y los agrega a la estructura proporcionada.
112
     *
113
     * Este método realiza diversas operaciones sobre los datos del DTE, como la
114
     * adición de pagos programados, datos de traslado, detalles y referencias.
115
     *
116
     * @param array $data Datos del DTE a procesar.
117
     * @param array &$dte Referencia al arreglo del DTE donde se agregarán los
118
     * datos.
119
     *
120
     * @return void
121
     */
122 49
    private function processDTEData(array $data, array &$dte): void
123
    {
124
        // Agregar pagos programados.
125 49
        $this->addScheduledPayment($data, $dte);
126
127
        // Agregar datos de traslado.
128 49
        $this->addTransferData($data, $dte);
129
130
        // Agregar indicador de servicio.
131 49
        $this->addServiceIndicator($data, $dte);
132
133
        // Agregar datos de exportación.
134 49
        $this->addExportData($data, $dte);
135
136
        // Agregar detalles y obtener valores afectos y exentos.
137 49
        [$n_itemAfecto, $n_itemExento] = $this->addDetails($data, $dte);
138
139
        // Procesar impuestos adicionales.
140 49
        $this->addAdditionalTaxes($data, $dte);
141
142
        // Procesar empresa constructora.
143 49
        $this->addConstructionCompany($data, $dte);
144
145
        // Agregar descuentos globales con valores afectos y exentos.
146 49
        $this->addGlobalDiscounts($data, $dte, $n_itemAfecto, $n_itemExento);
147
148
        // Agregar referencias.
149 49
        $this->addReferences($data, $dte);
150
    }
151
152
    /**
153
     * Valida los datos mínimos requeridos para procesar el documento.
154
     *
155
     * Este método verifica que se proporcionen los datos mínimos necesarios
156
     * para generar el documento. Si falta algún dato obligatorio, lanza una
157
     * excepción.
158
     *
159
     * Reglas de validación:
160
     *
161
     *   - Si los datos están vacíos, no se permite acceso directo.
162
     *   - El tipo de documento (`TpoDoc`) es obligatorio.
163
     *   - Los campos mínimos varían según el tipo de documento.
164
     *
165
     * @param array $data Datos del formulario enviados para procesar el
166
     * documento.
167
     * @throws ParserException Si falta algún dato obligatorio o el acceso es
168
     * directo.
169
     */
170 49
    private function validateMinimalData(array $data): void
171
    {
172
        // Si no se proporcionan datos, lanza una excepción indicando.
173 49
        if (empty($data)) {
174
            throw new ParserException(
175
                'No puede acceder de forma directa a la previsualización.'
176
            );
177
        }
178
179
        // Verificar que se haya indicado el tipo de documento.
180 49
        if (empty($data['TpoDoc'])) {
181
            throw new ParserException(
182
                'Debe indicar el tipo de documento a emitir.'
183
            );
184
        }
185
186
        // Definir los campos mínimos requeridos.
187 49
        $datos_minimos = [
188 49
            'FchEmis',
189 49
            'GiroEmis',
190 49
            'Acteco',
191
            // 'DirOrigen', // Nota: Se de debe pasar en los datos.
192
            // 'CmnaOrigen', // Nota: Se de debe pasar en los datos.
193 49
            'RUTRecep',
194 49
            'RznSocRecep',
195 49
            'DirRecep',
196 49
            'NmbItem',
197 49
        ];
198
199
        // Para ciertos tipos de documento, se requieren campos adicionales.
200 49
        if (!in_array($data['TpoDoc'], [56, 61, 110, 111, 112])) {
201 38
            $datos_minimos[] = 'GiroRecep';
202 38
            $datos_minimos[] = 'CmnaRecep';
203
        }
204
205
        // Validar que todos los campos mínimos estén presentes.
206 49
        foreach ($datos_minimos as $attr) {
207 49
            if (empty($data[$attr])) {
208
                throw new ParserException(sprintf(
209
                    'Error al recibir campos mínimos, falta: %s.',
210
                    $attr
211
                ));
212
            }
213
        }
214
    }
215
216
    /**
217
     * Crea la estructura inicial del DTE.
218
     *
219
     * Este método genera un arreglo con la estructura inicial del DTE,
220
     * incluyendo información del encabezado, emisor, receptor y otros datos
221
     * generales que se requieren para procesar el documento.
222
     *
223
     * Estructura del DTE:
224
     * - Encabezado:
225
     *   - IdDoc: Información del documento, como tipo y folio.
226
     *   - Emisor: Detalles del emisor, como RUT y dirección.
227
     *   - Receptor: Detalles del receptor, como RUT y razón social.
228
     *   - RUTSolicita: Información del solicitante, si aplica.
229
     *
230
     * @param array $data Datos de entrada para generar la estructura del DTE.
231
     * @param array &$dte Referencia donde se almacenará la estructura generada.
232
     *
233
     * @return void
234
     */
235 49
    private function setInitialDTE(array $data, array &$dte): void
236
    {
237
        // Crear la estructura base del DTE.
238 49
        $dte = [
239 49
            'Encabezado' => [
240 49
                'IdDoc' => [
241 49
                    'TipoDTE' => $data['TpoDoc'],
242 49
                    'Folio' => !empty($data['Folio'])
243 1
                        ? $data['Folio']
244
                        : false
245 49
                    ,
246 49
                    'FchEmis' => $data['FchEmis'],
247 49
                    'TpoTranCompra' => !empty($data['TpoTranCompra'])
248
                        ? $data['TpoTranCompra']
249
                        : false
250 49
                    ,
251 49
                    'TpoTranVenta' => !empty($data['TpoTranVenta'])
252
                        ? $data['TpoTranVenta']
253
                        : false
254 49
                    ,
255 49
                    'FmaPago' => !empty($data['FmaPago'])
256 4
                        ? $data['FmaPago']
257
                        : false
258 49
                    ,
259 49
                    'FchCancel' => $data['FchVenc'] < $data['FchEmis']
260
                        ? $data['FchVenc']
261
                        : false
262 49
                    ,
263 49
                    'PeriodoDesde' => !empty($data['PeriodoDesde'])
264
                        ? $data['PeriodoDesde']
265
                        : false
266 49
                    ,
267 49
                    'PeriodoHasta' => !empty($data['PeriodoHasta'])
268
                        ? $data['PeriodoHasta']
269
                        : false
270 49
                    ,
271 49
                    'MedioPago' => !empty($data['MedioPago'])
272
                        ? $data['MedioPago']
273
                        : false
274 49
                    ,
275 49
                    'TpoCtaPago' => !empty($data['TpoCtaPago'])
276
                        ? $data['TpoCtaPago']
277
                        : false
278 49
                    ,
279 49
                    'NumCtaPago' => !empty($data['NumCtaPago'])
280
                        ? $data['NumCtaPago']
281
                        : false
282 49
                    ,
283 49
                    'BcoPago' => !empty($data['BcoPago'])
284
                        ? $data['BcoPago']
285
                        : false
286 49
                    ,
287 49
                    'TermPagoGlosa' => !empty($data['TermPagoGlosa'])
288 2
                        ? $data['TermPagoGlosa']
289
                        : false
290 49
                    ,
291 49
                    'FchVenc' => $data['FchVenc'] > $data['FchEmis']
292 1
                        ? $data['FchVenc']
293
                        : false
294 49
                    ,
295 49
                ],
296 49
                'Emisor' => [
297 49
                    'RUTEmisor' => !empty($data['RUTEmisor'])
298 49
                        ? $data['RUTEmisor']
299
                        : false
300 49
                    ,
301 49
                    'RznSoc' => (
302 49
                        $data['RznSoc']
303 49
                        ?? $data['RznSocEmisor']
304
                        ?? null
305
                    ) ?: false,
306 49
                    'GiroEmis' => (
307 49
                        $data['GiroEmis']
308 49
                        ?? $data['GiroEmisor']
309
                        ?? null
310
                    ) ?: false,
311 49
                    'Telefono' => !empty($data['TelefonoEmisor'])
312
                        ? $data['TelefonoEmisor']
313
                        : false
314 49
                    ,
315 49
                    'CorreoEmisor' => !empty($data['CorreoEmisor'])
316
                        ? $data['CorreoEmisor']
317
                        : false
318 49
                    ,
319 49
                    'Acteco' => $data['Acteco'],
320 49
                    'CdgSIISucur' => $data['CdgSIISucur']
321
                        ? $data['CdgSIISucur']
322
                        : false
323 49
                    ,
324 49
                    'DirOrigen' => !empty($data['DirOrigen'])
325
                        ? $data['DirOrigen']
326
                        : false
327 49
                    ,
328 49
                    'CmnaOrigen' => !empty($data['CmnaOrigen'])
329
                        ? $data['CmnaOrigen']
330
                        : false
331 49
                    ,
332 49
                    'CdgVendedor' => $data['CdgVendedor']
333 49
                        ? $data['CdgVendedor']
334
                        : false
335 49
                    ,
336 49
                ],
337 49
                'Receptor' => [
338 49
                    'RUTRecep' => !empty($data['RUTRecep'])
339 49
                        ? $data['RUTRecep']
340
                        : false
341 49
                    ,
342 49
                    'CdgIntRecep' => !empty($data['CdgIntRecep'])
343
                        ? $data['CdgIntRecep']
344
                        : false
345 49
                    ,
346 49
                    'RznSocRecep' => !empty($data['RznSocRecep'])
347 49
                        ? $data['RznSocRecep']
348
                        : false
349 49
                    ,
350 49
                    'GiroRecep' => !empty($data['GiroRecep'])
351 42
                        ? $data['GiroRecep']
352
                        : false
353 49
                    ,
354 49
                    'Contacto' => !empty($data['Contacto'])
355 1
                        ? $data['Contacto']
356
                        : false
357 49
                    ,
358 49
                    'CorreoRecep' => !empty($data['CorreoRecep'])
359 15
                        ? $data['CorreoRecep']
360
                        : false
361 49
                    ,
362 49
                    'DirRecep' => !empty($data['DirRecep'])
363 49
                        ? $data['DirRecep']
364
                        : false
365 49
                    ,
366 49
                    'CmnaRecep' => !empty($data['CmnaRecep'])
367 42
                        ? $data['CmnaRecep']
368
                        : false
369 49
                    ,
370 49
                    'CiudadRecep' => !empty($data['CiudadRecep'])
371 7
                        ? $data['CiudadRecep']
372
                        : false
373 49
                    ,
374 49
                ],
375 49
                'RUTSolicita' => !empty($data['RUTSolicita'])
376
                    ? str_replace('.', '', $data['RUTSolicita'])
377
                    : false
378 49
                ,
379 49
            ],
380 49
        ];
381
    }
382
383
    /**
384
     * Agrega información de pagos programados al DTE.
385
     *
386
     * Este método verifica si la forma de pago es una venta a crédito
387
     * (`FmaPago == 2`) y, si no es una boleta, añade información de los
388
     * pagos programados al DTE.
389
     *
390
     * Comportamiento:
391
     *
392
     *   - Si no hay pagos definidos, usa la fecha de vencimiento (`FchVenc`) y
393
     *     añade una glosa indicando que la fecha de pago es igual al
394
     *     vencimiento.
395
     *   - Si hay pagos definidos, procesa las fechas, montos y glosas de los
396
     *     pagos.
397
     *
398
     * @param array $data Datos de entrada que incluyen información de pagos.
399
     * @param array $dte  Arreglo del DTE en el cual se agregarán los pagos.
400
     *
401
     * @return void
402
     */
403 49
    private function addScheduledPayment(array $data, array &$dte): void
404
    {
405
        // Agregar pagos programados si es venta a crédito y no es boleta.
406
        if (
407 49
            $data['FmaPago'] == 2
408 49
            && !in_array($dte['Encabezado']['IdDoc']['TipoDTE'], [39, 41])
409
        ) {
410
            // Si no hay pagos explícitos se copia la fecha de vencimiento y el
411
            // monto total se determinará en el proceso de normalización
412 1
            if (empty($data['FchPago'])) {
413 1
                if ($data['FchVenc'] > $data['FchEmis']) {
414
                    $dte['Encabezado']['IdDoc']['MntPagos'] = [
415
                        'FchPago' => $data['FchVenc'],
416
                        'GlosaPagos' => 'Fecha de pago igual al vencimiento',
417
                    ];
418
                }
419
            }
420
            // Si hay montos a pagar programados explícitamente.
421
            else {
422
                $dte['Encabezado']['IdDoc']['MntPagos'] = [];
423
                $n_pagos = count($data['FchPago']);
424
                for ($i = 0; $i < $n_pagos; $i++) {
425
                    $dte['Encabezado']['IdDoc']['MntPagos'][] = [
426
                        'FchPago' => $data['FchPago'][$i],
427
                        'MntPago' => $data['MntPago'][$i],
428
                        'GlosaPagos' => !empty($data['GlosaPagos'][$i])
429
                            ? $data['GlosaPagos'][$i]
430
                            : false,
431
                    ];
432
                }
433
            }
434
        }
435
    }
436
437
    /**
438
     * Agrega datos de traslado al DTE.
439
     *
440
     * Verifica si el documento es una guía de despacho (`TipoDTE == 52`).
441
     * Si es así, añade detalles de traslado, como patente, transportista,
442
     * chofer y destino.
443
     *
444
     * Comportamiento:
445
     *
446
     *   - Si se especifica `IndTraslado`, este se agrega al DTE.
447
     *   - Si hay información de transporte (patente, transportista, chofer o
448
     *     destino), esta se incluye en la sección de transporte del DTE.
449
     *
450
     * @param array $data Datos de entrada que incluyen información de traslado.
451
     * @param array $dte  Arreglo del DTE al cual se agregará la información.
452
     *
453
     * @return void
454
     */
455 49
    private function addTransferData(array $data, array &$dte): void
456
    {
457
        // Si no es guía de despacho no se agregan los datos de transporte.
458 49
        if ($dte['Encabezado']['IdDoc']['TipoDTE'] != 52) {
459 44
            return;
460
        }
461
462
        // Si no hay información relevante de transporte se retorna.
463 5
        $dte['Encabezado']['IdDoc']['IndTraslado'] = $data['IndTraslado'];
464
        if (!(
465 5
            !empty($data['Patente'])
466 5
            || !empty($data['RUTTrans'])
467 5
            || (!empty($data['RUTChofer']) && !empty($data['NombreChofer']))
468 5
            || !empty($data['DirDest'])
469 5
            || !empty($data['CmnaDest'])
470
        )) {
471
            return;
472
        }
473
474
        // Añadir la información de transporte.
475 5
        $dte['Encabezado']['Transporte'] = [
476 5
            'Patente' => !empty($data['Patente'])
477 4
                ? $data['Patente']
478
                : false,
479 5
            'RUTTrans' => !empty($data['RUTTrans'])
480 3
                ? str_replace('.', '', $data['RUTTrans'])
481
                : false,
482 5
            'Chofer' => (
483 5
                !empty($data['RUTChofer']) && !empty($data['NombreChofer'])
484 3
            ) ? [
485 3
                    'RUTChofer' => str_replace('.', '', $data['RUTChofer']),
486 3
                    'NombreChofer' => $data['NombreChofer'],
487 3
                ]
488
                : false,
489 5
            'DirDest' => !empty($data['DirDest'])
490 5
                ? $data['DirDest']
491
                : false,
492 5
            'CmnaDest' => !empty($data['CmnaDest'])
493 5
                ? $data['CmnaDest']
494
                : false,
495 5
        ];
496
    }
497
498
    /**
499
     * Agrega el indicador de servicio al DTE.
500
     *
501
     * Procesa `IndServicio` para determinar si debe incluirse en el DTE,
502
     * ajustándolo según el tipo de documento (`TipoDTE`) y validando su valor.
503
     *
504
     * Comportamiento:
505
     *
506
     *   - Si el DTE es una boleta (39 o 41), invierte los valores 1 y 2 de
507
     *     `IndServicio`.
508
     *   - Valida el valor de `IndServicio` según el tipo de documento
509
     *     (`TipoDTE`).
510
     *   - Asigna el indicador de servicio al DTE solo si el valor es válido.
511
     *
512
     * @param array $data Datos de entrada que incluyen el indicador de
513
     * servicio.
514
     * @param array $dte  Arreglo del DTE al cual se agregará el indicador.
515
     *
516
     * @return void
517
     */
518 49
    private function addServiceIndicator(array $data, array &$dte): void
519
    {
520
        // Si no hay indicador de servicio se retorna.
521 49
        if (empty($data['IndServicio'])) {
522 40
            return;
523
        }
524
525
        // Cambiar el tipo de indicador en boletas
526
        // (Valores invertidos respecto a facturas).
527 9
        if (in_array($dte['Encabezado']['IdDoc']['TipoDTE'], [39, 41])) {
528 1
            if ($data['IndServicio'] == 1) {
529
                $data['IndServicio'] = 2;
530 1
            } elseif ($data['IndServicio'] == 2) {
531
                $data['IndServicio'] = 1;
532
            }
533
        }
534
535
        // Quitar indicador de servicio si se pasó para un tipo de documento que
536
        // no corresponde.
537 9
        if (in_array($dte['Encabezado']['IdDoc']['TipoDTE'], [39, 41])) {
538 1
            if (!in_array($data['IndServicio'], [1, 2, 3, 4])) {
539
                $data['IndServicio'] = false;
540
            }
541
        } elseif (
542 8
            in_array($dte['Encabezado']['IdDoc']['TipoDTE'], [110, 111, 112])
543
        ) {
544 7
            if (!in_array($data['IndServicio'], [1, 3, 4, 5])) {
545
                $data['IndServicio'] = false;
546
            }
547
        } else {
548 1
            if (!in_array($data['IndServicio'], [1, 2, 3])) {
549
                $data['IndServicio'] = false;
550
            }
551
        }
552
553
        // Asignar el indicador de servicio al DTE si es válido.
554 9
        if ($data['IndServicio']) {
555 9
            $dte['Encabezado']['IdDoc']['IndServicio'] = $data['IndServicio'];
556
        }
557
    }
558
559
    /**
560
     * Agrega información de exportación al DTE.
561
     *
562
     * Verifica si el tipo de documento es de exportación (`TipoDTE` 110, 111 o
563
     * 112).
564
     *
565
     * Si corresponde, añade datos como la identificación del receptor
566
     * extranjero, moneda y tipo de cambio.
567
     *
568
     * Comportamiento:
569
     *
570
     *   - Añade el número de identificación (`NumId`) y nacionalidad del
571
     *     receptor.
572
     *   - Establece el tipo de moneda (`TpoMoneda`) y, si aplica, el tipo de
573
     *     cambio (`TpoCambio`).
574
     *
575
     * @param array $data Datos que incluyen información de exportación.
576
     * @param array $dte  Estructura del DTE donde se agregarán los datos.
577
     *
578
     * @return void
579
     */
580 49
    private function addExportData(array $data, array &$dte): void
581
    {
582
        // Si no es documento de exportación se retorna.
583 49
        if (!in_array($dte['Encabezado']['IdDoc']['TipoDTE'], [110, 111, 112])) {
584 42
            return;
585
        }
586
587
        // Agregar datos de exportación.
588 7
        if (!empty($data['NumId'])) {
589 1
            $dte['Encabezado']['Receptor']['Extranjero']['NumId'] =
590 1
                $data['NumId']
591 1
            ;
592
        }
593 7
        if (!empty($data['Nacionalidad'])) {
594 7
            $dte['Encabezado']['Receptor']['Extranjero']['Nacionalidad'] =
595 7
                $data['Nacionalidad']
596 7
            ;
597
        }
598 7
        $dte['Encabezado']['Totales']['TpoMoneda'] = $data['TpoMoneda'];
599 7
        if (!empty($data['TpoCambio'])) {
600 1
            $dte['Encabezado']['OtraMoneda'] = [
601 1
                'TpoMoneda' => 'PESO CL',
602 1
                'TpoCambio' => (float)$data['TpoCambio'],
603 1
            ];
604
        }
605
    }
606
607
    /**
608
     * Agrega detalles al DTE.
609
     *
610
     * Procesa y agrega los detalles del documento, como información de los
611
     * ítems, precios, impuestos, descuentos, entre otros. Clasifica los ítems
612
     * en afectos o exentos según sus características.
613
     *
614
     * Comportamiento:
615
     *
616
     *   - Procesa cada ítem verificando códigos, impuestos y descuentos.
617
     *   - Si el documento es una boleta (`TipoDTE == 39`), ajusta precios y
618
     *     descuentos incluyendo el IVA.
619
     *   - Valida que las boletas no contengan impuestos adicionales.
620
     *   - Clasifica los ítems como afectos o exentos.
621
     *
622
     * @param array $data Datos que incluyen los detalles de los ítems.
623
     * @param array $dte  Estructura del DTE donde se agregarán los detalles.
624
     *
625
     * @return array Arreglo con:
626
     *   - El número de ítems afectos.
627
     *   - El número de ítems exentos.
628
     *
629
     * @throws ParserException Si se detectan impuestos adicionales en una
630
     * boleta.
631
     */
632 49
    private function addDetails(array $data, array &$dte): array
633
    {
634
        // Inicializar contadores y lista de detalles.
635 49
        $n_detalles = count($data['NmbItem']);
636 49
        $dte['Detalle'] = [];
637 49
        $n_itemAfecto = 0;
638 49
        $n_itemExento = 0;
639
640
        // Obtener el IVA.
641 49
        $iva_sii = $this->getTax($data['TpoDoc']);
642
643
        // Procesar cada ítem.
644 49
        for ($i = 0; $i < $n_detalles; $i++) {
645 49
            $detalle = [];
646
647
            // Agregar código del ítem.
648 49
            if (!empty($data['VlrCodigo'][$i])) {
649
                if (!empty($data['TpoCodigo'][$i])) {
650
                    $TpoCodigo = $data['TpoCodigo'][$i];
651
                } else {
652
                    $TpoCodigo = 'INT1';
653
                }
654
                $detalle['CdgItem'] = [
655
                    'TpoCodigo' => $TpoCodigo,
656
                    'VlrCodigo' => $data['VlrCodigo'][$i],
657
                ];
658
            }
659
660
            // Agregar otros datos del ítem.
661 49
            $datos = [
662 49
                'IndExe',
663 49
                'NmbItem',
664 49
                'DscItem',
665 49
                'QtyItem',
666 49
                'UnmdItem',
667 49
                'PrcItem',
668 49
                'CodImpAdic',
669 49
            ];
670 49
            foreach ($datos as $d) {
671 49
                if (isset($data[$d][$i])) {
672 49
                    $valor = trim((string) $data[$d][$i]);
673 49
                    if (!empty($valor)) {
674 49
                        $detalle[$d] = is_numeric($valor)
675 49
                            ? (float) $valor
676 49
                            : $valor
677 49
                        ;
678
                    }
679
                }
680
            }
681
682
            // Si es boleta y el ítem no es exento, agregar IVA al precio.
683
            if (
684 49
                $dte['Encabezado']['IdDoc']['TipoDTE'] == 39
685 49
                && (!isset($detalle['IndExe']) || $detalle['IndExe'] == false)
686
            ) {
687
                // IVA.
688 2
                $iva = round((float) $detalle['PrcItem'] * ($iva_sii / 100));
689
690
                // Impuesto adicional (no se permiten impuestos adicionales en
691
                // boletas).
692 2
                if (!empty($detalle['CodImpAdic'])) {
693
                    throw new ParserException(
694
                        'No es posible generar una boleta que tenga impuestos '.
695
                        'adicionales mediante LibreDTE. '.
696
                        'Este es un caso de uso no considerado.',
697
                    );
698
                } else {
699 2
                    $adicional = 0;
700
                }
701
702
                // Agregar al precio.
703 2
                assert(is_numeric($detalle['PrcItem']));
704 2
                $detalle['PrcItem'] += $iva + $adicional;
705
            }
706
707
            // Agregar descuentos.
708 49
            if (!empty($data['ValorDR'][$i]) && !empty($data['TpoValor'][$i])) {
709 4
                if ($data['TpoValor'][$i] == '%') {
710 2
                    $detalle['DescuentoPct'] = round($data['ValorDR'][$i], 2);
711
                } else {
712 2
                    $detalle['DescuentoMonto'] = $data['ValorDR'][$i];
713
                    // Si es boleta y el item no es exento se le agrega el IVA
714
                    // al descuento.
715
                    if (
716 2
                        $dte['Encabezado']['IdDoc']['TipoDTE'] == 39
717 2
                        && (!isset($detalle['IndExe']) || !$detalle['IndExe'])
718
                    ) {
719
                        $iva_descuento = round(
720
                            $detalle['DescuentoMonto'] * ($iva_sii / 100)
721
                        );
722
                        $detalle['DescuentoMonto'] += $iva_descuento;
723
                    }
724
                }
725
            }
726
727
            // Agregar detalle al listado.
728 49
            $dte['Detalle'][] = $detalle;
729
730
            // Contabilizar item afecto o exento.
731 49
            if (empty($detalle['IndExe'])) {
732 33
                $n_itemAfecto++;
733 16
            } elseif ($detalle['IndExe'] == 1) {
734 16
                $n_itemExento++;
735
            }
736
        }
737
738 49
        return [$n_itemAfecto, $n_itemExento];
739
    }
740
741
    /**
742
     * Agrega información de impuestos adicionales al DTE.
743
     *
744
     * Identifica impuestos adicionales en los detalles del documento
745
     * (`Detalle`) y los agrega a los totales (`Totales`) del DTE con su código
746
     * y tasa.
747
     *
748
     * Comportamiento:
749
     *
750
     *   - Busca impuestos únicos en los ítems (`CodImpAdic`).
751
     *   - Obtiene la tasa de cada impuesto de los datos de entrada.
752
     *   - Si hay impuestos adicionales, los agrega en `ImptoReten`.
753
     *
754
     * @param array $data Datos con tasas de impuestos adicionales.
755
     * @param array $dte  Estructura del DTE para agregar los impuestos.
756
     *
757
     * @return void
758
     */
759 49
    private function addAdditionalTaxes(array $data, array &$dte): void
760
    {
761
        // Si hay impuestos adicionales se copian los datos a totales para que
762
        // se calculen los montos.
763 49
        $CodImpAdic = [];
764 49
        foreach ($dte['Detalle'] as $d) {
765
            if (
766 49
                !empty($d['CodImpAdic'])
767 49
                && !in_array($d['CodImpAdic'], $CodImpAdic)
768
            ) {
769 5
                $CodImpAdic[] = (int) $d['CodImpAdic'];
770
            }
771
        }
772
773
        // Crear el arreglo de impuestos retenidos con sus tasas.
774 49
        $ImptoReten = [];
775 49
        foreach ($CodImpAdic as $codigo) {
776 5
            if (!empty($data['impuesto_adicional_tasa_' . $codigo])) {
777 5
                $ImptoReten[] = [
778 5
                    'TipoImp' => $codigo,
779 5
                    'TasaImp' => $data['impuesto_adicional_tasa_' . $codigo],
780 5
                ];
781
            }
782
        }
783
784
        // Agregar impuestos adicionales a los totales si existen.
785 49
        if ($ImptoReten) {
786 5
            $dte['Encabezado']['Totales']['ImptoReten'] = $ImptoReten;
787
        }
788
    }
789
790
    /**
791
     * Marca el DTE como perteneciente a una empresa constructora.
792
     *
793
     * Verifica si el emisor es una empresa constructora. Si se cumplen las
794
     * condiciones, añade la clave `CredEC` en los totales del DTE, indicando
795
     * derecho a un crédito del 65%.
796
     *
797
     * Condiciones:
798
     *
799
     *   - La configuración `constructora` debe estar habilitada en los datos.
800
     *   - El tipo de documento (`TipoDTE`) debe ser factura, guía o nota.
801
     *   - El campo `CredEC` debe estar presente en los datos de entrada.
802
     *
803
     * @param array $data Datos que incluyen configuración del emisor.
804
     * @param array $dte  Estructura del DTE donde se marcará `CredEC`.
805
     *
806
     * @return void
807
     */
808 49
    private function addConstructionCompany(array $data, array &$dte): void
809
    {
810
        // Si la empresa es constructora se marca para obtener el crédito del
811
        // 65%.
812 49
        $config_extra_constructora = !empty($data['constructora'])
813
            ? $data['constructora']
814 49
            : false
815 49
        ;
816
        if (
817 49
            $config_extra_constructora
818 49
            && in_array($dte['Encabezado']['IdDoc']['TipoDTE'], [33, 52, 56, 61])
819 49
            && !empty($data['CredEC'])
820
        ) {
821
            $dte['Encabezado']['Totales']['CredEC'] = true;
822
        }
823
    }
824
825
    /**
826
     * Agrega descuentos globales al DTE.
827
     *
828
     * Procesa los descuentos globales definidos en los datos de entrada y
829
     * los aplica a ítems afectos, exentos o ambos.
830
     *
831
     * En boletas (`TipoDTE == 39`), ajusta valores en moneda para incluir el
832
     * IVA.
833
     *
834
     * Comportamiento:
835
     *
836
     *   - Valida el tipo (`TpoValor_global`) y el monto (`ValorDR_global`) del
837
     *     descuento.
838
     *   - Aplica descuentos globales a ítems afectos, exentos o ambos.
839
     *
840
     * @param array $data         Datos con información del descuento global.
841
     * @param array $dte          Estructura del DTE para agregar los descuentos.
842
     * @param int   $n_itemAfecto Número de ítems afectos en el documento.
843
     * @param int   $n_itemExento Número de ítems exentos en el documento.
844
     *
845
     * @return void
846
     */
847 49
    private function addGlobalDiscounts(
848
        array $data,
849
        array &$dte,
850
        int $n_itemAfecto,
851
        int $n_itemExento
852
    ): void {
853
        // Si no hay descuentos globales se retorna.
854 49
        if (empty($data['ValorDR_global']) || empty($data['TpoValor_global'])) {
855 45
            return;
856
        }
857
858
        // Obtener el IVA.
859 4
        $iva_sii = $this->getTax($data['TpoDoc']);
860
861
        // Agregar descuentos globales.
862 4
        $TpoValor_global = $data['TpoValor_global'];
863 4
        $ValorDR_global = $data['ValorDR_global'];
864
865
        // Si el descuento es porcentual, redondearlo a 2 decimales.
866 4
        if ($TpoValor_global == '%') {
867 4
            $ValorDR_global = round($ValorDR_global, 2);
868
        }
869
870
        // Para boletas con valor en moneda, ajustar con el IVA.
871
        if (
872 4
            $dte['Encabezado']['IdDoc']['TipoDTE'] == 39
873 4
            && $TpoValor_global == '$'
874
        ) {
875
            $ValorDR_global = round($ValorDR_global * (1 + $iva_sii / 100));
876
        }
877
878
        // Agregar descuentos globales al DTE.
879 4
        $dte['DscRcgGlobal'] = [];
880 4
        if ($n_itemAfecto) {
881 2
            $dte['DscRcgGlobal'][] = [
882 2
                'TpoMov' => 'D',
883 2
                'TpoValor' => $TpoValor_global,
884 2
                'ValorDR' => $ValorDR_global,
885 2
            ];
886
        }
887 4
        if ($n_itemExento) {
888 2
            $dte['DscRcgGlobal'][] = [
889 2
                'TpoMov' => 'D',
890 2
                'TpoValor' => $TpoValor_global,
891 2
                'ValorDR' => $ValorDR_global,
892 2
                'IndExeDR' => 1,
893 2
            ];
894
        }
895
    }
896
897
    /**
898
     * Agrega referencias al DTE.
899
     *
900
     * Procesa las referencias de los datos de entrada y las incluye en el DTE.
901
     * Cada referencia contiene información como tipo de documento, folio,
902
     * fecha, código de referencia y razón de referencia.
903
     *
904
     * Comportamiento:
905
     *
906
     *   - Recorre las referencias en los datos de entrada.
907
     *   - Si el folio es `0`, se marca como referencia global
908
     *     (`IndGlobal == 1`).
909
     *   - Añade las referencias procesadas al arreglo `Referencia` del DTE.
910
     *
911
     * @param array $data Datos con información de las referencias.
912
     * @param array $dte  Estructura del DTE donde se agregarán las referencias.
913
     *
914
     * @return void
915
     */
916 49
    private function addReferences(array $data, array &$dte): void
917
    {
918
        // Si no hay referencias se retorna.
919 49
        if (!isset($data['TpoDocRef'][0])) {
920 36
            return;
921
        }
922
923
        // Procesar cada referencia.
924 13
        $n_referencias = count($data['TpoDocRef']);
925 13
        $dte['Referencia'] = [];
926 13
        for ($i = 0; $i < $n_referencias; $i++) {
927 13
            $dte['Referencia'][] = [
928 13
                'TpoDocRef' => $data['TpoDocRef'][$i],
929 13
                'IndGlobal' => (
930 13
                    is_numeric($data['FolioRef'][$i])
931 13
                    && $data['FolioRef'][$i] == 0
932 13
                )
933 1
                    ? 1
934
                    : false
935 13
                ,
936 13
                'FolioRef' => $data['FolioRef'][$i],
937 13
                'FchRef' => $data['FchRef'][$i],
938 13
                'CodRef' => !empty($data['CodRef'][$i])
939 10
                    ? $data['CodRef'][$i]
940
                    : false
941 13
                ,
942 13
                'RazonRef' => !empty($data['RazonRef'][$i])
943 9
                    ? $data['RazonRef'][$i]
944
                    : false
945 13
                ,
946 13
            ];
947
        }
948
    }
949
950
    /**
951
     * Obtiene la tasa de impuesto predeterminada para un tipo de documento.
952
     *
953
     * Este método consulta el repositorio de tipos de documentos para obtener
954
     * la tasa de IVA predeterminada asociada al tipo de documento especificado.
955
     *
956
     * Comportamiento:
957
     *
958
     *   - Si no se encuentra el tipo de documento, retorna `null`.
959
     *   - Si se encuentra, retorna la tasa predeterminada de IVA.
960
     *
961
     * @param int $documentType ID del tipo de documento a consultar.
962
     * @return float|false La tasa de IVA del documento si tiene una asociada.
963
     * @throws ParserException Si el documento solicitado no fue encontrado.
964
     */
965 49
    private function getTax(int $documentType): float|false
966
    {
967
        // Buscar el documento en el repositorio.
968 49
        $result = $this->repositoryManager
969 49
            ->getRepository(TipoDocumentoInterface::class)
970 49
            ->find($documentType)
971 49
        ;
972
973
        // Retornar null si no se encuentra ningún resultado.
974 49
        if (empty($result)) {
975
            throw new ParserException(sprintf(
976
                'No se pudo recuperar el IVA para el documento %d.',
977
                $documentType
978
            ));
979
        }
980
981
        // Retornar el tax.
982 49
        return $result->getDefaultTasaIVA();
983
    }
984
}
985