EstandarParserStrategy::__construct()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 0

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 0
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 3
ccs 1
cts 1
cp 1
crap 1
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * LibreDTE: Biblioteca PHP (Núcleo).
7
 * Copyright (C) LibreDTE <https://www.libredte.cl>
8
 *
9
 * Este programa es software libre: usted puede redistribuirlo y/o modificarlo
10
 * bajo los términos de la Licencia Pública General Affero de GNU publicada 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