Passed
Push — master ( 459f8f...596ccb )
by
unknown
06:22
created

EstandarParserStrategy::decodeData()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 17
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3.7898

Importance

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