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);
|
||||
}
|
||||
|
||||
public function verifactu_logs(): HasMany
|
||||
{
|
||||
return $this->hasMany(VerifactuLog::class);
|
||||
}
|
||||
|
||||
public function task_schedulers(): HasMany
|
||||
{
|
||||
return $this->hasMany(Scheduler::class);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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' => [
|
||||
'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 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']);
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue