AeatClient::buildFingerprint()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 19
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 9
c 0
b 0
f 0
dl 0
loc 19
rs 9.9666
cc 1
nc 1
nop 8

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Squareetlabs\VeriFactu\Services;
6
7
use GuzzleHttp\Client;
8
use GuzzleHttp\Exception\GuzzleException;
9
use Squareetlabs\VeriFactu\Contracts\VeriFactuInvoice;
10
use Squareetlabs\VeriFactu\Models\Invoice;
11
use Illuminate\Support\Facades\Log;
12
13
class AeatClient
14
{
15
    private string $baseUri;
16
    private string $certPath;
17
    private ?string $certPassword;
18
    private Client $client;
19
    private bool $production;
20
    private bool $verifactuMode;
21
22
    public function __construct(string $certPath, ?string $certPassword = null, bool $production = false, ?bool $verifactuMode = null)
23
    {
24
        $this->certPath = $certPath;
25
        $this->certPassword = $certPassword;
26
        $this->production = $production;
27
        $this->verifactuMode = $verifactuMode ?? config('verifactu.verifactu_mode', true);
28
        $this->baseUri = $production
29
            ? 'https://www1.aeat.es'
30
            : 'https://prewww1.aeat.es';
31
        $this->client = new Client([
32
            'cert' => ($certPassword === null) ? $certPath : [$certPath, $certPassword],
33
            'base_uri' => $this->baseUri,
34
            'headers' => [
35
                'User-Agent' => 'LaravelVerifactu/1.0',
36
            ],
37
        ]);
38
    }
39
40
41
42
    /**
43
     * Build fingerprint/hash for invoice chaining
44
     *
45
     * @param string $issuerVat
46
     * @param string $numSerie
47
     * @param string $fechaExp
48
     * @param string $tipoFactura
49
     * @param string $cuotaTotal
50
     * @param string $importeTotal
51
     * @param string $ts
52
     * @param string $prevHash
53
     * @return string
54
     */
55
    private function buildFingerprint(
56
        string $issuerVat,
57
        string $numSerie,
58
        string $fechaExp,
59
        string $tipoFactura,
60
        string $cuotaTotal,
61
        string $importeTotal,
62
        string $ts,
63
        string $prevHash = ''
64
    ): string {
65
        $raw = 'IDEmisorFactura=' . $issuerVat
66
            . '&NumSerieFactura=' . $numSerie
67
            . '&FechaExpedicionFactura=' . $fechaExp
68
            . '&TipoFactura=' . $tipoFactura
69
            . '&CuotaTotal=' . $cuotaTotal
70
            . '&ImporteTotal=' . $importeTotal
71
            . '&Huella=' . $prevHash
72
            . '&FechaHoraHusoGenRegistro=' . $ts;
73
        return strtoupper(hash('sha256', $raw));
74
    }
75
76
    /**
77
     * Send invoice registration to AEAT with support for invoice chaining
78
     *
79
     * @param Invoice $invoice
80
     * @param array|null $previous Previous invoice data for chaining (hash, number, date)
81
     * @return array
82
     */
83
    /**
84
     * Send invoice registration to AEAT with support for invoice chaining
85
     *
86
     * @param VeriFactuInvoice $invoice
87
     * @param array|null $previous Previous invoice data for chaining (hash, number, date)
88
     * @return array
89
     */
90
    /**
91
     * Send invoice registration to AEAT with support for invoice chaining
92
     *
93
     * @param VeriFactuInvoice $invoice
94
     * @param array|null $previous Previous invoice data for chaining (hash, number, date)
95
     * @return array
96
     */
97
    public function sendInvoice(VeriFactuInvoice $invoice, ?array $previous = null): array
98
    {
99
        // 1. Obtener datos del emisor
100
        $issuer = config('verifactu.issuer');
101
        $issuerName = $issuer['name'] ?? '';
102
        $issuerVat = $issuer['vat'] ?? '';
103
104
        // 2. Preparar datos comunes
105
        $ts = \Carbon\Carbon::now('UTC')->format('c');
106
        $numSerie = (string) $invoice->getInvoiceNumber();
107
        $fechaExp = $invoice->getIssueDate()->format('d-m-Y');
108
        $tipoFactura = $invoice->getInvoiceType();
109
        $cuotaTotal = sprintf('%.2f', (float) $invoice->getTaxAmount());
110
        $importeTotal = sprintf('%.2f', (float) $invoice->getTotalAmount());
111
        $prevHash = $previous['hash'] ?? $invoice->getPreviousHash() ?? '';
112
113
        // 3. Generar huella
114
        $huella = $this->buildFingerprint(
115
            $issuerVat,
116
            $numSerie,
117
            $fechaExp,
118
            $tipoFactura,
119
            $cuotaTotal,
120
            $importeTotal,
121
            $ts,
122
            $prevHash
123
        );
124
125
        // 4. Construir partes del mensaje
126
        $cabecera = $this->buildHeader($issuerName, $issuerVat);
127
        $detalle = $this->buildBreakdowns($invoice);
128
        $encadenamiento = $this->buildChaining($previous, $issuerVat);
129
        $destinatarios = $this->buildRecipients($invoice);
130
131
        // 5. Construir RegistroAlta
132
        $registroAlta = $this->buildRegistration(
133
            $invoice,
134
            $issuerName,
135
            $issuerVat,
136
            $numSerie,
137
            $fechaExp,
138
            $tipoFactura,
139
            $cuotaTotal,
140
            $importeTotal,
141
            $ts,
142
            $huella,
143
            $detalle,
144
            $encadenamiento,
145
            $destinatarios
146
        );
147
148
        $body = [
149
            'Cabecera' => $cabecera,
150
            'RegistroFactura' => [
151
                ['RegistroAlta' => $registroAlta]
152
            ],
153
        ];
154
155
        // 6. Enviar
156
        return $this->performSoapCall($body, $huella, $numSerie, $fechaExp, $ts, $previous);
157
    }
158
159
    private function buildHeader(string $issuerName, string $issuerVat): array
160
    {
161
        return [
162
            'ObligadoEmision' => [
163
                'NombreRazon' => $issuerName,
164
                'NIF' => $issuerVat,
165
            ],
166
        ];
167
    }
168
169
    private function buildBreakdowns(VeriFactuInvoice $invoice): array
170
    {
171
        $breakdowns = $invoice->getBreakdowns();
172
        $detalle = [];
173
174
        foreach ($breakdowns as $breakdown) {
175
            $detalle[] = [
176
                'ClaveRegimen' => $breakdown->getRegimeType(),
177
                'CalificacionOperacion' => $breakdown->getOperationType(),
178
                'TipoImpositivo' => (float) $breakdown->getTaxRate(),
179
                'BaseImponibleOimporteNoSujeto' => sprintf('%.2f', (float) $breakdown->getBaseAmount()),
180
                'CuotaRepercutida' => sprintf('%.2f', (float) $breakdown->getTaxAmount()),
181
            ];
182
        }
183
184
        if (count($detalle) === 0) {
185
            $base = sprintf('%.2f', (float) $invoice->getTotalAmount() - $invoice->getTaxAmount());
186
            $detalle[] = [
187
                'ClaveRegimen' => '01',
188
                'CalificacionOperacion' => 'S1',
189
                'TipoImpositivo' => 0.0,
190
                'BaseImponibleOimporteNoSujeto' => $base,
191
                'CuotaRepercutida' => sprintf('%.2f', 0.0),
192
            ];
193
        }
194
195
        return $detalle;
196
    }
197
198
    private function buildChaining(?array $previous, string $issuerVat): array
199
    {
200
        if ($previous) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $previous of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
201
            return [
202
                'RegistroAnterior' => [
203
                    'IDEmisorFactura' => $issuerVat,
204
                    'NumSerieFactura' => $previous['number'],
205
                    'FechaExpedicionFactura' => $previous['date'],
206
                    'Huella' => $previous['hash'],
207
                ],
208
            ];
209
        }
210
        return ['PrimerRegistro' => 'S'];
211
    }
212
213
    private function buildRecipients(VeriFactuInvoice $invoice): ?array
214
    {
215
        $recipients = $invoice->getRecipients();
216
        if ($recipients->count() > 0) {
217
            $destinatarios = [];
218
            foreach ($recipients as $recipient) {
219
                $r = ['NombreRazon' => $recipient->getName()];
220
                $taxId = $recipient->getTaxId();
221
                if (!empty($taxId)) {
222
                    $r['NIF'] = $taxId;
223
                }
224
                $destinatarios[] = $r;
225
            }
226
            return ['IDDestinatario' => $destinatarios];
227
        }
228
        return null;
229
    }
230
231
    private function buildRegistration(
232
        VeriFactuInvoice $invoice,
233
        string $issuerName,
234
        string $issuerVat,
235
        string $numSerie,
236
        string $fechaExp,
237
        string $tipoFactura,
238
        string $cuotaTotal,
239
        string $importeTotal,
240
        string $ts,
241
        string $huella,
242
        array $detalle,
243
        array $encadenamiento,
244
        ?array $destinatarios
245
    ): array {
246
        $registroAlta = [
247
            'IDVersion' => '1.0',
248
            'IDFactura' => [
249
                'IDEmisorFactura' => $issuerVat,
250
                'NumSerieFactura' => $numSerie,
251
                'FechaExpedicionFactura' => $fechaExp,
252
            ],
253
            'NombreRazonEmisor' => $issuerName,
254
            'TipoFactura' => $tipoFactura,
255
            'DescripcionOperacion' => $invoice->getOperationDescription(),
256
            'Desglose' => ['DetalleDesglose' => $detalle],
257
            'CuotaTotal' => $cuotaTotal,
258
            'ImporteTotal' => $importeTotal,
259
            'Encadenamiento' => $encadenamiento,
260
            'SistemaInformatico' => [
261
                'NombreRazon' => $issuerName,
262
                'NIF' => $issuerVat,
263
                'NombreSistemaInformatico' => config('verifactu.sistema_informatico.name', 'LaravelVerifactu'),
264
                'IdSistemaInformatico' => config('verifactu.sistema_informatico.id', 'LV'),
265
                'Version' => config('verifactu.sistema_informatico.version', '1.0'),
266
                'NumeroInstalacion' => config('verifactu.sistema_informatico.installation_number', '001'),
267
                'TipoUsoPosibleSoloVerifactu' => config('verifactu.sistema_informatico.only_verifactu_capable', 'S'),
268
                'TipoUsoPosibleMultiOT' => config('verifactu.sistema_informatico.multi_obligated_entities_capable', 'N'),
269
                'IndicadorMultiplesOT' => config('verifactu.sistema_informatico.has_multiple_obligated_entities', 'N'),
270
            ],
271
            'FechaHoraHusoGenRegistro' => $ts,
272
            'TipoHuella' => '01',
273
            'Huella' => $huella,
274
        ];
275
276
        // Campos opcionales nuevos
277
        if ($invoice->getOperationDate()) {
278
            $registroAlta['FechaOperacion'] = $invoice->getOperationDate()->format('d-m-Y');
279
        }
280
281
        if ($invoice->getTaxPeriod()) {
282
            $registroAlta['PeriodoImpositivo'] = [
283
                'Ejercicio' => $invoice->getIssueDate()->format('Y'),
284
                'Periodo' => $invoice->getTaxPeriod(),
285
            ];
286
        }
287
288
        if ($invoice->getCorrectionType()) {
289
            $registroAlta['TipoRectificativa'] = $invoice->getCorrectionType();
290
        }
291
292
        if ($invoice->getExternalReference()) {
293
            $registroAlta['RefExterna'] = $invoice->getExternalReference();
294
        }
295
296
        if ($destinatarios) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $destinatarios of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
297
            $registroAlta['Destinatarios'] = $destinatarios;
298
        }
299
300
        return $registroAlta;
301
    }
302
303
    protected function getSoapClient(): \SoapClient
304
    {
305
        if ($this->production) {
306
            $wsdl = $this->verifactuMode
307
                ? 'https://www1.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP?wsdl'
308
                : 'https://www1.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/RequerimientoSOAP?wsdl';
309
        } else {
310
            $wsdl = 'https://prewww2.aeat.es/static_files/common/internet/dep/aplicaciones/es/aeat/tikeV1.0/cont/ws/SistemaFacturacion.wsdl';
311
        }
312
313
        $options = [
314
            'local_cert' => $this->certPath,
315
            'passphrase' => $this->certPassword,
316
            'trace' => true,
317
            'exceptions' => true,
318
            'cache_wsdl' => 0,
319
            'soap_version' => SOAP_1_1,
320
            'connection_timeout' => 30,
321
            'stream_context' => stream_context_create([
322
                'ssl' => [
323
                    'verify_peer' => true,
324
                    'verify_peer_name' => true,
325
                    'allow_self_signed' => false,
326
                    'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT,
327
                ],
328
                'http' => [
329
                    'user_agent' => 'LaravelVerifactu/1.0',
330
                ],
331
            ]),
332
        ];
333
334
        return new \SoapClient($wsdl, $options);
335
    }
336
337
    private function performSoapCall(array $body, string $huella, string $numSerie, string $fechaExp, string $ts, ?array $previous): array
338
    {
339
        if ($this->production) {
340
            $location = $this->verifactuMode
341
                ? 'https://www1.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP'
342
                : 'https://www1.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/RequerimientoSOAP';
343
        } else {
344
            $location = $this->verifactuMode
345
                ? 'https://prewww1.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP'
346
                : 'https://prewww1.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/RequerimientoSOAP';
347
        }
348
349
        try {
350
            $client = $this->getSoapClient();
351
            $client->__setLocation($location);
352
            $response = $client->__soapCall('RegFactuSistemaFacturacion', [$body]);
353
            return [
354
                'status' => 'success',
355
                'request' => $client->__getLastRequest(),
356
                'response' => $client->__getLastResponse(),
357
                'aeat_response' => $response,
358
                'hash' => $huella,
359
                'number' => $numSerie,
360
                'date' => $fechaExp,
361
                'timestamp' => $ts,
362
                'first' => $previous ? false : true,
363
            ];
364
        } catch (\SoapFault $e) {
365
            return [
366
                'status' => 'error',
367
                'message' => $e->getMessage(),
368
                'request' => isset($client) ? $client->__getLastRequest() : null,
369
                'response' => isset($client) ? $client->__getLastResponse() : null,
370
            ];
371
        }
372
    }
373
}
374
375