EstandarParserStrategy::addTransferData()   C
last analyzed

Complexity

Conditions 14
Paths 66

Size

Total Lines 40
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 24
CRAP Score 14.0125

Importance

Changes 0
Metric Value
cc 14
eloc 28
c 0
b 0
f 0
nc 66
nop 2
dl 0
loc 40
ccs 24
cts 25
cp 0.96
crap 14.0125
rs 6.2666

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