Passed
Push — main ( 7fcf59...53a5b5 )
by Jacobo
02:34
created

AeatClient::buildRegistration()   B

Complexity

Conditions 6
Paths 32

Size

Total Lines 70
Code Lines 39

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 39
c 1
b 0
f 0
dl 0
loc 70
rs 8.6737
cc 6
nc 32
nop 13

How to fix   Long Method    Many Parameters   

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:

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
21
    public function __construct(string $certPath, ?string $certPassword = null, bool $production = false)
22
    {
23
        $this->certPath = $certPath;
24
        $this->certPassword = $certPassword;
25
        $this->production = $production;
26
        $this->baseUri = $production
27
            ? 'https://www1.aeat.es'
28
            : 'https://prewww1.aeat.es';
29
        $this->client = new Client([
30
            'cert' => ($certPassword === null) ? $certPath : [$certPath, $certPassword],
31
            'base_uri' => $this->baseUri,
32
            'headers' => [
33
                'User-Agent' => 'LaravelVerifactu/1.0',
34
            ],
35
        ]);
36
    }
37
38
39
40
    /**
41
     * Build fingerprint/hash for invoice chaining
42
     *
43
     * @param string $issuerVat
44
     * @param string $numSerie
45
     * @param string $fechaExp
46
     * @param string $tipoFactura
47
     * @param string $cuotaTotal
48
     * @param string $importeTotal
49
     * @param string $ts
50
     * @param string $prevHash
51
     * @return string
52
     */
53
    private function buildFingerprint(
54
        string $issuerVat,
55
        string $numSerie,
56
        string $fechaExp,
57
        string $tipoFactura,
58
        string $cuotaTotal,
59
        string $importeTotal,
60
        string $ts,
61
        string $prevHash = ''
62
    ): string {
63
        $raw = 'IDEmisorFactura=' . $issuerVat
64
            . '&NumSerieFactura=' . $numSerie
65
            . '&FechaExpedicionFactura=' . $fechaExp
66
            . '&TipoFactura=' . $tipoFactura
67
            . '&CuotaTotal=' . $cuotaTotal
68
            . '&ImporteTotal=' . $importeTotal
69
            . '&Huella=' . $prevHash
70
            . '&FechaHoraHusoGenRegistro=' . $ts;
71
        return strtoupper(hash('sha256', $raw));
72
    }
73
74
    /**
75
     * Send invoice registration to AEAT with support for invoice chaining
76
     *
77
     * @param Invoice $invoice
78
     * @param array|null $previous Previous invoice data for chaining (hash, number, date)
79
     * @return array
80
     */
81
    /**
82
     * Send invoice registration to AEAT with support for invoice chaining
83
     *
84
     * @param VeriFactuInvoice $invoice
85
     * @param array|null $previous Previous invoice data for chaining (hash, number, date)
86
     * @return array
87
     */
88
    /**
89
     * Send invoice registration to AEAT with support for invoice chaining
90
     *
91
     * @param VeriFactuInvoice $invoice
92
     * @param array|null $previous Previous invoice data for chaining (hash, number, date)
93
     * @return array
94
     */
95
    public function sendInvoice(VeriFactuInvoice $invoice, ?array $previous = null): array
96
    {
97
        // 1. Obtener datos del emisor
98
        $issuer = config('verifactu.issuer');
99
        $issuerName = $issuer['name'] ?? '';
100
        $issuerVat = $issuer['vat'] ?? '';
101
102
        // 2. Preparar datos comunes
103
        $ts = \Carbon\Carbon::now('UTC')->format('c');
104
        $numSerie = (string) $invoice->getInvoiceNumber();
105
        $fechaExp = $invoice->getIssueDate()->format('d-m-Y');
106
        $tipoFactura = $invoice->getInvoiceType();
107
        $cuotaTotal = sprintf('%.2f', (float) $invoice->getTaxAmount());
108
        $importeTotal = sprintf('%.2f', (float) $invoice->getTotalAmount());
109
        $prevHash = $previous['hash'] ?? $invoice->getPreviousHash() ?? '';
110
111
        // 3. Generar huella
112
        $huella = $this->buildFingerprint(
113
            $issuerVat,
114
            $numSerie,
115
            $fechaExp,
116
            $tipoFactura,
117
            $cuotaTotal,
118
            $importeTotal,
119
            $ts,
120
            $prevHash
121
        );
122
123
        // 4. Construir partes del mensaje
124
        $cabecera = $this->buildHeader($issuerName, $issuerVat);
125
        $detalle = $this->buildBreakdowns($invoice);
126
        $encadenamiento = $this->buildChaining($previous, $issuerVat);
127
        $destinatarios = $this->buildRecipients($invoice);
128
129
        // 5. Construir RegistroAlta
130
        $registroAlta = $this->buildRegistration(
131
            $invoice,
132
            $issuerName,
133
            $issuerVat,
134
            $numSerie,
135
            $fechaExp,
136
            $tipoFactura,
137
            $cuotaTotal,
138
            $importeTotal,
139
            $ts,
140
            $huella,
141
            $detalle,
142
            $encadenamiento,
143
            $destinatarios
144
        );
145
146
        $body = [
147
            'Cabecera' => $cabecera,
148
            'RegistroFactura' => [
149
                ['RegistroAlta' => $registroAlta]
150
            ],
151
        ];
152
153
        // 6. Enviar
154
        return $this->performSoapCall($body, $huella, $numSerie, $fechaExp, $ts, $previous);
155
    }
156
157
    private function buildHeader(string $issuerName, string $issuerVat): array
158
    {
159
        return [
160
            'ObligadoEmision' => [
161
                'NombreRazon' => $issuerName,
162
                'NIF' => $issuerVat,
163
            ],
164
        ];
165
    }
166
167
    private function buildBreakdowns(VeriFactuInvoice $invoice): array
168
    {
169
        $breakdowns = $invoice->getBreakdowns();
170
        $detalle = [];
171
172
        foreach ($breakdowns as $breakdown) {
173
            $detalle[] = [
174
                'ClaveRegimen' => $breakdown->getRegimeType(),
175
                'CalificacionOperacion' => $breakdown->getOperationType(),
176
                'TipoImpositivo' => (float) $breakdown->getTaxRate(),
177
                'BaseImponibleOimporteNoSujeto' => sprintf('%.2f', (float) $breakdown->getBaseAmount()),
178
                'CuotaRepercutida' => sprintf('%.2f', (float) $breakdown->getTaxAmount()),
179
            ];
180
        }
181
182
        if (count($detalle) === 0) {
183
            $base = sprintf('%.2f', (float) $invoice->getTotalAmount() - $invoice->getTaxAmount());
184
            $detalle[] = [
185
                'ClaveRegimen' => '01',
186
                'CalificacionOperacion' => 'S1',
187
                'TipoImpositivo' => 0.0,
188
                'BaseImponibleOimporteNoSujeto' => $base,
189
                'CuotaRepercutida' => sprintf('%.2f', 0.0),
190
            ];
191
        }
192
193
        return $detalle;
194
    }
195
196
    private function buildChaining(?array $previous, string $issuerVat): array
197
    {
198
        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...
199
            return [
200
                'RegistroAnterior' => [
201
                    'IDEmisorFactura' => $issuerVat,
202
                    'NumSerieFactura' => $previous['number'],
203
                    'FechaExpedicionFactura' => $previous['date'],
204
                    'Huella' => $previous['hash'],
205
                ],
206
            ];
207
        }
208
        return ['PrimerRegistro' => 'S'];
209
    }
210
211
    private function buildRecipients(VeriFactuInvoice $invoice): ?array
212
    {
213
        $recipients = $invoice->getRecipients();
214
        if ($recipients->count() > 0) {
215
            $destinatarios = [];
216
            foreach ($recipients as $recipient) {
217
                $r = ['NombreRazon' => $recipient->getName()];
218
                $taxId = $recipient->getTaxId();
219
                if (!empty($taxId)) {
220
                    $r['NIF'] = $taxId;
221
                }
222
                $destinatarios[] = $r;
223
            }
224
            return ['IDDestinatario' => $destinatarios];
225
        }
226
        return null;
227
    }
228
229
    private function buildRegistration(
230
        VeriFactuInvoice $invoice,
231
        string $issuerName,
232
        string $issuerVat,
233
        string $numSerie,
234
        string $fechaExp,
235
        string $tipoFactura,
236
        string $cuotaTotal,
237
        string $importeTotal,
238
        string $ts,
239
        string $huella,
240
        array $detalle,
241
        array $encadenamiento,
242
        ?array $destinatarios
243
    ): array {
244
        $registroAlta = [
245
            'IDVersion' => '1.0',
246
            'IDFactura' => [
247
                'IDEmisorFactura' => $issuerVat,
248
                'NumSerieFactura' => $numSerie,
249
                'FechaExpedicionFactura' => $fechaExp,
250
            ],
251
            'NombreRazonEmisor' => $issuerName,
252
            'TipoFactura' => $tipoFactura,
253
            'DescripcionOperacion' => $invoice->getOperationDescription(),
254
            'Desglose' => ['DetalleDesglose' => $detalle],
255
            'CuotaTotal' => $cuotaTotal,
256
            'ImporteTotal' => $importeTotal,
257
            'Encadenamiento' => $encadenamiento,
258
            'SistemaInformatico' => [
259
                'NombreRazon' => $issuerName,
260
                'NIF' => $issuerVat,
261
                'NombreSistemaInformatico' => env('APP_NAME', 'LaravelVerifactu'),
262
                'IdSistemaInformatico' => '01',
263
                'Version' => '1.0',
264
                'NumeroInstalacion' => '001',
265
                'TipoUsoPosibleSoloVerifactu' => 'S',
266
                'TipoUsoPosibleMultiOT' => 'N',
267
                'IndicadorMultiplesOT' => 'N',
268
            ],
269
            'FechaHoraHusoGenRegistro' => $ts,
270
            'TipoHuella' => '01',
271
            'Huella' => $huella,
272
        ];
273
274
        // Campos opcionales nuevos
275
        if ($invoice->getOperationDate()) {
276
            $registroAlta['FechaOperacion'] = $invoice->getOperationDate()->format('d-m-Y');
277
        }
278
279
        if ($invoice->getTaxPeriod()) {
280
            $registroAlta['PeriodoImpositivo'] = [
281
                'Ejercicio' => $invoice->getIssueDate()->format('Y'),
282
                'Periodo' => $invoice->getTaxPeriod(),
283
            ];
284
        }
285
286
        if ($invoice->getCorrectionType()) {
287
            $registroAlta['TipoRectificativa'] = $invoice->getCorrectionType();
288
        }
289
290
        if ($invoice->getExternalReference()) {
291
            $registroAlta['RefExterna'] = $invoice->getExternalReference();
292
        }
293
294
        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...
295
            $registroAlta['Destinatarios'] = $destinatarios;
296
        }
297
298
        return $registroAlta;
299
    }
300
301
    protected function getSoapClient(): \SoapClient
302
    {
303
        $wsdl = $this->production
304
            ? 'https://www1.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP?wsdl'
305
            : 'https://prewww2.aeat.es/static_files/common/internet/dep/aplicaciones/es/aeat/tikeV1.0/cont/ws/SistemaFacturacion.wsdl';
306
307
        $options = [
308
            'local_cert' => $this->certPath,
309
            'passphrase' => $this->certPassword,
310
            'trace' => true,
311
            'exceptions' => true,
312
            'cache_wsdl' => 0,
313
            'soap_version' => SOAP_1_1,
314
            'connection_timeout' => 30,
315
            'stream_context' => stream_context_create([
316
                'ssl' => [
317
                    'verify_peer' => true,
318
                    'verify_peer_name' => true,
319
                    'allow_self_signed' => false,
320
                    'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT,
321
                ],
322
                'http' => [
323
                    'user_agent' => 'LaravelVerifactu/1.0',
324
                ],
325
            ]),
326
        ];
327
328
        return new \SoapClient($wsdl, $options);
329
    }
330
331
    private function performSoapCall(array $body, string $huella, string $numSerie, string $fechaExp, string $ts, ?array $previous): array
332
    {
333
        $location = $this->production
334
            ? 'https://www1.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP'
335
            : 'https://prewww1.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP';
336
337
        try {
338
            $client = $this->getSoapClient();
339
            $client->__setLocation($location);
340
            $response = $client->__soapCall('RegFactuSistemaFacturacion', [$body]);
341
            return [
342
                'status' => 'success',
343
                'request' => $client->__getLastRequest(),
344
                'response' => $client->__getLastResponse(),
345
                'aeat_response' => $response,
346
                'hash' => $huella,
347
                'number' => $numSerie,
348
                'date' => $fechaExp,
349
                'timestamp' => $ts,
350
                'first' => $previous ? false : true,
351
            ];
352
        } catch (\SoapFault $e) {
353
            return [
354
                'status' => 'error',
355
                'message' => $e->getMessage(),
356
                'request' => isset($client) ? $client->__getLastRequest() : null,
357
                'response' => isset($client) ? $client->__getLastResponse() : null,
358
            ];
359
        }
360
    }
361
}
362
363