Add logging to wstest for requirements

This commit is contained in:
David Bomba 2025-08-07 17:12:57 +10:00
parent bf5359cb72
commit 4127eb32f9
8 changed files with 765 additions and 602 deletions

View File

@ -428,6 +428,11 @@ class Company extends BaseModel
return $this->hasMany(Scheduler::class);
}
public function verifactu_logs(): HasMany
{
return $this->hasMany(VerifactuLog::class);
}
public function task_schedulers(): HasMany
{
return $this->hasMany(Scheduler::class);

View File

@ -400,6 +400,11 @@ class Invoice extends BaseModel
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
{
return $this->hasMany(Task::class);

View File

@ -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'];
}
}

View File

@ -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
}
}
}

View File

@ -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()
];
}
}

View File

@ -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'
]
];
}
}

View File

@ -156,4 +156,9 @@ return [
'quickbooks_webhook' => [
'verifier_token' => env('QUICKBOOKS_VERIFIER_TOKEN', false),
],
'verifactu' => [
'sender_nif' => env('VERIFACTU_SENDER_NIF', ''),
'certificate' => env('VERIFACTU_CERTIFICATE', ''),
'ssl_key' => env('VERIFACTU_SSL_KEY', ''),
],
];

View File

@ -5,6 +5,7 @@ namespace Tests\Feature\EInvoice\Verifactu\Models;
use Tests\TestCase;
use Illuminate\Support\Facades\Log;
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\Desglose;
use App\Services\EDocument\Standards\Verifactu\Models\Encadenamiento;
@ -16,21 +17,44 @@ use App\Services\EDocument\Standards\Verifactu\Models\PersonaFisicaJuridica;
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()
{
// Generate current timestamp in the correct format
$currentTimestamp = now()->setTimezone('Europe/Madrid')->format('Y-m-d\TH:i:s');
// Generate current timestamp in the correct format
$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');
// $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);
nlog($currentTimestamp);
$invoice = new Invoice();
$invoice
->setIdVersion('1.0')
->setIdFactura('FAC2023002')
->setFechaExpedicionFactura('2023-01-02')
->setFechaExpedicionFactura('02-01-2025')
->setRefExterna('REF-123')
->setNombreRazonEmisor('Empresa Ejemplo SL')
->setTipoFactura('F1')
@ -91,152 +115,148 @@ $currentTimestamp = now()->setTimezone('Europe/Madrid')->format('Y-m-d\TH:i:s');
$this->assertNotNull($soapXml);
$correctHash = $this->calculateVerifactuHash(
$invoice->getTercero()->getNif(), // IDEmisorFactura
$invoice->getIdFactura(), // NumSerieFactura
$invoice->getFechaHoraHusoGenRegistro(), // FechaExpedicionFactura
$invoice->getTipoFactura(), // TipoFactura
$invoice->getCuotaTotal(), // CuotaTotal
$invoice->getImporteTotal(), // ImporteTotal
'', // Huella (empty for first calculation)
$currentTimestamp // FechaHoraHusoGenRegistro (current time)
);
// Replace the placeholder with the correct hash
$soapXml = str_replace('PLACEHOLDER_HUELLA', $correctHash, $soapXml);
$correctHash = $this->calculateVerifactuHash(
$invoice->getTercero()->getNif(), // IDEmisorFactura
$invoice->getIdFactura(), // NumSerieFactura
$invoice->getFechaHoraHusoGenRegistro(), // FechaExpedicionFactura
$invoice->getTipoFactura(), // TipoFactura
$invoice->getCuotaTotal(), // CuotaTotal
$invoice->getImporteTotal(), // ImporteTotal
'', // Huella (empty for first calculation)
$currentTimestamp // FechaHoraHusoGenRegistro (current time)
);
nlog("test_generated_invoice_xml_can_send_to_web_service");
nlog('Calculated hash for XML: ' . $correctHash);
// Replace the placeholder with the correct hash
$soapXml = str_replace('PLACEHOLDER_HUELLA', $correctHash, $soapXml);
// 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();
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-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());
// 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());
}
//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()
{
// Generate current timestamp in the correct format
// $currentTimestamp = date('Y-m-d\TH:i:sP');
$currentTimestamp = now()->setTimezone('Europe/Madrid')->format('Y-m-d\TH:i:sP');
$invoice_number = 'TEST0033343443';
$previous_invoice_number = 'TEST0033343442';
$invoice_date = '02-07-2025';
$previous_hash = '10C643EDC7DC727FAC6BAEBAAC7BEA67B5C1369A5A5ED74E5AD3149FC30A3C8C';
$nif = 'A39200019';
$currentTimestamp = now()->setTimezone('Europe/Madrid')->format('Y-m-d\TH:i:sP');
$invoice_number = 'TEST0033343443';
$previous_invoice_number = 'TEST0033343442';
$invoice_date = '02-07-2025';
$previous_hash = '10C643EDC7DC727FAC6BAEBAAC7BEA67B5C1369A5A5ED74E5AD3149FC30A3C8C';
$nif = 'A39200019';
$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/>
<soapenv:Body>
<sum:RegFactuSistemaFacturacion>
<sum:Cabecera>
<!-- ObligadoEmision: The computer system submitting on behalf of the invoice issuer -->
<sum1:ObligadoEmision>
<sum1:NombreRazon>CERTIFICADO FISICA PRUEBAS</sum1:NombreRazon>
<sum1:NIF>{$nif}</sum1:NIF>
</sum1:ObligadoEmision>
</sum:Cabecera>
<sum:RegistroFactura>
<sum1:RegistroAlta>
<sum1:IDVersion>1.0</sum1:IDVersion>
<!-- IDFactura: The actual invoice issuer (using same test NIF) -->
<sum1:IDFactura>
<sum1:IDEmisorFactura>{$nif}</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:RegistroAnterior>
<sum1:IDEmisorFactura>{$nif}</sum1:IDEmisorFactura>
<sum1:NumSerieFactura>{$previous_invoice_number}</sum1:NumSerieFactura>
<sum1:FechaExpedicionFactura>02-07-2025</sum1:FechaExpedicionFactura>
<sum1:Huella>{$previous_hash}</sum1:Huella>
</sum1:RegistroAnterior>
</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:RegistroAlta>
</sum:RegistroFactura>
</sum:RegFactuSistemaFacturacion>
</soapenv:Body>
</soapenv:Envelope>
XML;
$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/>
<soapenv:Body>
<sum:RegFactuSistemaFacturacion>
<sum:Cabecera>
<!-- ObligadoEmision: The computer system submitting on behalf of the invoice issuer -->
<sum1:ObligadoEmision>
<sum1:NombreRazon>CERTIFICADO FISICA PRUEBAS</sum1:NombreRazon>
<sum1:NIF>{$nif}</sum1:NIF>
</sum1:ObligadoEmision>
</sum:Cabecera>
<sum:RegistroFactura>
<sum1:RegistroAlta>
<sum1:IDVersion>1.0</sum1:IDVersion>
<!-- IDFactura: The actual invoice issuer (using same test NIF) -->
<sum1:IDFactura>
<sum1:IDEmisorFactura>{$nif}</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:RegistroAnterior>
<sum1:IDEmisorFactura>{$nif}</sum1:IDEmisorFactura>
<sum1:NumSerieFactura>{$previous_invoice_number}</sum1:NumSerieFactura>
<sum1:FechaExpedicionFactura>02-07-2025</sum1:FechaExpedicionFactura>
<sum1:Huella>{$previous_hash}</sum1:Huella>
</sum1:RegistroAnterior>
</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:RegistroAlta>
</sum:RegistroFactura>
</sum:RegFactuSistemaFacturacion>
</soapenv:Body>
</soapenv:Envelope>
XML;
// Calculate the correct hash using AEAT's specified format
$correctHash = $this->calculateVerifactuHash(
@ -290,168 +310,171 @@ XML;
$this->assertTrue($response->successful());
$responseProcessor = new ResponseProcessor();
$responseProcessor->processResponse($response->body());
$responseProcessor = new ResponseProcessor();
$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');
$invoice_number = 'TEST0033343436';
$invoice_date = '02-07-2025';
$calc_hash = 'A0B4D14E6F7769860C8A4EAFFA3EEBF52B7044685BD69D1DB5BBD68EA0E2BA21';
$nif = '99999910G';
$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>
$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>
<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: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: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: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>
</sum1:RegistroModificacion>
</sum:ModificacionFactura>
</soapenv:Body>
</soapenv:Envelope>
XML;
</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)
);
// 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);
// Replace the placeholder with the correct hash
$soapXml = str_replace('PLACEHOLDER_HUELLA', $correctHash, $soapXml);
nlog('Calculated hash for XML: ' . $correctHash);
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();
// 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');
// 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());
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());
}
if (!$response->successful()) {
\Log::error('Request failed with status: ' . $response->status());
\Log::error('Response body: ' . $response->body());
}
$this->assertTrue($response->successful());
$this->assertTrue($response->successful());
$responseProcessor = new ResponseProcessor();
$responseProcessor->processResponse($response->body());
nlog($responseProcessor->getSummary());
$this->assertTrue($responseProcessor->getSummary()['success']);
$responseProcessor = new ResponseProcessor();
$responseProcessor->processResponse($response->body());
nlog($responseProcessor->getSummary());
$this->assertTrue($responseProcessor->getSummary()['success']);
}