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); return $this->hasMany(Scheduler::class);
} }
public function verifactu_logs(): HasMany
{
return $this->hasMany(VerifactuLog::class);
}
public function task_schedulers(): HasMany public function task_schedulers(): HasMany
{ {
return $this->hasMany(Scheduler::class); return $this->hasMany(Scheduler::class);

View File

@ -400,6 +400,11 @@ class Invoice extends BaseModel
return $this->hasMany(Credit::class); return $this->hasMany(Credit::class);
} }
public function verifactu_logs(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(VerifactuLog::class);
}
public function tasks(): \Illuminate\Database\Eloquent\Relations\HasMany public function tasks(): \Illuminate\Database\Eloquent\Relations\HasMany
{ {
return $this->hasMany(Task::class); return $this->hasMany(Task::class);

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' => [ 'quickbooks_webhook' => [
'verifier_token' => env('QUICKBOOKS_VERIFIER_TOKEN', false), 'verifier_token' => env('QUICKBOOKS_VERIFIER_TOKEN', false),
], ],
'verifactu' => [
'sender_nif' => env('VERIFACTU_SENDER_NIF', ''),
'certificate' => env('VERIFACTU_CERTIFICATE', ''),
'ssl_key' => env('VERIFACTU_SSL_KEY', ''),
],
]; ];

View File

@ -5,6 +5,7 @@ namespace Tests\Feature\EInvoice\Verifactu\Models;
use Tests\TestCase; use Tests\TestCase;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use App\Services\EDocument\Standards\Verifactu\AeatAuthority;
use App\Services\EDocument\Standards\Verifactu\Models\Invoice; use App\Services\EDocument\Standards\Verifactu\Models\Invoice;
use App\Services\EDocument\Standards\Verifactu\Models\Desglose; use App\Services\EDocument\Standards\Verifactu\Models\Desglose;
use App\Services\EDocument\Standards\Verifactu\Models\Encadenamiento; use App\Services\EDocument\Standards\Verifactu\Models\Encadenamiento;
@ -16,6 +17,28 @@ use App\Services\EDocument\Standards\Verifactu\Models\PersonaFisicaJuridica;
class WSTest extends TestCase class WSTest extends TestCase
{ {
protected function setUp(): void
{
parent::setUp();
if (config('ninja.is_travis')) {
$this->markTestSkipped('No credentials to test Verifactu');
}
}
//@todo - need to test that the user has granted power of attorney to the system
//@todo - data must be written to the database to confirm this.
public function test_verifactu_authority()
{
$authority = new AeatAuthority();
$authority->setTestMode();
$success = $authority->run('A39200019');
$this->assertTrue($success);
}
//@todo - need to confirm that building the xml and sending works.
public function test_generated_invoice_xml_can_send_to_web_service() public function test_generated_invoice_xml_can_send_to_web_service()
{ {
@ -26,11 +49,12 @@ $currentTimestamp = now()->setTimezone('Europe/Madrid')->format('Y-m-d\TH:i:s');
// $currentTimestamp = now()->subDays(5)->format('Y-m-d\TH:i:s'); // $currentTimestamp = now()->subDays(5)->format('Y-m-d\TH:i:s');
nlog($currentTimestamp); nlog($currentTimestamp);
$invoice = new Invoice(); $invoice = new Invoice();
$invoice $invoice
->setIdVersion('1.0') ->setIdVersion('1.0')
->setIdFactura('FAC2023002') ->setIdFactura('FAC2023002')
->setFechaExpedicionFactura('2023-01-02') ->setFechaExpedicionFactura('02-01-2025')
->setRefExterna('REF-123') ->setRefExterna('REF-123')
->setNombreRazonEmisor('Empresa Ejemplo SL') ->setNombreRazonEmisor('Empresa Ejemplo SL')
->setTipoFactura('F1') ->setTipoFactura('F1')
@ -91,8 +115,6 @@ $currentTimestamp = now()->setTimezone('Europe/Madrid')->format('Y-m-d\TH:i:s');
$this->assertNotNull($soapXml); $this->assertNotNull($soapXml);
$correctHash = $this->calculateVerifactuHash( $correctHash = $this->calculateVerifactuHash(
$invoice->getTercero()->getNif(), // IDEmisorFactura $invoice->getTercero()->getNif(), // IDEmisorFactura
$invoice->getIdFactura(), // NumSerieFactura $invoice->getIdFactura(), // NumSerieFactura
@ -144,12 +166,10 @@ if (!$response->successful()) {
$this->assertTrue($response->successful()); $this->assertTrue($response->successful());
} }
//Confirmed, this works. requires us to track the previous hash for each company to be used in subsequent calls.
public function test_send_aeat_example_to_verifactu() public function test_send_aeat_example_to_verifactu()
{ {
// Generate current timestamp in the correct format // Generate current timestamp in the correct format
@ -299,7 +319,13 @@ $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.
}
//@todo - Need to test that modifying an invoice works.
public function test_cancel_and_modify_existing_invoice() public function test_cancel_and_modify_existing_invoice()
{ {
@ -450,9 +476,6 @@ nlog($responseProcessor->getSummary());
$this->assertTrue($responseProcessor->getSummary()['success']); $this->assertTrue($responseProcessor->getSummary()['success']);
} }