Working on ws tests

This commit is contained in:
David Bomba 2025-08-07 14:15:17 +10:00
parent b93ec6dd93
commit ea663394b1
5 changed files with 957 additions and 94 deletions

View File

@ -43,6 +43,7 @@ class Invoice extends BaseXmlModel
protected ?string $privateKeyPath = null;
protected ?string $publicKeyPath = null;
protected ?string $certificatePath = null;
protected ?string $fechaExpedicionFactura = null;
public function __construct()
{
@ -65,6 +66,17 @@ class Invoice extends BaseXmlModel
return $this;
}
public function getFechaExpedicionFactura(): string
{
return $this->fechaExpedicionFactura ?? now()->format('Y-m-d');
}
public function setFechaExpedicionFactura(string $fechaExpedicionFactura): self
{
$this->fechaExpedicionFactura = $fechaExpedicionFactura;
return $this;
}
public function getIdFactura(): string
{
return $this->idFactura;
@ -598,7 +610,7 @@ class Invoice extends BaseXmlModel
$idFactura = $this->createElement($doc, 'IDFactura');
$idFactura->appendChild($this->createElement($doc, 'IDEmisorFactura', $this->tercero?->getNif() ?? 'B12345678'));
$idFactura->appendChild($this->createElement($doc, 'NumSerieFactura', $this->idFactura));
$idFactura->appendChild($this->createElement($doc, 'FechaExpedicionFactura', date('d-m-Y')));
$idFactura->appendChild($this->createElement($doc, 'FechaExpedicionFactura', $this->getFechaExpedicionFactura()));
$root->appendChild($idFactura);
if ($this->refExterna !== null) {

View File

@ -0,0 +1,331 @@
<?php
namespace App\Services\EDocument\Standards\Verifactu\Response;
use DOMDocument;
use DOMElement;
use DOMNodeList;
use Exception;
use Illuminate\Support\Facades\Log;
class ResponseProcessor
{
private DOMDocument $dom;
private ?DOMElement $root = null;
public function __construct()
{
$this->dom = new DOMDocument();
}
/**
* Process AEAT XML response and return structured array
*/
public function processResponse(string $xmlResponse): array
{
try {
$this->loadXml($xmlResponse);
return [
'success' => $this->isSuccessful(),
'status' => $this->getStatus(),
'errors' => $this->getErrors(),
'warnings' => $this->getWarnings(),
'data' => $this->getResponseData(),
'metadata' => $this->getMetadata(),
'raw_response' => $xmlResponse
];
} catch (Exception $e) {
Log::error('Error processing AEAT response', [
'error' => $e->getMessage(),
'xml' => $xmlResponse
]);
return [
'success' => false,
'error' => 'Failed to process response: ' . $e->getMessage(),
'raw_response' => $xmlResponse
];
}
}
/**
* Load XML into DOM
*/
private function loadXml(string $xml): void
{
libxml_use_internal_errors(true);
libxml_clear_errors();
if (!$this->dom->loadXML($xml)) {
$errors = libxml_get_errors();
libxml_clear_errors();
throw new Exception('Invalid XML: ' . ($errors[0]->message ?? 'Unknown error'));
}
$this->root = $this->dom->documentElement;
}
/**
* Check if response indicates success
*/
private function isSuccessful(): bool
{
$estadoEnvio = $this->getElementText('//tikR:EstadoEnvio');
return $estadoEnvio === 'Correcto';
}
/**
* Get response status
*/
private function getStatus(): string
{
return $this->getElementText('//tikR:EstadoEnvio') ?? 'Unknown';
}
/**
* Get all errors from response
*/
private function getErrors(): array
{
$errors = [];
// Check for SOAP faults
$fault = $this->getElementText('//env:Fault/faultstring');
if ($fault) {
$errors[] = [
'type' => 'SOAP_Fault',
'code' => $this->getElementText('//env:Fault/faultcode'),
'message' => $fault,
'details' => $this->getElementText('//env:Fault/detail/callstack')
];
}
// Check for business logic errors
$respuestaLineas = $this->dom->getElementsByTagNameNS(
'https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/RespuestaSuministro.xsd',
'RespuestaLinea'
);
foreach ($respuestaLineas as $linea) {
$estadoRegistro = $this->getElementText('.//tikR:EstadoRegistro', $linea);
if ($estadoRegistro === 'Incorrecto') {
$errors[] = [
'type' => 'Business_Error',
'code' => $this->getElementText('.//tikR:CodigoErrorRegistro', $linea),
'message' => $this->getElementText('.//tikR:DescripcionErrorRegistro', $linea),
'invoice_data' => $this->getInvoiceData($linea)
];
}
}
return $errors;
}
/**
* Get warnings from response
*/
private function getWarnings(): array
{
$warnings = [];
// Check for subsanacion (correction) messages
$subsanacion = $this->getElementText('//tikR:RespuestaLinea/tikR:Subsanacion');
if ($subsanacion) {
$warnings[] = [
'type' => 'Subsanacion',
'message' => $subsanacion
];
}
return $warnings;
}
/**
* Get response data
*/
private function getResponseData(): array
{
$data = [];
// Get header information
$cabecera = $this->getElement('//tikR:Cabecera');
if ($cabecera) {
$data['header'] = [
'obligado_emision' => [
'nombre_razon' => $this->getElementText('.//tik:NombreRazon', $cabecera),
'nif' => $this->getElementText('.//tik:NIF', $cabecera)
]
];
}
// Get processing informationp
$data['processing'] = [
'tiempo_espera_envio' => $this->getElementText('//tikR:TiempoEsperaEnvio'),
'estado_envio' => $this->getElementText('//tikR:EstadoEnvio')
];
// Get invoice responses
$data['invoices'] = $this->getInvoiceResponses();
return $data;
}
/**
* Get metadata from response
*/
private function getMetadata(): array
{
return [
'request_id' => $this->getElementText('//tikR:RespuestaLinea/tikR:IDFactura/tik:IDEmisorFactura'),
'invoice_series' => $this->getElementText('//tikR:RespuestaLinea/tikR:IDFactura/tik:NumSerieFactura'),
'invoice_date' => $this->getElementText('//tikR:RespuestaLinea/tikR:IDFactura/tik:FechaExpedicionFactura'),
'operation_type' => $this->getElementText('//tikR:RespuestaLinea/tikR:Operacion/tik:TipoOperacion'),
'external_reference' => $this->getElementText('//tikR:RespuestaLinea/tikR:RefExterna')
];
}
/**
* Get invoice responses
*/
private function getInvoiceResponses(): array
{
$invoices = [];
$respuestaLineas = $this->dom->getElementsByTagNameNS(
'https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/RespuestaSuministro.xsd',
'RespuestaLinea'
);
foreach ($respuestaLineas as $linea) {
$invoices[] = [
'id_emisor' => $this->getElementText('.//tikR:IDFactura/tik:IDEmisorFactura', $linea),
'num_serie' => $this->getElementText('.//tikR:IDFactura/tik:NumSerieFactura', $linea),
'fecha_expedicion' => $this->getElementText('.//tikR:IDFactura/tik:FechaExpedicionFactura', $linea),
'tipo_operacion' => $this->getElementText('.//tikR:Operacion/tik:TipoOperacion', $linea),
'ref_externa' => $this->getElementText('.//tikR:RefExterna', $linea),
'estado_registro' => $this->getElementText('.//tikR:EstadoRegistro', $linea),
'codigo_error' => $this->getElementText('.//tikR:CodigoErrorRegistro', $linea),
'descripcion_error' => $this->getElementText('.//tikR:DescripcionErrorRegistro', $linea),
'subsanacion' => $this->getElementText('.//tikR:Subsanacion', $linea)
];
}
return $invoices;
}
/**
* Get invoice data from response line
*/
private function getInvoiceData(DOMElement $linea): array
{
return [
'id_emisor' => $this->getElementText('.//tikR:IDFactura/tik:IDEmisorFactura', $linea),
'num_serie' => $this->getElementText('.//tikR:IDFactura/tik:NumSerieFactura', $linea),
'fecha_expedicion' => $this->getElementText('.//tikR:IDFactura/tik:FechaExpedicionFactura', $linea),
'tipo_operacion' => $this->getElementText('.//tikR:Operacion/tik:TipoOperacion', $linea),
'ref_externa' => $this->getElementText('.//tikR:RefExterna', $linea)
];
}
/**
* Get element text by XPath
*/
private function getElementText(string $xpath, ?DOMElement $context = null): ?string
{
$xpathObj = new \DOMXPath($this->dom);
// Register namespaces
$xpathObj->registerNamespace('env', 'http://schemas.xmlsoap.org/soap/envelope/');
$xpathObj->registerNamespace('tikR', 'https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/RespuestaSuministro.xsd');
$xpathObj->registerNamespace('tik', 'https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd');
$nodeList = $context ? $xpathObj->query($xpath, $context) : $xpathObj->query($xpath);
if ($nodeList && $nodeList->length > 0) {
return trim($nodeList->item(0)->nodeValue);
}
return null;
}
/**
* Get element by XPath
*/
private function getElement(string $xpath): ?DOMElement
{
$xpathObj = new \DOMXPath($this->dom);
// Register namespaces
$xpathObj->registerNamespace('env', 'http://schemas.xmlsoap.org/soap/envelope/');
$xpathObj->registerNamespace('tikR', 'https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/RespuestaSuministro.xsd');
$xpathObj->registerNamespace('tik', 'https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd');
$nodeList = $xpathObj->query($xpath);
if ($nodeList && $nodeList->length > 0) {
return $nodeList->item(0);
}
return null;
}
/**
* Check if response has errors
*/
public function hasErrors(): bool
{
return !empty($this->getErrors());
}
/**
* Get first error message
*/
public function getFirstError(): ?string
{
$errors = $this->getErrors();
return $errors[0]['message'] ?? null;
}
/**
* Get error codes
*/
public function getErrorCodes(): array
{
$codes = [];
$errors = $this->getErrors();
foreach ($errors as $error) {
if (isset($error['code'])) {
$codes[] = $error['code'];
}
}
return $codes;
}
/**
* Check if specific error code exists
*/
public function hasErrorCode(string $code): bool
{
return in_array($code, $this->getErrorCodes());
}
/**
* Get summary of response
*/
public function getSummary(): array
{
return [
'success' => $this->isSuccessful(),
'status' => $this->getStatus(),
'error_count' => count($this->getErrors()),
'warning_count' => count($this->getWarnings()),
'invoice_count' => count($this->getInvoiceResponses()),
'first_error' => $this->getFirstError(),
'error_codes' => $this->getErrorCodes()
];
}
}

View File

@ -0,0 +1,331 @@
<?php
namespace App\Services\EDocument\Standards\Verifactu;
use DOMDocument;
use DOMElement;
use DOMNodeList;
use Exception;
use Illuminate\Support\Facades\Log;
class ResponseProcessor
{
private DOMDocument $dom;
private ?DOMElement $root = null;
public function __construct()
{
$this->dom = new DOMDocument();
}
/**
* Process AEAT XML response and return structured array
*/
public function processResponse(string $xmlResponse): array
{
try {
$this->loadXml($xmlResponse);
return [
'success' => $this->isSuccessful(),
'status' => $this->getStatus(),
'errors' => $this->getErrors(),
'warnings' => $this->getWarnings(),
'data' => $this->getResponseData(),
'metadata' => $this->getMetadata(),
'raw_response' => $xmlResponse
];
} catch (Exception $e) {
Log::error('Error processing AEAT response', [
'error' => $e->getMessage(),
'xml' => $xmlResponse
]);
return [
'success' => false,
'error' => 'Failed to process response: ' . $e->getMessage(),
'raw_response' => $xmlResponse
];
}
}
/**
* Load XML into DOM
*/
private function loadXml(string $xml): void
{
libxml_use_internal_errors(true);
libxml_clear_errors();
if (!$this->dom->loadXML($xml)) {
$errors = libxml_get_errors();
libxml_clear_errors();
throw new Exception('Invalid XML: ' . ($errors[0]->message ?? 'Unknown error'));
}
$this->root = $this->dom->documentElement;
}
/**
* Check if response indicates success
*/
private function isSuccessful(): bool
{
$estadoEnvio = $this->getElementText('//tikR:EstadoEnvio');
return $estadoEnvio === 'Correcto';
}
/**
* Get response status
*/
private function getStatus(): string
{
return $this->getElementText('//tikR:EstadoEnvio') ?? 'Unknown';
}
/**
* Get all errors from response
*/
private function getErrors(): array
{
$errors = [];
// Check for SOAP faults
$fault = $this->getElementText('//env:Fault/faultstring');
if ($fault) {
$errors[] = [
'type' => 'SOAP_Fault',
'code' => $this->getElementText('//env:Fault/faultcode'),
'message' => $fault,
'details' => $this->getElementText('//env:Fault/detail/callstack')
];
}
// Check for business logic errors
$respuestaLineas = $this->dom->getElementsByTagNameNS(
'https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/RespuestaSuministro.xsd',
'RespuestaLinea'
);
foreach ($respuestaLineas as $linea) {
$estadoRegistro = $this->getElementText('.//tikR:EstadoRegistro', $linea);
if ($estadoRegistro === 'Incorrecto') {
$errors[] = [
'type' => 'Business_Error',
'code' => $this->getElementText('.//tikR:CodigoErrorRegistro', $linea),
'message' => $this->getElementText('.//tikR:DescripcionErrorRegistro', $linea),
'invoice_data' => $this->getInvoiceData($linea)
];
}
}
return $errors;
}
/**
* Get warnings from response
*/
private function getWarnings(): array
{
$warnings = [];
// Check for subsanacion (correction) messages
$subsanacion = $this->getElementText('//tikR:RespuestaLinea/tikR:Subsanacion');
if ($subsanacion) {
$warnings[] = [
'type' => 'Subsanacion',
'message' => $subsanacion
];
}
return $warnings;
}
/**
* Get response data
*/
private function getResponseData(): array
{
$data = [];
// Get header information
$cabecera = $this->getElement('//tikR:Cabecera');
if ($cabecera) {
$data['header'] = [
'obligado_emision' => [
'nombre_razon' => $this->getElementText('.//tik:NombreRazon', $cabecera),
'nif' => $this->getElementText('.//tik:NIF', $cabecera)
]
];
}
// Get processing information
$data['processing'] = [
'tiempo_espera_envio' => $this->getElementText('//tikR:TiempoEsperaEnvio'),
'estado_envio' => $this->getElementText('//tikR:EstadoEnvio')
];
// Get invoice responses
$data['invoices'] = $this->getInvoiceResponses();
return $data;
}
/**
* Get metadata from response
*/
private function getMetadata(): array
{
return [
'request_id' => $this->getElementText('//tikR:RespuestaLinea/tikR:IDFactura/tik:IDEmisorFactura'),
'invoice_series' => $this->getElementText('//tikR:RespuestaLinea/tikR:IDFactura/tik:NumSerieFactura'),
'invoice_date' => $this->getElementText('//tikR:RespuestaLinea/tikR:IDFactura/tik:FechaExpedicionFactura'),
'operation_type' => $this->getElementText('//tikR:RespuestaLinea/tikR:Operacion/tik:TipoOperacion'),
'external_reference' => $this->getElementText('//tikR:RespuestaLinea/tikR:RefExterna')
];
}
/**
* Get invoice responses
*/
private function getInvoiceResponses(): array
{
$invoices = [];
$respuestaLineas = $this->dom->getElementsByTagNameNS(
'https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/RespuestaSuministro.xsd',
'RespuestaLinea'
);
foreach ($respuestaLineas as $linea) {
$invoices[] = [
'id_emisor' => $this->getElementText('.//tikR:IDFactura/tik:IDEmisorFactura', $linea),
'num_serie' => $this->getElementText('.//tikR:IDFactura/tik:NumSerieFactura', $linea),
'fecha_expedicion' => $this->getElementText('.//tikR:IDFactura/tik:FechaExpedicionFactura', $linea),
'tipo_operacion' => $this->getElementText('.//tikR:Operacion/tik:TipoOperacion', $linea),
'ref_externa' => $this->getElementText('.//tikR:RefExterna', $linea),
'estado_registro' => $this->getElementText('.//tikR:EstadoRegistro', $linea),
'codigo_error' => $this->getElementText('.//tikR:CodigoErrorRegistro', $linea),
'descripcion_error' => $this->getElementText('.//tikR:DescripcionErrorRegistro', $linea),
'subsanacion' => $this->getElementText('.//tikR:Subsanacion', $linea)
];
}
return $invoices;
}
/**
* Get invoice data from response line
*/
private function getInvoiceData(DOMElement $linea): array
{
return [
'id_emisor' => $this->getElementText('.//tikR:IDFactura/tik:IDEmisorFactura', $linea),
'num_serie' => $this->getElementText('.//tikR:IDFactura/tik:NumSerieFactura', $linea),
'fecha_expedicion' => $this->getElementText('.//tikR:IDFactura/tik:FechaExpedicionFactura', $linea),
'tipo_operacion' => $this->getElementText('.//tikR:Operacion/tik:TipoOperacion', $linea),
'ref_externa' => $this->getElementText('.//tikR:RefExterna', $linea)
];
}
/**
* Get element text by XPath
*/
private function getElementText(string $xpath, ?DOMElement $context = null): ?string
{
$xpathObj = new \DOMXPath($this->dom);
// Register namespaces
$xpathObj->registerNamespace('env', 'http://schemas.xmlsoap.org/soap/envelope/');
$xpathObj->registerNamespace('tikR', 'https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/RespuestaSuministro.xsd');
$xpathObj->registerNamespace('tik', 'https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd');
$nodeList = $context ? $xpathObj->query($xpath, $context) : $xpathObj->query($xpath);
if ($nodeList && $nodeList->length > 0) {
return trim($nodeList->item(0)->nodeValue);
}
return null;
}
/**
* Get element by XPath
*/
private function getElement(string $xpath): ?DOMElement
{
$xpathObj = new \DOMXPath($this->dom);
// Register namespaces
$xpathObj->registerNamespace('env', 'http://schemas.xmlsoap.org/soap/envelope/');
$xpathObj->registerNamespace('tikR', 'https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/RespuestaSuministro.xsd');
$xpathObj->registerNamespace('tik', 'https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd');
$nodeList = $xpathObj->query($xpath);
if ($nodeList && $nodeList->length > 0) {
return $nodeList->item(0);
}
return null;
}
/**
* Check if response has errors
*/
public function hasErrors(): bool
{
return !empty($this->getErrors());
}
/**
* Get first error message
*/
public function getFirstError(): ?string
{
$errors = $this->getErrors();
return $errors[0]['message'] ?? null;
}
/**
* Get error codes
*/
public function getErrorCodes(): array
{
$codes = [];
$errors = $this->getErrors();
foreach ($errors as $error) {
if (isset($error['code'])) {
$codes[] = $error['code'];
}
}
return $codes;
}
/**
* Check if specific error code exists
*/
public function hasErrorCode(string $code): bool
{
return in_array($code, $this->getErrorCodes());
}
/**
* Get summary of response
*/
public function getSummary(): array
{
return [
'success' => $this->isSuccessful(),
'status' => $this->getStatus(),
'error_count' => count($this->getErrors()),
'warning_count' => count($this->getWarnings()),
'invoice_count' => count($this->getInvoiceResponses()),
'first_error' => $this->getFirstError(),
'error_codes' => $this->getErrorCodes()
];
}
}

114
composer.lock generated
View File

@ -960,16 +960,16 @@
},
{
"name": "braintree/braintree_php",
"version": "6.27.0",
"version": "6.28.0",
"source": {
"type": "git",
"url": "https://github.com/braintree/braintree_php.git",
"reference": "2843f65ef19f5b2a0344b07a59a632df92857993"
"reference": "f8693ee85b232c6fe4599100cdaba2c07ca34b79"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/braintree/braintree_php/zipball/2843f65ef19f5b2a0344b07a59a632df92857993",
"reference": "2843f65ef19f5b2a0344b07a59a632df92857993",
"url": "https://api.github.com/repos/braintree/braintree_php/zipball/f8693ee85b232c6fe4599100cdaba2c07ca34b79",
"reference": "f8693ee85b232c6fe4599100cdaba2c07ca34b79",
"shasum": ""
},
"require": {
@ -1003,9 +1003,9 @@
"description": "Braintree PHP Client Library",
"support": {
"issues": "https://github.com/braintree/braintree_php/issues",
"source": "https://github.com/braintree/braintree_php/tree/6.27.0"
"source": "https://github.com/braintree/braintree_php/tree/6.28.0"
},
"time": "2025-06-26T19:49:44+00:00"
"time": "2025-08-05T18:53:15+00:00"
},
{
"name": "brick/math",
@ -1665,16 +1665,16 @@
},
{
"name": "doctrine/dbal",
"version": "4.3.1",
"version": "4.3.2",
"source": {
"type": "git",
"url": "https://github.com/doctrine/dbal.git",
"reference": "ac336c95ea9e13433d56ca81c308b39db0e1a2a7"
"reference": "7669f131d43b880de168b2d2df9687d152d6c762"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/dbal/zipball/ac336c95ea9e13433d56ca81c308b39db0e1a2a7",
"reference": "ac336c95ea9e13433d56ca81c308b39db0e1a2a7",
"url": "https://api.github.com/repos/doctrine/dbal/zipball/7669f131d43b880de168b2d2df9687d152d6c762",
"reference": "7669f131d43b880de168b2d2df9687d152d6c762",
"shasum": ""
},
"require": {
@ -1751,7 +1751,7 @@
],
"support": {
"issues": "https://github.com/doctrine/dbal/issues",
"source": "https://github.com/doctrine/dbal/tree/4.3.1"
"source": "https://github.com/doctrine/dbal/tree/4.3.2"
},
"funding": [
{
@ -1767,7 +1767,7 @@
"type": "tidelift"
}
],
"time": "2025-07-22T10:09:51+00:00"
"time": "2025-08-05T13:30:38+00:00"
},
{
"name": "doctrine/deprecations",
@ -2402,20 +2402,20 @@
},
{
"name": "elasticsearch/elasticsearch",
"version": "v8.18.0",
"version": "v8.19.0",
"source": {
"type": "git",
"url": "https://github.com/elastic/elasticsearch-php.git",
"reference": "df8ee73046c688ee9ce2d69cb5c54a03ca38cc5c"
"reference": "1771284cb43a7b653634d418b6f5f0ec84ff8a6d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/elastic/elasticsearch-php/zipball/df8ee73046c688ee9ce2d69cb5c54a03ca38cc5c",
"reference": "df8ee73046c688ee9ce2d69cb5c54a03ca38cc5c",
"url": "https://api.github.com/repos/elastic/elasticsearch-php/zipball/1771284cb43a7b653634d418b6f5f0ec84ff8a6d",
"reference": "1771284cb43a7b653634d418b6f5f0ec84ff8a6d",
"shasum": ""
},
"require": {
"elastic/transport": "^8.10",
"elastic/transport": "^8.11",
"guzzlehttp/guzzle": "^7.0",
"php": "^7.4 || ^8.0",
"psr/http-client": "^1.0",
@ -2453,9 +2453,9 @@
],
"support": {
"issues": "https://github.com/elastic/elasticsearch-php/issues",
"source": "https://github.com/elastic/elasticsearch-php/tree/v8.18.0"
"source": "https://github.com/elastic/elasticsearch-php/tree/v8.19.0"
},
"time": "2025-05-02T10:38:56+00:00"
"time": "2025-08-06T16:58:06+00:00"
},
{
"name": "endroid/qr-code",
@ -3034,7 +3034,7 @@
},
{
"name": "google/apiclient-services",
"version": "v0.405.0",
"version": "v0.406.0",
"source": {
"type": "git",
"url": "https://github.com/googleapis/google-api-php-client-services.git",
@ -3072,7 +3072,7 @@
],
"support": {
"issues": "https://github.com/googleapis/google-api-php-client-services/issues",
"source": "https://github.com/googleapis/google-api-php-client-services/tree/v0.405.0"
"source": "https://github.com/googleapis/google-api-php-client-services/tree/v0.406.0"
},
"time": "2025-06-04T17:28:44+00:00"
},
@ -8150,29 +8150,29 @@
},
{
"name": "nette/utils",
"version": "v4.0.7",
"version": "v4.0.8",
"source": {
"type": "git",
"url": "https://github.com/nette/utils.git",
"reference": "e67c4061eb40b9c113b218214e42cb5a0dda28f2"
"reference": "c930ca4e3cf4f17dcfb03037703679d2396d2ede"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nette/utils/zipball/e67c4061eb40b9c113b218214e42cb5a0dda28f2",
"reference": "e67c4061eb40b9c113b218214e42cb5a0dda28f2",
"url": "https://api.github.com/repos/nette/utils/zipball/c930ca4e3cf4f17dcfb03037703679d2396d2ede",
"reference": "c930ca4e3cf4f17dcfb03037703679d2396d2ede",
"shasum": ""
},
"require": {
"php": "8.0 - 8.4"
"php": "8.0 - 8.5"
},
"conflict": {
"nette/finder": "<3",
"nette/schema": "<1.2.2"
},
"require-dev": {
"jetbrains/phpstorm-attributes": "dev-master",
"jetbrains/phpstorm-attributes": "^1.2",
"nette/tester": "^2.5",
"phpstan/phpstan": "^1.0",
"phpstan/phpstan-nette": "^2.0@stable",
"tracy/tracy": "^2.9"
},
"suggest": {
@ -8190,6 +8190,9 @@
}
},
"autoload": {
"psr-4": {
"Nette\\": "src"
},
"classmap": [
"src/"
]
@ -8230,9 +8233,9 @@
],
"support": {
"issues": "https://github.com/nette/utils/issues",
"source": "https://github.com/nette/utils/tree/v4.0.7"
"source": "https://github.com/nette/utils/tree/v4.0.8"
},
"time": "2025-06-03T04:55:08+00:00"
"time": "2025-08-06T21:43:34+00:00"
},
{
"name": "nikic/php-parser",
@ -8673,16 +8676,16 @@
},
{
"name": "open-telemetry/context",
"version": "1.2.1",
"version": "1.3.0",
"source": {
"type": "git",
"url": "https://github.com/opentelemetry-php/context.git",
"reference": "1eb2b837ee9362db064a6b65d5ecce15a9f9f020"
"reference": "4d5d98f1d4311a55b8d07e3d4c06d2430b4e6efc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/opentelemetry-php/context/zipball/1eb2b837ee9362db064a6b65d5ecce15a9f9f020",
"reference": "1eb2b837ee9362db064a6b65d5ecce15a9f9f020",
"url": "https://api.github.com/repos/opentelemetry-php/context/zipball/4d5d98f1d4311a55b8d07e3d4c06d2430b4e6efc",
"reference": "4d5d98f1d4311a55b8d07e3d4c06d2430b4e6efc",
"shasum": ""
},
"require": {
@ -8728,7 +8731,7 @@
"issues": "https://github.com/open-telemetry/opentelemetry-php/issues",
"source": "https://github.com/open-telemetry/opentelemetry-php"
},
"time": "2025-05-07T23:36:50+00:00"
"time": "2025-08-04T03:25:06+00:00"
},
{
"name": "paragonie/constant_time_encoding",
@ -10465,16 +10468,16 @@
},
{
"name": "psy/psysh",
"version": "v0.12.9",
"version": "v0.12.10",
"source": {
"type": "git",
"url": "https://github.com/bobthecow/psysh.git",
"reference": "1b801844becfe648985372cb4b12ad6840245ace"
"reference": "6e80abe6f2257121f1eb9a4c55bf29d921025b22"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/bobthecow/psysh/zipball/1b801844becfe648985372cb4b12ad6840245ace",
"reference": "1b801844becfe648985372cb4b12ad6840245ace",
"url": "https://api.github.com/repos/bobthecow/psysh/zipball/6e80abe6f2257121f1eb9a4c55bf29d921025b22",
"reference": "6e80abe6f2257121f1eb9a4c55bf29d921025b22",
"shasum": ""
},
"require": {
@ -10524,12 +10527,11 @@
"authors": [
{
"name": "Justin Hileman",
"email": "justin@justinhileman.info",
"homepage": "http://justinhileman.com"
"email": "justin@justinhileman.info"
}
],
"description": "An interactive shell for modern PHP.",
"homepage": "http://psysh.org",
"homepage": "https://psysh.org",
"keywords": [
"REPL",
"console",
@ -10538,9 +10540,9 @@
],
"support": {
"issues": "https://github.com/bobthecow/psysh/issues",
"source": "https://github.com/bobthecow/psysh/tree/v0.12.9"
"source": "https://github.com/bobthecow/psysh/tree/v0.12.10"
},
"time": "2025-06-23T02:35:06+00:00"
"time": "2025-08-04T12:39:37+00:00"
},
{
"name": "pusher/pusher-php-server",
@ -10863,16 +10865,16 @@
},
{
"name": "razorpay/razorpay",
"version": "2.9.1",
"version": "2.9.2",
"source": {
"type": "git",
"url": "https://github.com/razorpay/razorpay-php.git",
"reference": "d5e49ef12c4862f6bc8003f87f79b942a9dd3a8b"
"reference": "c5cf59941eb2d888e80371328d932e6e8266d352"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/razorpay/razorpay-php/zipball/d5e49ef12c4862f6bc8003f87f79b942a9dd3a8b",
"reference": "d5e49ef12c4862f6bc8003f87f79b942a9dd3a8b",
"url": "https://api.github.com/repos/razorpay/razorpay-php/zipball/c5cf59941eb2d888e80371328d932e6e8266d352",
"reference": "c5cf59941eb2d888e80371328d932e6e8266d352",
"shasum": ""
},
"require": {
@ -10924,7 +10926,7 @@
"issues": "https://github.com/Razorpay/razorpay-php/issues",
"source": "https://github.com/Razorpay/razorpay-php"
},
"time": "2025-03-20T12:51:47+00:00"
"time": "2025-08-05T07:13:20+00:00"
},
{
"name": "rmccue/requests",
@ -11476,16 +11478,16 @@
},
{
"name": "setasign/fpdi",
"version": "v2.6.3",
"version": "v2.6.4",
"source": {
"type": "git",
"url": "https://github.com/Setasign/FPDI.git",
"reference": "67c31f5e50c93c20579ca9e23035d8c540b51941"
"reference": "4b53852fde2734ec6a07e458a085db627c60eada"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Setasign/FPDI/zipball/67c31f5e50c93c20579ca9e23035d8c540b51941",
"reference": "67c31f5e50c93c20579ca9e23035d8c540b51941",
"url": "https://api.github.com/repos/Setasign/FPDI/zipball/4b53852fde2734ec6a07e458a085db627c60eada",
"reference": "4b53852fde2734ec6a07e458a085db627c60eada",
"shasum": ""
},
"require": {
@ -11500,7 +11502,7 @@
"setasign/fpdf": "~1.8.6",
"setasign/tfpdf": "~1.33",
"squizlabs/php_codesniffer": "^3.5",
"tecnickcom/tcpdf": "^6.2"
"tecnickcom/tcpdf": "^6.8"
},
"suggest": {
"setasign/fpdf": "FPDI will extend this class but as it is also possible to use TCPDF or tFPDF as an alternative. There's no fixed dependency configured."
@ -11536,7 +11538,7 @@
],
"support": {
"issues": "https://github.com/Setasign/FPDI/issues",
"source": "https://github.com/Setasign/FPDI/tree/v2.6.3"
"source": "https://github.com/Setasign/FPDI/tree/v2.6.4"
},
"funding": [
{
@ -11544,7 +11546,7 @@
"type": "tidelift"
}
],
"time": "2025-02-05T13:22:35+00:00"
"time": "2025-08-05T09:57:14+00:00"
},
{
"name": "smalot/pdfparser",

View File

@ -9,6 +9,7 @@ use App\Services\EDocument\Standards\Verifactu\Models\Invoice;
use App\Services\EDocument\Standards\Verifactu\Models\Desglose;
use App\Services\EDocument\Standards\Verifactu\Models\Encadenamiento;
use App\Services\EDocument\Standards\Verifactu\Models\SistemaInformatico;
use App\Services\EDocument\Standards\Verifactu\Response\ResponseProcessor;
use App\Services\EDocument\Standards\Verifactu\Models\PersonaFisicaJuridica;
@ -19,12 +20,17 @@ class WSTest extends TestCase
{
// Generate current timestamp in the correct format
$currentTimestamp = date('Y-m-d\TH:i:s');
$currentTimestamp = now()->setTimezone('Europe/Madrid')->format('Y-m-d\TH:i:s');
// $currentTimestamp = \Carbon\Carbon::parse('2023-01-01')->format('Y-m-d\TH:i:s');
// $currentTimestamp = now()->subDays(5)->format('Y-m-d\TH:i:s');
nlog($currentTimestamp);
$invoice = new Invoice();
$invoice
->setIdVersion('1.0')
->setIdFactura('FAC-2023-001')
->setIdFactura('FAC2023002')
->setFechaExpedicionFactura('2023-01-02')
->setRefExterna('REF-123')
->setNombreRazonEmisor('Empresa Ejemplo SL')
->setTipoFactura('F1')
@ -49,7 +55,7 @@ $currentTimestamp = date('Y-m-d\TH:i:s');
// Add emitter
$emisor = new PersonaFisicaJuridica();
$emisor
->setNif('B12345678')
->setNif('A39200019')
->setRazonSocial('Empresa Ejemplo SL');
$invoice->setTercero($emisor);
@ -69,7 +75,7 @@ $currentTimestamp = date('Y-m-d\TH:i:s');
$sistema = new SistemaInformatico();
$sistema
->setNombreRazon('Sistema de Facturación')
->setNif('99999910G')
->setNif('A39200019')
->setNombreSistemaInformatico('SistemaFacturacion')
->setIdSistemaInformatico('01')
->setVersion('1.0')
@ -101,11 +107,12 @@ $correctHash = $this->calculateVerifactuHash(
// Replace the placeholder with the correct hash
$soapXml = str_replace('PLACEHOLDER_HUELLA', $correctHash, $soapXml);
\Log::info('Calculated hash for XML: ' . $correctHash);
nlog("test_generated_invoice_xml_can_send_to_web_service");
nlog('Calculated hash for XML: ' . $correctHash);
// Sign the XML before sending
$certPath = storage_path('aeat-cert2.pem');
$keyPath = storage_path('aeat-key2.pem');
$certPath = storage_path('aeat-cert5.pem');
$keyPath = storage_path('aeat-key5.pem');
$signingService = new \App\Services\EDocument\Standards\Verifactu\Signing\SigningService($soapXml, file_get_contents($keyPath), file_get_contents($certPath));
$soapXml = $signingService->sign();
@ -115,20 +122,20 @@ $response = Http::withHeaders([
'SOAPAction' => '',
])
->withOptions([
'cert' => storage_path('aeat-cert2.pem'),
'ssl_key' => storage_path('aeat-key2.pem'),
'cert' => storage_path('aeat-cert5.pem'),
'ssl_key' => storage_path('aeat-key5.pem'),
'verify' => false,
'timeout' => 30,
])
->withBody($soapXml, 'text/xml')
->post('https://prewww1.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP');
\Log::info('Request with AEAT official test data:');
\Log::info($soapXml);
\Log::info('Response with AEAT official test data:');
\Log::info('Response Status: ' . $response->status());
\Log::info('Response Headers: ' . json_encode($response->headers()));
\Log::info('Response Body: ' . $response->body());
nlog('Request with AEAT official test data:');
nlog($soapXml);
nlog('Response with AEAT official test data:');
nlog('Response Status: ' . $response->status());
nlog('Response Headers: ' . json_encode($response->headers()));
nlog('Response Body: ' . $response->body());
if (!$response->successful()) {
\Log::error('Request failed with status: ' . $response->status());
@ -146,8 +153,14 @@ $this->assertTrue($response->successful());
public function test_send_aeat_example_to_verifactu()
{
// Generate current timestamp in the correct format
$currentTimestamp = date('Y-m-d\TH:i:sP');
// $currentTimestamp = date('Y-m-d\TH:i:sP');
$currentTimestamp = now()->setTimezone('Europe/Madrid')->format('Y-m-d\TH:i:sP');
$invoice_number = 'TEST0033343437';
$invoice_date = '02-07-2025';
$calc_hash = 'A0B4D14E6F7769860C8A4EAFFA3EEBF52B7044685BD69D1DB5BBD68EA0E2BA21';
$nif = '99999910G';
$soapXml = <<<XML
<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
@ -169,8 +182,8 @@ $this->assertTrue($response->successful());
<!-- IDFactura: The actual invoice issuer (using same test NIF) -->
<sum1:IDFactura>
<sum1:IDEmisorFactura>99999910G</sum1:IDEmisorFactura>
<sum1:NumSerieFactura>TEST-003</sum1:NumSerieFactura>
<sum1:FechaExpedicionFactura>24-06-2025</sum1:FechaExpedicionFactura>
<sum1:NumSerieFactura>{$invoice_number}</sum1:NumSerieFactura>
<sum1:FechaExpedicionFactura>{$invoice_date}</sum1:FechaExpedicionFactura>
</sum1:IDFactura>
<!-- NombreRazonEmisor: The actual business that issued the invoice -->
<sum1:NombreRazonEmisor>CERTIFICADO FISICA PRUEBAS</sum1:NombreRazonEmisor>
@ -179,7 +192,7 @@ $this->assertTrue($response->successful());
<sum1:Destinatarios>
<sum1:IDDestinatario>
<sum1:NombreRazon>Test Recipient Company</sum1:NombreRazon>
<sum1:NIF>B12345674</sum1:NIF>
<sum1:NIF>A39200019</sum1:NIF>
</sum1:IDDestinatario>
</sum1:Destinatarios>
<sum1:Desglose>
@ -195,12 +208,17 @@ $this->assertTrue($response->successful());
<sum1:ImporteTotal>121.00</sum1:ImporteTotal>
<!-- Encadenamiento: Required chaining information -->
<sum1:Encadenamiento>
<sum1:PrimerRegistro>S</sum1:PrimerRegistro>
<sum1:RegistroAnterior>
<sum1:IDEmisorFactura>99999910G</sum1:IDEmisorFactura>
<sum1:NumSerieFactura>TEST0033343437</sum1:NumSerieFactura>
<sum1:FechaExpedicionFactura>02-07-2025</sum1:FechaExpedicionFactura>
<sum1:Huella>A0B4D14E6F7769860C8A4EAFFA3EEBF52B7044685BD69D1DB5BBD68EA0E2BA21</sum1:Huella>
</sum1:RegistroAnterior>
</sum1:Encadenamiento>
<!-- SistemaInformatico: The computer system details (same as ObligadoEmision) -->
<sum1:SistemaInformatico>
<sum1:NombreRazon>CERTIFICADO FISICA PRUEBAS</sum1:NombreRazon>
<sum1:NIF>99999910G</sum1:NIF>
<sum1:NombreRazon>Sistema de Facturación</sum1:NombreRazon>
<sum1:NIF>A39200019</sum1:NIF>
<sum1:NombreSistemaInformatico>InvoiceNinja</sum1:NombreSistemaInformatico>
<sum1:IdSistemaInformatico>77</sum1:IdSistemaInformatico>
<sum1:Version>1.0.03</sum1:Version>
@ -221,9 +239,9 @@ XML;
// Calculate the correct hash using AEAT's specified format
$correctHash = $this->calculateVerifactuHash(
'99999910G', // IDEmisorFactura
'TEST-003', // NumSerieFactura
'24-06-2025', // FechaExpedicionFactura
$nif, // IDEmisorFactura
$invoice_number, // NumSerieFactura
$invoice_date, // FechaExpedicionFactura
'F1', // TipoFactura
'21.00', // CuotaTotal
'121.00', // ImporteTotal
@ -234,11 +252,11 @@ XML;
// Replace the placeholder with the correct hash
$soapXml = str_replace('PLACEHOLDER_HUELLA', $correctHash, $soapXml);
\Log::info('Calculated hash for XML: ' . $correctHash);
nlog('Calculated hash for XML: ' . $correctHash);
// Sign the XML before sending
$certPath = storage_path('aeat-cert2.pem');
$keyPath = storage_path('aeat-key2.pem');
$certPath = storage_path('aeat-cert5.pem');
$keyPath = storage_path('aeat-key5.pem');
$signingService = new \App\Services\EDocument\Standards\Verifactu\Signing\SigningService($soapXml, file_get_contents($keyPath), file_get_contents($certPath));
$soapXml = $signingService->sign();
@ -248,20 +266,20 @@ XML;
'SOAPAction' => '',
])
->withOptions([
'cert' => storage_path('aeat-cert2.pem'),
'ssl_key' => storage_path('aeat-key2.pem'),
'cert' => storage_path('aeat-cert5.pem'),
'ssl_key' => storage_path('aeat-key5.pem'),
'verify' => false,
'timeout' => 30,
])
->withBody($soapXml, 'text/xml')
->post('https://prewww1.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP');
\Log::info('Request with AEAT official test data:');
\Log::info($soapXml);
\Log::info('Response with AEAT official test data:');
\Log::info('Response Status: ' . $response->status());
\Log::info('Response Headers: ' . json_encode($response->headers()));
\Log::info('Response Body: ' . $response->body());
nlog('Request with AEAT official test data:');
nlog($soapXml);
nlog('Response with AEAT official test data:');
nlog('Response Status: ' . $response->status());
nlog('Response Headers: ' . json_encode($response->headers()));
nlog('Response Body: ' . $response->body());
if (!$response->successful()) {
\Log::error('Request failed with status: ' . $response->status());
@ -269,8 +287,174 @@ XML;
}
$this->assertTrue($response->successful());
$responseProcessor = new ResponseProcessor();
$responseProcessor->processResponse($response->body());
nlog($responseProcessor->getSummary());
$this->assertTrue($responseProcessor->getSummary()['success']);
}
public function test_cancel_and_modify_existing_invoice()
{
$currentTimestamp = now()->setTimezone('Europe/Madrid')->format('Y-m-d\TH:i:sP');
$invoice_number = 'TEST0033343436';
$invoice_date = '02-07-2025';
$calc_hash = 'A0B4D14E6F7769860C8A4EAFFA3EEBF52B7044685BD69D1DB5BBD68EA0E2BA21';
$nif = '99999910G';
$soapXml = <<<XML
<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:sum="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroLR.xsd" xmlns:sum1="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd">
<soapenv:Header>
<tik:ObligadoEmision xmlns:tik="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroLR.xsd">
<tik:NIF>A39200019</tik:NIF>
<tik:NombreRazon>Sistema de Facturación</tik:NombreRazon>
</tik:ObligadoEmision>
</soapenv:Header>
<soapenv:Body>
<sum:ModificacionFactura>
<sum1:RegistroAnulacion>
<sum1:IDFactura>
<sum1:IDEmisorFactura>99999910G</sum1:IDEmisorFactura>
<sum1:NumSerieFactura>TEST0033343436</sum1:NumSerieFactura>
<sum1:FechaExpedicionFactura>02-07-2025</sum1:FechaExpedicionFactura>
</sum1:IDFactura>
<sum1:MotivoAnulacion>1</sum1:MotivoAnulacion> <!-- 1 = Sustitución por otra factura -->
</sum1:RegistroAnulacion>
<sum1:RegistroModificacion>
<sum1:IDVersion>1.0</sum1:IDVersion>
<!-- IDFactura: The actual invoice issuer (using same test NIF) -->
<sum1:IDFactura>
<sum1:IDEmisorFactura>99999910G</sum1:IDEmisorFactura>
<sum1:NumSerieFactura>{$invoice_number}</sum1:NumSerieFactura>
<sum1:FechaExpedicionFactura>{$invoice_date}</sum1:FechaExpedicionFactura>
</sum1:IDFactura>
<!-- NombreRazonEmisor: The actual business that issued the invoice -->
<sum1:NombreRazonEmisor>CERTIFICADO FISICA PRUEBAS</sum1:NombreRazonEmisor>
<sum1:TipoFactura>F1</sum1:TipoFactura>
<sum1:DescripcionOperacion>Test invoice submitted by computer system on behalf of business</sum1:DescripcionOperacion>
<sum1:Destinatarios>
<sum1:IDDestinatario>
<sum1:NombreRazon>Test Recipient Company</sum1:NombreRazon>
<sum1:NIF>A39200019</sum1:NIF>
</sum1:IDDestinatario>
</sum1:Destinatarios>
<sum1:Desglose>
<sum1:DetalleDesglose>
<sum1:ClaveRegimen>01</sum1:ClaveRegimen>
<sum1:CalificacionOperacion>S1</sum1:CalificacionOperacion>
<sum1:TipoImpositivo>21</sum1:TipoImpositivo>
<sum1:BaseImponibleOimporteNoSujeto>100.00</sum1:BaseImponibleOimporteNoSujeto>
<sum1:CuotaRepercutida>21.00</sum1:CuotaRepercutida>
</sum1:DetalleDesglose>
</sum1:Desglose>
<sum1:CuotaTotal>21.00</sum1:CuotaTotal>
<sum1:ImporteTotal>121.00</sum1:ImporteTotal>
<!-- Encadenamiento: Required chaining information -->
<sum1:Encadenamiento>
<sum1:PrimerRegistro>N</sum1:PrimerRegistro>
</sum1:Encadenamiento>
<!-- SistemaInformatico: The computer system details (same as ObligadoEmision) -->
<sum1:SistemaInformatico>
<sum1:NombreRazon>Sistema de Facturación</sum1:NombreRazon>
<sum1:NIF>A39200019</sum1:NIF>
<sum1:NombreSistemaInformatico>InvoiceNinja</sum1:NombreSistemaInformatico>
<sum1:IdSistemaInformatico>77</sum1:IdSistemaInformatico>
<sum1:Version>1.0.03</sum1:Version>
<sum1:NumeroInstalacion>383</sum1:NumeroInstalacion>
<sum1:TipoUsoPosibleSoloVerifactu>N</sum1:TipoUsoPosibleSoloVerifactu>
<sum1:TipoUsoPosibleMultiOT>S</sum1:TipoUsoPosibleMultiOT>
<sum1:IndicadorMultiplesOT>S</sum1:IndicadorMultiplesOT>
</sum1:SistemaInformatico>
<sum1:FechaHoraHusoGenRegistro>{$currentTimestamp}</sum1:FechaHoraHusoGenRegistro>
<sum1:TipoHuella>01</sum1:TipoHuella>
<sum1:Huella>PLACEHOLDER_HUELLA</sum1:Huella>
</sum1:RegistroModificacion>
</sum:ModificacionFactura>
</soapenv:Body>
</soapenv:Envelope>
XML;
// Calculate the correct hash using AEAT's specified format
$correctHash = $this->calculateVerifactuHash(
$nif, // IDEmisorFactura
$invoice_number, // NumSerieFactura
$invoice_date, // FechaExpedicionFactura
'F1', // TipoFactura
'21.00', // CuotaTotal
'121.00', // ImporteTotal
'', // Huella (empty for first calculation)
$currentTimestamp // FechaHoraHusoGenRegistro (current time)
);
// Replace the placeholder with the correct hash
$soapXml = str_replace('PLACEHOLDER_HUELLA', $correctHash, $soapXml);
nlog('Calculated hash for XML: ' . $correctHash);
// Sign the XML before sending
$certPath = storage_path('aeat-cert5.pem');
$keyPath = storage_path('aeat-key5.pem');
$signingService = new \App\Services\EDocument\Standards\Verifactu\Signing\SigningService($soapXml, file_get_contents($keyPath), file_get_contents($certPath));
$soapXml = $signingService->sign();
// Try direct HTTP approach instead of SOAP client
$response = Http::withHeaders([
'Content-Type' => 'text/xml; charset=utf-8',
'SOAPAction' => '',
])
->withOptions([
'cert' => storage_path('aeat-cert5.pem'),
'ssl_key' => storage_path('aeat-key5.pem'),
'verify' => false,
'timeout' => 30,
])
->withBody($soapXml, 'text/xml')
->post('https://prewww1.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP');
nlog('Request with AEAT official test data:');
nlog($soapXml);
nlog('Response with AEAT official test data:');
nlog('Response Status: ' . $response->status());
nlog('Response Headers: ' . json_encode($response->headers()));
nlog('Response Body: ' . $response->body());
if (!$response->successful()) {
\Log::error('Request failed with status: ' . $response->status());
\Log::error('Response body: ' . $response->body());
}
$this->assertTrue($response->successful());
$responseProcessor = new ResponseProcessor();
$responseProcessor->processResponse($response->body());
nlog($responseProcessor->getSummary());
$this->assertTrue($responseProcessor->getSummary()['success']);
}
/**
* Calculate Verifactu hash using AEAT's specified format
* Based on AEAT response showing the exact format they use
@ -295,10 +479,13 @@ XML;
"Huella={$huella}&" .
"FechaHoraHusoGenRegistro={$fechaHoraHusoGenRegistro}";
\Log::info('Hash input string: ' . $hashInput);
nlog('Hash input string: ' . $hashInput);
// Calculate SHA256 hash and return in uppercase
return strtoupper(hash('sha256', $hashInput));
}
}