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

AeatClient::buildBreakdowns()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 27
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 18
c 0
b 0
f 0
dl 0
loc 27
rs 9.6666
cc 3
nc 4
nop 1
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