EstandarParserStrategy::addDetails()   F
last analyzed

Complexity

Conditions 20
Paths 376

Size

Total Lines 107
Code Lines 60

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 45
CRAP Score 27.2186

Importance

Changes 0
Metric Value
cc 20
eloc 60
c 0
b 0
f 0
nc 376
nop 2
dl 0
loc 107
ccs 45
cts 61
cp 0.7377
crap 27.2186
rs 1.1333

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