AeatClient::performSoapCall()   B
last analyzed

Complexity

Conditions 8
Paths 84

Size

Total Lines 33
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 28
c 1
b 0
f 0
dl 0
loc 33
rs 8.4444
cc 8
nc 84
nop 6
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