Passed
Push — master ( 9d79d3...4bb85e )
by Esteban De La Fuente
08:41
created

SiiCheckXmlDocumentSentStatusResponse::getData()   A

Complexity

Conditions 5
Paths 9

Size

Total Lines 46
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 0
Metric Value
eloc 25
dl 0
loc 46
c 0
b 0
f 0
ccs 0
cts 30
cp 0
rs 9.2088
cc 5
nc 9
nop 0
crap 30
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\Integration\Support\Response;
26
27
use libredte\lib\Core\Package\Billing\Component\Integration\Abstract\AbstractSiiWsdlResponse;
28
29
/**
30
 * Respuesta de la consulta de estado de un documento subido al SII.
31
 *
32
 * Referencia: https://www.sii.cl/factura_electronica/factura_mercado/estado_envio.pdf
33
 */
34
class SiiCheckXmlDocumentSentStatusResponse extends AbstractSiiWsdlResponse
35
{
36
    /**
37
     * Estados de salida.
38
     *
39
     * El resultado de la consulta al SII puede arrojar uno de estos estados.
40
     */
41
    private const STATUSES = [
42
        'RSC' => 'Rechazado por Error en Schema',
43
        'SOK' => 'Schema Validado',
44
        'CRT' => 'Carátula OK',
45
        'RFR' => 'Rechazado por Error en Firma',
46
        'FOK' => 'Firma de Envió Validada',
47
        'PDR' => 'Envió en Proceso',
48
        'RCT' => 'Rechazado por Error en Carátula',
49
        'EPR' => 'Envío Procesado',
50
    ];
51
52
    /**
53
     * Estados de salida por ERROR.
54
     *
55
     * El resultado de la consulta al SII puede arrojar uno de estos estados de
56
     * error.
57
     */
58
    private const ERRORS = [
59
        // Otros Errores.
60
        'ESTADO' => [
61
            '-1' => 'ERROR: RETORNO CAMPO ESTADO, NO EXISTE',
62
            '-2' => 'ERROR RETORNO',
63
            '-3' => 'ERROR: RUT USUARIO NO EXISTE',
64
            '-4' => 'ERROR OBTENCION DE DATOS',
65
            '-5' => 'ERROR RETORNO DATOS',
66
            '-6' => 'ERROR: USUARIO NO AUTORIZADO',
67
            '-7' => 'ERROR RETORNO DATOS',
68
            '-8' => 'ERROR: RETORNO DATOS',
69
            '-9' => 'ERROR: RETORNO DATOS',
70
            '-10' => 'ERROR: VALIDA RUT USUARIO',
71
            '-11' => 'ERR_CODE, SQL_CODE, SRV_CODE',
72
            '-12' => 'ERROR: RETORNO CONSULTA',
73
            '-13' => 'ERROR RUT USUARIO NULO',
74
            '-14' => 'ERROR XML RETORNO DATOS',
75
            //'OTRO' => 'No documentado.',
76
        ],
77
        // Errores por autenticación.
78
        'TOKEN' => [
79
            '001' => 'Cookie Inactivo (o Token Inactivo)',
80
            '002' => 'Token Inactivo',
81
            '003' => 'Token No Existe',
82
        ],
83
        // Errores de consulta.
84
        'SRV_CODE' => [
85
            0 => 'Todo Ok',
86
            1 => 'Error en Entrada',
87
            2 => 'Error SQL',
88
        ],
89
        'SQL_CODE' => [
90
            0 => 'Schema Validado',
91
            //'OTRO' => 'Código de Oracle',
92
        ],
93
        'ERR_CODE' => [
94
            0 => 'Se retorna el estado',
95
            1 => 'El envío no es de la Empresa, faltan parámetros de entrada.',
96
            2 => 'Error de Proceso',
97
        ],
98
    ];
99
100
    /**
101
     * Obtiene los datos normalizados de la respuesta.
102
     *
103
     * @return array Datos normalizados de la respuesta del SII.
104
     */
105
    public function getData(): array
106
    {
107
        // Si no existen los datos normalizados de la respuesta se normalizan.
108
        if (!isset($this->data)) {
109
            // Si hay número de atención se normaliza.
110
            [$number, $datetime] = !empty($this->headers['NUM_ATENCION'])
111
                ? $this->parseNumeroAtencion($this->headers['NUM_ATENCION'])
112
                : null
113
            ;
114
115
            // Determinar Track ID.
116
            $track_id = $this->headers['TRACKID']
117
                ?? $this->requestData['TrackId']
118
                ?? null
119
            ;
120
121
            // RUT de la empresa que realizó la consulta del envío.
122
            $company = isset($this->requestData['Rut']) && isset($this->requestData['Dv'])
123
                ? $this->requestData['Rut'] . '-' . $this->requestData['Dv']
124
                : null
125
            ;
126
127
            // Normalizar el estado de la consulta del envío.
128
            [$status, $error, $description] = $this->parseEstado($this->headers);
129
130
            // Normalizar los documentos.
131
            $documents = $this->parseDocumentos($this->body);
132
            $resume = $this->calculateResume($documents);
133
134
            // Armar los datos normalizados.
135
            $this->data = [
136
                'query_number' => $number ?? null,
137
                'query_datetime' => $datetime ?? null,
138
                'track_id' => $track_id,
139
                'company' => $company,
140
                'status' => $status,
141
                'error' => $error,
142
                'description' => $description,
143
                'resume' => $resume,
144
                'documents' => $documents,
145
                'token' => $this->requestData['Token'] ?? null,
146
            ];
147
        }
148
149
        // Entregar los datos de la respuesta del envío del documento al SII.
150
        return $this->data;
151
    }
152
153
    /**
154
     * Devuelve el estado de revisión del documento.
155
     *
156
     * @return string El estado de la revisión del documento.
157
     */
158
    public function getReviewStatus(): string
159
    {
160
        $data = $this->getData();
161
162
        if ($data['status'] === 'EPR') {
163
            if ($this->hasRejectedDocuments()) {
164
                return 'RCH - DTE Rechazado';
165
            }
166
167
            if ($this->hasRepairsDocuments()) {
168
                return 'RLV - DTE Aceptado con Reparos Leves';
169
            }
170
        }
171
172
        return $data['description']
173
            ? ($data['status'] . ' - ' . $data['description'])
174
            : $data['status']
175
        ;
176
    }
177
178
    /**
179
     * Devuelve el detalle de la revisión del documento.
180
     *
181
     * @return string|null El detalle de la revisión o `null` si no existe.
182
     */
183
    public function getReviewDetail(): ?string
184
    {
185
        $data = $this->getData();
186
187
        if ($data['status'] === 'EPR') {
188
            if ($this->allDocumentsAreAccepted()) {
189
                return 'DTE aceptado';
190
            }
191
        }
192
193
        return null;
194
    }
195
196
    /**
197
     * Verifica si todos los documentos fueron aceptados.
198
     *
199
     * @return bool True si todos los documentos fueron aceptados.
200
     */
201
    private function allDocumentsAreAccepted(): bool
202
    {
203
        $data = $this->getData();
204
205
        return $data['resume']['reported'] === $data['resume']['accepted'];
206
    }
207
208
    /**
209
     * Verifica si hay documentos rechazados.
210
     *
211
     * @return bool True si existen documentos rechazados.
212
     */
213
    private function hasRejectedDocuments(): bool
214
    {
215
        $data = $this->getData();
216
217
        return $data['resume']['rejected'] > 0;
218
    }
219
220
    /**
221
     * Verifica si hay documentos con reparos.
222
     *
223
     * @return bool True si existen documentos con reparos.
224
     */
225
    private function hasRepairsDocuments(): bool
226
    {
227
        $data = $this->getData();
228
229
        return $data['resume']['repairs'] > 0;
230
    }
231
232
    /**
233
     * Parsea el estado de la respuesta del SII.
234
     *
235
     * @param array $headers Encabezados de la respuesta.
236
     * @return array Arreglo con el estado, error y descripción.
237
     */
238
    private function parseEstado(array $headers): array
239
    {
240
        // Asignar el código del estado y asumir que hubo error en el proceso
241
        // de envío del documento al SII.
242
        $status = $headers['ESTADO'];
243
        $error = true;
244
        $description = null;
245
246
        // Verificar si el estado es uno de los estados "normales", o sea, no
247
        // es un estado de error en el envío. Se valida esto primero pues es lo
248
        // que normalmente debería entregar el SII en la respuesta.
249
        if (isset(self::STATUSES[$status])) {
250
            $error = $status[0] === 'R';
251
            $description = $headers['GLOSA'] ?? null;
252
        }
253
254
        // Si el estado es un error de token se asigna.
255
        elseif (isset(self::ERRORS['TOKEN'][$status])) {
256
            //$description = self::ERRORS['TOKEN'][$code];
257
            $description = $headers['GLOSA'] ?? null;
258
        }
259
260
        // El error es uno de los números negativos. Donde hay 3 opciones:
261
        // El error es -11 y puede ser uno de los 3 que detalla -11.
262
        elseif ($status == '-11') {
263
            $errors = [];
264
            foreach (['SRV_CODE', 'SQL_CODE', 'ERR_CODE'] as $errorType) {
265
                if (isset($headers[$errorType])) {
266
                    $errors[] = self::ERRORS[$errorType][$headers[$errorType]]
267
                        ?? sprintf(
268
                            'Error %s #%s',
269
                            $errorType,
270
                            $headers[$errorType]
271
                        )
272
                    ;
273
                }
274
            }
275
            $description = implode(' ', $errors);
276
        }
277
278
        // El error es uno de los definidos como error de estado (excepto -11).
279
        elseif (isset(self::ERRORS['ESTADO'][$status])) {
280
            $description = self::ERRORS['ESTADO'][$status];
281
        }
282
283
        // El error es de estado pero no se tiene el código de error registrado.
284
        else {
285
            $description = sprintf(
286
                'Error desconocido código #%s al subir el XML.',
287
                $status
288
            );
289
        }
290
291
        // Entregar valores determinados para el error.
292
        return [$status, $error, $description];
293
    }
294
295
    /**
296
     * Parsea los documentos de la respuesta del SII.
297
     *
298
     * @param array $body Cuerpo de la respuesta del SII.
299
     * @return array Arreglo con los documentos normalizados.
300
     */
301
    private function parseDocumentos(array $body): array
302
    {
303
        // No vienen documentos en el cuerpo.
304
        if (empty($body['TIPO_DOCTO'])) {
305
            return [];
306
        }
307
308
        // Índices donde vienen los datos de los documentos.
309
        $keysMap = [
310
            'TIPO_DOCTO'  => 'type',
311
            'INFORMADOS'  => 'reported',
312
            'ACEPTADOS'   => 'accepted',
313
            'RECHAZADOS'  => 'rejected',
314
            'REPAROS'     => 'repairs',
315
        ];
316
        $keys = array_keys($keysMap);
317
318
        // Normalizar a arreglo de documentos (y otros datos) si no lo son.
319
        if (!is_array($body[$keys[0]])) {
320
            foreach ($keys as $key) {
321
                $body[$key] = [$body[$key] ?? 0];
322
            }
323
        }
324
325
        // Iterar y armar estado de los documentos.
326
        $n_documents = count($body[$keys[0]]);
327
        $documents = [];
328
        for ($i = 0; $i < $n_documents; $i++) {
329
            $document = [];
330
            foreach ($keysMap as $source => $destination) {
331
                $document[$destination] = $body[$source][$i];
332
            }
333
            $documents[$body[$keys[0]][$i]] = $document;
334
        }
335
336
        // Entregar documentos encontrados.
337
        return $documents;
338
    }
339
340
    /**
341
     * Calcula el resumen de los documentos procesados.
342
     *
343
     * @param array $documents Arreglo con los documentos.
344
     * @return array Resumen con el total de reportados, aceptados, rechazados
345
     * y reparos.
346
     */
347
    private function calculateResume(array $documents): array
348
    {
349
        $resume = [
350
            'reported' => 0,
351
            'accepted' => 0,
352
            'rejected' => 0,
353
            'repairs' => 0,
354
        ];
355
        $keys = array_keys($resume);
356
357
        foreach ($documents as $document) {
358
            foreach ($keys as $key) {
359
                $resume[$key] += ($document[$key] ?? 0);
360
            }
361
        }
362
363
        return $resume;
364
    }
365
}
366