Add logging to wstest for requirements
This commit is contained in:
parent
bf5359cb72
commit
4127eb32f9
|
|
@ -428,6 +428,11 @@ class Company extends BaseModel
|
||||||
return $this->hasMany(Scheduler::class);
|
return $this->hasMany(Scheduler::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function verifactu_logs(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(VerifactuLog::class);
|
||||||
|
}
|
||||||
|
|
||||||
public function task_schedulers(): HasMany
|
public function task_schedulers(): HasMany
|
||||||
{
|
{
|
||||||
return $this->hasMany(Scheduler::class);
|
return $this->hasMany(Scheduler::class);
|
||||||
|
|
|
||||||
|
|
@ -400,6 +400,11 @@ class Invoice extends BaseModel
|
||||||
return $this->hasMany(Credit::class);
|
return $this->hasMany(Credit::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function verifactu_logs(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(VerifactuLog::class);
|
||||||
|
}
|
||||||
|
|
||||||
public function tasks(): \Illuminate\Database\Eloquent\Relations\HasMany
|
public function tasks(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||||
{
|
{
|
||||||
return $this->hasMany(Task::class);
|
return $this->hasMany(Task::class);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Invoice Ninja (https://invoiceninja.com).
|
||||||
|
*
|
||||||
|
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2025. Invoice Ninja LLC (https://invoiceninja.com)
|
||||||
|
*
|
||||||
|
* @license https://www.elastic.co/licensing/elastic-license
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Services\EDocument\Standards\Verifactu;
|
||||||
|
|
||||||
|
use App\Services\EDocument\Standards\Verifactu\ResponseProcessor;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
|
||||||
|
class AeatAuthority
|
||||||
|
{
|
||||||
|
|
||||||
|
// @todo - in the UI, the user must navigate to AEAT link, and add Invoice Ninja as a third party. We cannot send without this.
|
||||||
|
// @todo - need to store the verification of this in the company
|
||||||
|
// https://sede.agenciatributaria.gob.es/Sede/ayuda/consultas-informaticas/otros-servicios-ayuda-tecnica/consultar-confirmar-renunciar-apoderamiento-recibido.html
|
||||||
|
// @todo - register with AEAT as a third party - power of attorney
|
||||||
|
// Log in with their certificate, DNIe, or Cl@ve PIN.
|
||||||
|
// Select: "Otorgar poder a un tercero"
|
||||||
|
// Enter:
|
||||||
|
// Your SaaS company's NIF as the authorized party
|
||||||
|
// Power code: LGTINVDI (or GENERALDATPE)
|
||||||
|
// Confirm
|
||||||
|
// https://sede.agenciatributaria.gob.es/wlpl/BDC/conapoderWS
|
||||||
|
private string $base_url = 'https://sede.agenciatributaria.gob.es/wlpl/BDC/conapoderWS';
|
||||||
|
|
||||||
|
private string $sandbox_url = 'https://prewww1.aeat.es/wlpl/BDC/conapoderWS';
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setTestMode(): self
|
||||||
|
{
|
||||||
|
$this->base_url = $this->sandbox_url;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function run(string $client_nif): bool
|
||||||
|
{
|
||||||
|
|
||||||
|
$sender_nif = config('services.verifactu.sender_nif');
|
||||||
|
$certificate = config('services.verifactu.certificate');
|
||||||
|
$ssl_key = config('services.verifactu.ssl_key');
|
||||||
|
|
||||||
|
$xml = <<<XML
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
|
||||||
|
xmlns:apod="http://www2.agenciatributaria.gob.es/apoderamiento/ws/apoderamientos">
|
||||||
|
<soapenv:Header/>
|
||||||
|
<soapenv:Body>
|
||||||
|
<apod:ConsultaApoderamiento>
|
||||||
|
<apod:identificadorApoderado>
|
||||||
|
<apod:nifRepresentante>{$sender_nif}</apod:nifRepresentante>
|
||||||
|
</apod:identificadorApoderado>
|
||||||
|
<apod:identificadorPoderdante>
|
||||||
|
<apod:nifPoderdante>{$client_nif}</apod:nifPoderdante>
|
||||||
|
</apod:identificadorPoderdante>
|
||||||
|
<apod:codigoPoder>LGTINVDI</apod:codigoPoder>
|
||||||
|
</apod:ConsultaApoderamiento>
|
||||||
|
</soapenv:Body>
|
||||||
|
</soapenv:Envelope>
|
||||||
|
XML;
|
||||||
|
|
||||||
|
$signingService = new \App\Services\EDocument\Standards\Verifactu\Signing\SigningService($xml, file_get_contents($ssl_key), file_get_contents($certificate));
|
||||||
|
$soapXml = $signingService->sign();
|
||||||
|
|
||||||
|
$response = Http::withHeaders([
|
||||||
|
'Content-Type' => 'text/xml; charset=utf-8',
|
||||||
|
'SOAPAction' => '',
|
||||||
|
])
|
||||||
|
->withOptions([
|
||||||
|
'cert' => $certificate,
|
||||||
|
'ssl_key' => $ssl_key,
|
||||||
|
'verify' => false,
|
||||||
|
'timeout' => 30,
|
||||||
|
])
|
||||||
|
->withBody($soapXml, 'text/xml')
|
||||||
|
->post($this->base_url);
|
||||||
|
|
||||||
|
$success = $response->successful();
|
||||||
|
|
||||||
|
$responseProcessor = new ResponseProcessor();
|
||||||
|
|
||||||
|
$parsedResponse = $responseProcessor->processResponse($response->body());
|
||||||
|
nlog($response->body());
|
||||||
|
nlog($parsedResponse);
|
||||||
|
|
||||||
|
return $parsedResponse['success'];
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Invoice Ninja (https://invoiceninja.com).
|
||||||
|
*
|
||||||
|
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2025. Invoice Ninja LLC (https://invoiceninja.com)
|
||||||
|
*
|
||||||
|
* @license https://www.elastic.co/licensing/elastic-license
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Services\EDocument\Standards\Verifactu;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use App\Services\EDocument\Standards\Verifactu\ResponseProcessor;
|
||||||
|
|
||||||
|
class AeatClient
|
||||||
|
{
|
||||||
|
private string $base_url;
|
||||||
|
|
||||||
|
private string $sandbox_url = 'https://prewww1.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP';
|
||||||
|
|
||||||
|
public function __construct(private ?string $certificate = null, private ?string $ssl_key = null)
|
||||||
|
{
|
||||||
|
$this->init();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* initialize the certificates
|
||||||
|
*
|
||||||
|
* @return self
|
||||||
|
*/
|
||||||
|
private function init(): self
|
||||||
|
{
|
||||||
|
$this->certificate = $this->certificate ?? file_get_contents(config('services.verifactu.certificate'));
|
||||||
|
$this->ssl_key = $this->ssl_key ?? file_get_contents(config('services.verifactu.ssl_key'));
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* setTestMode
|
||||||
|
*
|
||||||
|
* @return self
|
||||||
|
*/
|
||||||
|
public function setTestMode(): self
|
||||||
|
{
|
||||||
|
$this->base_url = $this->sandbox_url;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* run
|
||||||
|
*
|
||||||
|
* @param mixed $entity
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function run($entity): void
|
||||||
|
{
|
||||||
|
// build the payload
|
||||||
|
|
||||||
|
// harvest any previous hashes
|
||||||
|
|
||||||
|
// send the payload to the AEAT
|
||||||
|
|
||||||
|
// await the response and insert new row into the verifactu_logs table
|
||||||
|
|
||||||
|
// write an activity (success or failure)
|
||||||
|
|
||||||
|
// on success, add a reference to invoice->backup->guid
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private function buildPayload($entity): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function harvestPreviousHashes($entity): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function send($xml)
|
||||||
|
{
|
||||||
|
|
||||||
|
$response = Http::withHeaders([
|
||||||
|
'Content-Type' => 'text/xml; charset=utf-8',
|
||||||
|
'SOAPAction' => '',
|
||||||
|
])
|
||||||
|
->withOptions([
|
||||||
|
'cert' => $this->certificate,
|
||||||
|
'ssl_key' => $this->ssl_key,
|
||||||
|
'verify' => false,
|
||||||
|
'timeout' => 30,
|
||||||
|
])
|
||||||
|
->withBody($xml, 'text/xml')
|
||||||
|
->post($this->base_url);
|
||||||
|
|
||||||
|
$success = $response->successful();
|
||||||
|
|
||||||
|
$responseProcessor = new ResponseProcessor();
|
||||||
|
|
||||||
|
$parsedResponse = $responseProcessor->processResponse($response->body());
|
||||||
|
|
||||||
|
nlog($parsedResponse);
|
||||||
|
|
||||||
|
if($parsedResponse['success']){
|
||||||
|
|
||||||
|
//write the success activity
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
//handle the failure
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,331 +0,0 @@
|
||||||
<?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()
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,237 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\EDocument\Standards\Verifactu\Validation;
|
||||||
|
|
||||||
|
use App\Services\EDocument\Standards\Verifactu\Models\Invoice;
|
||||||
|
use InvalidArgumentException;
|
||||||
|
|
||||||
|
class InvoiceValidator
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Validate an invoice against AEAT business rules
|
||||||
|
*/
|
||||||
|
public function validate(Invoice $invoice): array
|
||||||
|
{
|
||||||
|
$errors = [];
|
||||||
|
|
||||||
|
// Validate NIF format
|
||||||
|
$errors = array_merge($errors, $this->validateNif($invoice));
|
||||||
|
|
||||||
|
// Validate date formats
|
||||||
|
$errors = array_merge($errors, $this->validateDates($invoice));
|
||||||
|
|
||||||
|
// Validate invoice numbers
|
||||||
|
$errors = array_merge($errors, $this->validateInvoiceNumbers($invoice));
|
||||||
|
|
||||||
|
// Validate amounts
|
||||||
|
$errors = array_merge($errors, $this->validateAmounts($invoice));
|
||||||
|
|
||||||
|
// Validate tax rates
|
||||||
|
$errors = array_merge($errors, $this->validateTaxRates($invoice));
|
||||||
|
|
||||||
|
// Validate business logic
|
||||||
|
$errors = array_merge($errors, $this->validateBusinessLogic($invoice));
|
||||||
|
|
||||||
|
return $errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate NIF format (Spanish tax identification)
|
||||||
|
*/
|
||||||
|
private function validateNif(Invoice $invoice): array
|
||||||
|
{
|
||||||
|
$errors = [];
|
||||||
|
|
||||||
|
// Check emitter NIF
|
||||||
|
if ($invoice->getTercero() && $invoice->getTercero()->getNif()) {
|
||||||
|
$nif = $invoice->getTercero()->getNif();
|
||||||
|
if (!$this->isValidNif($nif)) {
|
||||||
|
$errors[] = "Invalid emitter NIF format: {$nif}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check system NIF
|
||||||
|
if ($invoice->getSistemaInformatico() && $invoice->getSistemaInformatico()->getNif()) {
|
||||||
|
$nif = $invoice->getSistemaInformatico()->getNif();
|
||||||
|
if (!$this->isValidNif($nif)) {
|
||||||
|
$errors[] = "Invalid system NIF format: {$nif}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate date formats
|
||||||
|
*/
|
||||||
|
private function validateDates(Invoice $invoice): array
|
||||||
|
{
|
||||||
|
$errors = [];
|
||||||
|
|
||||||
|
// Validate FechaHoraHusoGenRegistro format (YYYY-MM-DDTHH:MM:SS+HH:MM)
|
||||||
|
$fechaHora = $invoice->getFechaHoraHusoGenRegistro();
|
||||||
|
if ($fechaHora && !preg_match('/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[+-]\d{2}:\d{2}$/', $fechaHora)) {
|
||||||
|
$errors[] = "Invalid FechaHoraHusoGenRegistro format. Expected: YYYY-MM-DDTHH:MM:SS+HH:MM, Got: {$fechaHora}";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate FechaExpedicionFactura format (YYYY-MM-DD)
|
||||||
|
if ($invoice->getIdFactura() && method_exists($invoice->getIdFactura(), 'getFechaExpedicionFactura')) {
|
||||||
|
$fecha = $invoice->getIdFactura()->getFechaExpedicionFactura();
|
||||||
|
if ($fecha && !preg_match('/^\d{4}-\d{2}-\d{2}$/', $fecha)) {
|
||||||
|
$errors[] = "Invalid FechaExpedicionFactura format. Expected: YYYY-MM-DD, Got: {$fecha}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate invoice numbers
|
||||||
|
*/
|
||||||
|
private function validateInvoiceNumbers(Invoice $invoice): array
|
||||||
|
{
|
||||||
|
$errors = [];
|
||||||
|
|
||||||
|
if ($invoice->getIdFactura() && method_exists($invoice->getIdFactura(), 'getNumSerieFactura')) {
|
||||||
|
$numero = $invoice->getIdFactura()->getNumSerieFactura();
|
||||||
|
|
||||||
|
// Check for common problematic patterns
|
||||||
|
if (str_contains($numero, 'TEST') && strlen($numero) < 10) {
|
||||||
|
$errors[] = "Test invoice numbers should be at least 10 characters long";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for special characters that might cause issues
|
||||||
|
if (preg_match('/[^A-Za-z0-9\-_]/', $numero)) {
|
||||||
|
$errors[] = "Invoice number contains invalid characters. Only letters, numbers, hyphens and underscores allowed";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate amounts
|
||||||
|
*/
|
||||||
|
private function validateAmounts(Invoice $invoice): array
|
||||||
|
{
|
||||||
|
$errors = [];
|
||||||
|
|
||||||
|
// Validate total amounts
|
||||||
|
if ($invoice->getImporteTotal() <= 0) {
|
||||||
|
$errors[] = "ImporteTotal must be greater than 0";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($invoice->getCuotaTotal() < 0) {
|
||||||
|
$errors[] = "CuotaTotal cannot be negative (use rectification invoice for negative amounts)";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate decimal places (AEAT expects 2 decimal places)
|
||||||
|
if (fmod($invoice->getImporteTotal() * 100, 1) !== 0.0) {
|
||||||
|
$errors[] = "ImporteTotal must have maximum 2 decimal places";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fmod($invoice->getCuotaTotal() * 100, 1) !== 0.0) {
|
||||||
|
$errors[] = "CuotaTotal must have maximum 2 decimal places";
|
||||||
|
}
|
||||||
|
|
||||||
|
return $errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate tax rates
|
||||||
|
*/
|
||||||
|
private function validateTaxRates(Invoice $invoice): array
|
||||||
|
{
|
||||||
|
$errors = [];
|
||||||
|
|
||||||
|
// Check if desglose exists and has valid tax rates
|
||||||
|
if ($invoice->getDesglose()) {
|
||||||
|
$desglose = $invoice->getDesglose();
|
||||||
|
|
||||||
|
// Validate tax rates are standard Spanish rates
|
||||||
|
$validRates = [0, 4, 10, 21];
|
||||||
|
|
||||||
|
// This would need to be implemented based on your Desglose structure
|
||||||
|
// $taxRate = $desglose->getTipoImpositivo();
|
||||||
|
// if (!in_array($taxRate, $validRates)) {
|
||||||
|
// $errors[] = "Invalid tax rate: {$taxRate}. Valid rates are: " . implode(', ', $validRates);
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
return $errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate business logic rules
|
||||||
|
*/
|
||||||
|
private function validateBusinessLogic(Invoice $invoice): array
|
||||||
|
{
|
||||||
|
$errors = [];
|
||||||
|
|
||||||
|
// Check for required fields based on invoice type
|
||||||
|
if ($invoice->getTipoFactura() === 'R1' && !$invoice->getTipoRectificativa()) {
|
||||||
|
$errors[] = "Rectification invoices (R1) must specify TipoRectificativa";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for simplified invoice requirements
|
||||||
|
if ($invoice->getTipoFactura() === 'F2' && !$invoice->getFacturaSimplificadaArt7273()) {
|
||||||
|
$errors[] = "Simplified invoices (F2) must specify FacturaSimplificadaArt7273";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for system information requirements
|
||||||
|
if (!$invoice->getSistemaInformatico()) {
|
||||||
|
$errors[] = "SistemaInformatico is required for all invoices";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for encadenamiento requirements
|
||||||
|
if (!$invoice->getEncadenamiento()) {
|
||||||
|
$errors[] = "Encadenamiento is required for all invoices";
|
||||||
|
}
|
||||||
|
|
||||||
|
return $errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if NIF format is valid for Spanish tax identification
|
||||||
|
*/
|
||||||
|
private function isValidNif(string $nif): bool
|
||||||
|
{
|
||||||
|
// Basic format validation for Spanish NIFs
|
||||||
|
// Company NIFs: Letter + 8 digits (e.g., B12345678)
|
||||||
|
// Individual NIFs: 8 digits + letter (e.g., 12345678A)
|
||||||
|
|
||||||
|
$pattern = '/^([A-Z]\d{8}|\d{8}[A-Z])$/';
|
||||||
|
return preg_match($pattern, $nif) === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get validation rules as array for documentation
|
||||||
|
*/
|
||||||
|
public function getValidationRules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'nif' => [
|
||||||
|
'format' => 'Company: Letter + 8 digits (B12345678), Individual: 8 digits + letter (12345678A)',
|
||||||
|
'required' => true
|
||||||
|
],
|
||||||
|
'dates' => [
|
||||||
|
'FechaHoraHusoGenRegistro' => 'YYYY-MM-DDTHH:MM:SS+HH:MM',
|
||||||
|
'FechaExpedicionFactura' => 'YYYY-MM-DD'
|
||||||
|
],
|
||||||
|
'amounts' => [
|
||||||
|
'decimal_places' => 'Maximum 2 decimal places',
|
||||||
|
'positive' => 'ImporteTotal must be positive',
|
||||||
|
'tax_rates' => 'Valid rates: 0%, 4%, 10%, 21%'
|
||||||
|
],
|
||||||
|
'invoice_numbers' => [
|
||||||
|
'min_length' => 'Test numbers should be at least 10 characters',
|
||||||
|
'characters' => 'Only letters, numbers, hyphens, underscores'
|
||||||
|
],
|
||||||
|
'business_logic' => [
|
||||||
|
'R1_invoices' => 'Must specify TipoRectificativa',
|
||||||
|
'F2_invoices' => 'Must specify FacturaSimplificadaArt7273',
|
||||||
|
'required_fields' => 'SistemaInformatico and Encadenamiento are required'
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -156,4 +156,9 @@ return [
|
||||||
'quickbooks_webhook' => [
|
'quickbooks_webhook' => [
|
||||||
'verifier_token' => env('QUICKBOOKS_VERIFIER_TOKEN', false),
|
'verifier_token' => env('QUICKBOOKS_VERIFIER_TOKEN', false),
|
||||||
],
|
],
|
||||||
|
'verifactu' => [
|
||||||
|
'sender_nif' => env('VERIFACTU_SENDER_NIF', ''),
|
||||||
|
'certificate' => env('VERIFACTU_CERTIFICATE', ''),
|
||||||
|
'ssl_key' => env('VERIFACTU_SSL_KEY', ''),
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|
@ -5,6 +5,7 @@ namespace Tests\Feature\EInvoice\Verifactu\Models;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
use Illuminate\Support\Facades\Http;
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use App\Services\EDocument\Standards\Verifactu\AeatAuthority;
|
||||||
use App\Services\EDocument\Standards\Verifactu\Models\Invoice;
|
use App\Services\EDocument\Standards\Verifactu\Models\Invoice;
|
||||||
use App\Services\EDocument\Standards\Verifactu\Models\Desglose;
|
use App\Services\EDocument\Standards\Verifactu\Models\Desglose;
|
||||||
use App\Services\EDocument\Standards\Verifactu\Models\Encadenamiento;
|
use App\Services\EDocument\Standards\Verifactu\Models\Encadenamiento;
|
||||||
|
|
@ -16,21 +17,44 @@ use App\Services\EDocument\Standards\Verifactu\Models\PersonaFisicaJuridica;
|
||||||
class WSTest extends TestCase
|
class WSTest extends TestCase
|
||||||
{
|
{
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
if (config('ninja.is_travis')) {
|
||||||
|
$this->markTestSkipped('No credentials to test Verifactu');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
//@todo - need to test that the user has granted power of attorney to the system
|
||||||
|
//@todo - data must be written to the database to confirm this.
|
||||||
|
public function test_verifactu_authority()
|
||||||
|
{
|
||||||
|
$authority = new AeatAuthority();
|
||||||
|
$authority->setTestMode();
|
||||||
|
$success = $authority->run('A39200019');
|
||||||
|
|
||||||
|
$this->assertTrue($success);
|
||||||
|
}
|
||||||
|
|
||||||
|
//@todo - need to confirm that building the xml and sending works.
|
||||||
public function test_generated_invoice_xml_can_send_to_web_service()
|
public function test_generated_invoice_xml_can_send_to_web_service()
|
||||||
{
|
{
|
||||||
|
|
||||||
// Generate current timestamp in the correct format
|
// Generate current timestamp in the correct format
|
||||||
$currentTimestamp = now()->setTimezone('Europe/Madrid')->format('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 = \Carbon\Carbon::parse('2023-01-01')->format('Y-m-d\TH:i:s');
|
||||||
// $currentTimestamp = now()->subDays(5)->format('Y-m-d\TH:i:s');
|
// $currentTimestamp = now()->subDays(5)->format('Y-m-d\TH:i:s');
|
||||||
|
|
||||||
nlog($currentTimestamp);
|
nlog($currentTimestamp);
|
||||||
|
|
||||||
$invoice = new Invoice();
|
$invoice = new Invoice();
|
||||||
$invoice
|
$invoice
|
||||||
->setIdVersion('1.0')
|
->setIdVersion('1.0')
|
||||||
->setIdFactura('FAC2023002')
|
->setIdFactura('FAC2023002')
|
||||||
->setFechaExpedicionFactura('2023-01-02')
|
->setFechaExpedicionFactura('02-01-2025')
|
||||||
->setRefExterna('REF-123')
|
->setRefExterna('REF-123')
|
||||||
->setNombreRazonEmisor('Empresa Ejemplo SL')
|
->setNombreRazonEmisor('Empresa Ejemplo SL')
|
||||||
->setTipoFactura('F1')
|
->setTipoFactura('F1')
|
||||||
|
|
@ -91,9 +115,7 @@ $currentTimestamp = now()->setTimezone('Europe/Madrid')->format('Y-m-d\TH:i:s');
|
||||||
|
|
||||||
$this->assertNotNull($soapXml);
|
$this->assertNotNull($soapXml);
|
||||||
|
|
||||||
|
$correctHash = $this->calculateVerifactuHash(
|
||||||
|
|
||||||
$correctHash = $this->calculateVerifactuHash(
|
|
||||||
$invoice->getTercero()->getNif(), // IDEmisorFactura
|
$invoice->getTercero()->getNif(), // IDEmisorFactura
|
||||||
$invoice->getIdFactura(), // NumSerieFactura
|
$invoice->getIdFactura(), // NumSerieFactura
|
||||||
$invoice->getFechaHoraHusoGenRegistro(), // FechaExpedicionFactura
|
$invoice->getFechaHoraHusoGenRegistro(), // FechaExpedicionFactura
|
||||||
|
|
@ -102,22 +124,22 @@ $correctHash = $this->calculateVerifactuHash(
|
||||||
$invoice->getImporteTotal(), // ImporteTotal
|
$invoice->getImporteTotal(), // ImporteTotal
|
||||||
'', // Huella (empty for first calculation)
|
'', // Huella (empty for first calculation)
|
||||||
$currentTimestamp // FechaHoraHusoGenRegistro (current time)
|
$currentTimestamp // FechaHoraHusoGenRegistro (current time)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Replace the placeholder with the correct hash
|
// Replace the placeholder with the correct hash
|
||||||
$soapXml = str_replace('PLACEHOLDER_HUELLA', $correctHash, $soapXml);
|
$soapXml = str_replace('PLACEHOLDER_HUELLA', $correctHash, $soapXml);
|
||||||
|
|
||||||
nlog("test_generated_invoice_xml_can_send_to_web_service");
|
nlog("test_generated_invoice_xml_can_send_to_web_service");
|
||||||
nlog('Calculated hash for XML: ' . $correctHash);
|
nlog('Calculated hash for XML: ' . $correctHash);
|
||||||
|
|
||||||
// Sign the XML before sending
|
// Sign the XML before sending
|
||||||
$certPath = storage_path('aeat-cert5.pem');
|
$certPath = storage_path('aeat-cert5.pem');
|
||||||
$keyPath = storage_path('aeat-key5.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));
|
$signingService = new \App\Services\EDocument\Standards\Verifactu\Signing\SigningService($soapXml, file_get_contents($keyPath), file_get_contents($certPath));
|
||||||
$soapXml = $signingService->sign();
|
$soapXml = $signingService->sign();
|
||||||
|
|
||||||
// Try direct HTTP approach instead of SOAP client
|
// Try direct HTTP approach instead of SOAP client
|
||||||
$response = Http::withHeaders([
|
$response = Http::withHeaders([
|
||||||
'Content-Type' => 'text/xml; charset=utf-8',
|
'Content-Type' => 'text/xml; charset=utf-8',
|
||||||
'SOAPAction' => '',
|
'SOAPAction' => '',
|
||||||
])
|
])
|
||||||
|
|
@ -130,41 +152,39 @@ $response = Http::withHeaders([
|
||||||
->withBody($soapXml, 'text/xml')
|
->withBody($soapXml, 'text/xml')
|
||||||
->post('https://prewww1.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP');
|
->post('https://prewww1.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP');
|
||||||
|
|
||||||
nlog('Request with AEAT official test data:');
|
nlog('Request with AEAT official test data:');
|
||||||
nlog($soapXml);
|
nlog($soapXml);
|
||||||
nlog('Response with AEAT official test data:');
|
nlog('Response with AEAT official test data:');
|
||||||
nlog('Response Status: ' . $response->status());
|
nlog('Response Status: ' . $response->status());
|
||||||
nlog('Response Headers: ' . json_encode($response->headers()));
|
nlog('Response Headers: ' . json_encode($response->headers()));
|
||||||
nlog('Response Body: ' . $response->body());
|
nlog('Response Body: ' . $response->body());
|
||||||
|
|
||||||
if (!$response->successful()) {
|
if (!$response->successful()) {
|
||||||
\Log::error('Request failed with status: ' . $response->status());
|
\Log::error('Request failed with status: ' . $response->status());
|
||||||
\Log::error('Response body: ' . $response->body());
|
\Log::error('Response body: ' . $response->body());
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->assertTrue($response->successful());
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
$this->assertTrue($response->successful());
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//Confirmed, this works. requires us to track the previous hash for each company to be used in subsequent calls.
|
||||||
public function test_send_aeat_example_to_verifactu()
|
public function test_send_aeat_example_to_verifactu()
|
||||||
{
|
{
|
||||||
// Generate current timestamp in the correct format
|
// 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');
|
$currentTimestamp = now()->setTimezone('Europe/Madrid')->format('Y-m-d\TH:i:sP');
|
||||||
$invoice_number = 'TEST0033343443';
|
$invoice_number = 'TEST0033343443';
|
||||||
$previous_invoice_number = 'TEST0033343442';
|
$previous_invoice_number = 'TEST0033343442';
|
||||||
$invoice_date = '02-07-2025';
|
$invoice_date = '02-07-2025';
|
||||||
$previous_hash = '10C643EDC7DC727FAC6BAEBAAC7BEA67B5C1369A5A5ED74E5AD3149FC30A3C8C';
|
$previous_hash = '10C643EDC7DC727FAC6BAEBAAC7BEA67B5C1369A5A5ED74E5AD3149FC30A3C8C';
|
||||||
$nif = 'A39200019';
|
$nif = 'A39200019';
|
||||||
|
|
||||||
$soapXml = <<<XML
|
$soapXml = <<<XML
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
|
<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: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">
|
xmlns:sum1="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd">
|
||||||
<soapenv:Header/>
|
<soapenv:Header/>
|
||||||
|
|
@ -235,8 +255,8 @@ $nif = 'A39200019';
|
||||||
</sum:RegistroFactura>
|
</sum:RegistroFactura>
|
||||||
</sum:RegFactuSistemaFacturacion>
|
</sum:RegFactuSistemaFacturacion>
|
||||||
</soapenv:Body>
|
</soapenv:Body>
|
||||||
</soapenv:Envelope>
|
</soapenv:Envelope>
|
||||||
XML;
|
XML;
|
||||||
|
|
||||||
// Calculate the correct hash using AEAT's specified format
|
// Calculate the correct hash using AEAT's specified format
|
||||||
$correctHash = $this->calculateVerifactuHash(
|
$correctHash = $this->calculateVerifactuHash(
|
||||||
|
|
@ -290,29 +310,35 @@ XML;
|
||||||
$this->assertTrue($response->successful());
|
$this->assertTrue($response->successful());
|
||||||
|
|
||||||
|
|
||||||
$responseProcessor = new ResponseProcessor();
|
$responseProcessor = new ResponseProcessor();
|
||||||
$responseProcessor->processResponse($response->body());
|
$responseProcessor->processResponse($response->body());
|
||||||
|
|
||||||
nlog($responseProcessor->getSummary());
|
nlog($responseProcessor->getSummary());
|
||||||
|
|
||||||
$this->assertTrue($responseProcessor->getSummary()['success']);
|
$this->assertTrue($responseProcessor->getSummary()['success']);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//@todo - need to test that cancelling an invoice works.
|
||||||
|
public function test_cancel_existing_invoice()
|
||||||
|
{
|
||||||
|
//@todo - need to test that cancelling an invoice works.
|
||||||
|
}
|
||||||
|
|
||||||
public function test_cancel_and_modify_existing_invoice()
|
//@todo - Need to test that modifying an invoice works.
|
||||||
{
|
public function test_cancel_and_modify_existing_invoice()
|
||||||
|
{
|
||||||
|
|
||||||
|
|
||||||
$currentTimestamp = now()->setTimezone('Europe/Madrid')->format('Y-m-d\TH:i:sP');
|
$currentTimestamp = now()->setTimezone('Europe/Madrid')->format('Y-m-d\TH:i:sP');
|
||||||
$invoice_number = 'TEST0033343436';
|
$invoice_number = 'TEST0033343436';
|
||||||
$invoice_date = '02-07-2025';
|
$invoice_date = '02-07-2025';
|
||||||
$calc_hash = 'A0B4D14E6F7769860C8A4EAFFA3EEBF52B7044685BD69D1DB5BBD68EA0E2BA21';
|
$calc_hash = 'A0B4D14E6F7769860C8A4EAFFA3EEBF52B7044685BD69D1DB5BBD68EA0E2BA21';
|
||||||
$nif = '99999910G';
|
$nif = '99999910G';
|
||||||
|
|
||||||
$soapXml = <<<XML
|
$soapXml = <<<XML
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?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: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>
|
<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: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:NIF>A39200019</tik:NIF>
|
||||||
|
|
@ -321,7 +347,7 @@ $nif = '99999910G';
|
||||||
</soapenv:Header>
|
</soapenv:Header>
|
||||||
|
|
||||||
<soapenv:Body>
|
<soapenv:Body>
|
||||||
<sum:ModificacionFactura>
|
<sum:ModificacionFactura>
|
||||||
|
|
||||||
<sum1:RegistroAnulacion>
|
<sum1:RegistroAnulacion>
|
||||||
<sum1:IDFactura>
|
<sum1:IDFactura>
|
||||||
|
|
@ -385,14 +411,14 @@ $nif = '99999910G';
|
||||||
|
|
||||||
</sum1:RegistroModificacion>
|
</sum1:RegistroModificacion>
|
||||||
|
|
||||||
</sum:ModificacionFactura>
|
</sum:ModificacionFactura>
|
||||||
</soapenv:Body>
|
</soapenv:Body>
|
||||||
</soapenv:Envelope>
|
</soapenv:Envelope>
|
||||||
XML;
|
XML;
|
||||||
|
|
||||||
|
|
||||||
// Calculate the correct hash using AEAT's specified format
|
// Calculate the correct hash using AEAT's specified format
|
||||||
$correctHash = $this->calculateVerifactuHash(
|
$correctHash = $this->calculateVerifactuHash(
|
||||||
$nif, // IDEmisorFactura
|
$nif, // IDEmisorFactura
|
||||||
$invoice_number, // NumSerieFactura
|
$invoice_number, // NumSerieFactura
|
||||||
$invoice_date, // FechaExpedicionFactura
|
$invoice_date, // FechaExpedicionFactura
|
||||||
|
|
@ -401,21 +427,21 @@ $correctHash = $this->calculateVerifactuHash(
|
||||||
'121.00', // ImporteTotal
|
'121.00', // ImporteTotal
|
||||||
'', // Huella (empty for first calculation)
|
'', // Huella (empty for first calculation)
|
||||||
$currentTimestamp // FechaHoraHusoGenRegistro (current time)
|
$currentTimestamp // FechaHoraHusoGenRegistro (current time)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Replace the placeholder with the correct hash
|
// Replace the placeholder with the correct hash
|
||||||
$soapXml = str_replace('PLACEHOLDER_HUELLA', $correctHash, $soapXml);
|
$soapXml = str_replace('PLACEHOLDER_HUELLA', $correctHash, $soapXml);
|
||||||
|
|
||||||
nlog('Calculated hash for XML: ' . $correctHash);
|
nlog('Calculated hash for XML: ' . $correctHash);
|
||||||
|
|
||||||
// Sign the XML before sending
|
// Sign the XML before sending
|
||||||
$certPath = storage_path('aeat-cert5.pem');
|
$certPath = storage_path('aeat-cert5.pem');
|
||||||
$keyPath = storage_path('aeat-key5.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));
|
$signingService = new \App\Services\EDocument\Standards\Verifactu\Signing\SigningService($soapXml, file_get_contents($keyPath), file_get_contents($certPath));
|
||||||
$soapXml = $signingService->sign();
|
$soapXml = $signingService->sign();
|
||||||
|
|
||||||
// Try direct HTTP approach instead of SOAP client
|
// Try direct HTTP approach instead of SOAP client
|
||||||
$response = Http::withHeaders([
|
$response = Http::withHeaders([
|
||||||
'Content-Type' => 'text/xml; charset=utf-8',
|
'Content-Type' => 'text/xml; charset=utf-8',
|
||||||
'SOAPAction' => '',
|
'SOAPAction' => '',
|
||||||
])
|
])
|
||||||
|
|
@ -428,30 +454,27 @@ $response = Http::withHeaders([
|
||||||
->withBody($soapXml, 'text/xml')
|
->withBody($soapXml, 'text/xml')
|
||||||
->post('https://prewww1.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP');
|
->post('https://prewww1.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP');
|
||||||
|
|
||||||
nlog('Request with AEAT official test data:');
|
nlog('Request with AEAT official test data:');
|
||||||
nlog($soapXml);
|
nlog($soapXml);
|
||||||
nlog('Response with AEAT official test data:');
|
nlog('Response with AEAT official test data:');
|
||||||
nlog('Response Status: ' . $response->status());
|
nlog('Response Status: ' . $response->status());
|
||||||
nlog('Response Headers: ' . json_encode($response->headers()));
|
nlog('Response Headers: ' . json_encode($response->headers()));
|
||||||
nlog('Response Body: ' . $response->body());
|
nlog('Response Body: ' . $response->body());
|
||||||
|
|
||||||
if (!$response->successful()) {
|
if (!$response->successful()) {
|
||||||
\Log::error('Request failed with status: ' . $response->status());
|
\Log::error('Request failed with status: ' . $response->status());
|
||||||
\Log::error('Response body: ' . $response->body());
|
\Log::error('Response body: ' . $response->body());
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->assertTrue($response->successful());
|
$this->assertTrue($response->successful());
|
||||||
|
|
||||||
|
|
||||||
$responseProcessor = new ResponseProcessor();
|
$responseProcessor = new ResponseProcessor();
|
||||||
$responseProcessor->processResponse($response->body());
|
$responseProcessor->processResponse($response->body());
|
||||||
|
|
||||||
nlog($responseProcessor->getSummary());
|
|
||||||
|
|
||||||
$this->assertTrue($responseProcessor->getSummary()['success']);
|
|
||||||
|
|
||||||
|
|
||||||
|
nlog($responseProcessor->getSummary());
|
||||||
|
|
||||||
|
$this->assertTrue($responseProcessor->getSummary()['success']);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue