853 lines
34 KiB
PHP
853 lines
34 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature\EInvoice\Verifactu;
|
|
|
|
use Tests\TestCase;
|
|
use App\Models\User;
|
|
use App\Models\Client;
|
|
use App\Models\Account;
|
|
use App\Models\Company;
|
|
use App\Models\Invoice;
|
|
use Faker\Factory as Faker;
|
|
use App\Models\CompanyToken;
|
|
use App\Models\VerifactuLog;
|
|
use App\Models\ClientContact;
|
|
use App\DataMapper\InvoiceItem;
|
|
use App\Factory\InvoiceFactory;
|
|
use App\DataMapper\ClientSettings;
|
|
use App\DataMapper\CompanySettings;
|
|
use App\Factory\CompanyUserFactory;
|
|
use Illuminate\Support\Facades\Http;
|
|
use App\Repositories\InvoiceRepository;
|
|
use App\Services\EDocument\Standards\Verifactu;
|
|
use App\Services\EDocument\Standards\Verifactu\RegistroAlta;
|
|
use App\Services\EDocument\Standards\Verifactu\Models\Desglose;
|
|
use App\Services\EDocument\Standards\Verifactu\Models\IDFactura;
|
|
use App\Services\EDocument\Standards\Verifactu\ResponseProcessor;
|
|
use App\Services\EDocument\Standards\Verifactu\Models\Encadenamiento;
|
|
use App\Services\EDocument\Standards\Verifactu\Models\RegistroAnterior;
|
|
use App\Services\EDocument\Standards\Validation\VerifactuDocumentValidator;
|
|
use App\Services\EDocument\Standards\Verifactu\Models\Invoice as VerifactuInvoice;
|
|
|
|
class VerifactuFeatureTest extends TestCase
|
|
{
|
|
/** @var Account $account */
|
|
private Account $account;
|
|
private $company;
|
|
private $user;
|
|
private $cu;
|
|
private $token;
|
|
private $client;
|
|
private $faker;
|
|
|
|
// private string $nombre_razon = 'CERTIFICADO ENTIDAD PRUEBAS'; //must match the cert name
|
|
private string $nombre_razon = 'CERTIFICADO FISICA PRUEBAS'; //must match the cert name
|
|
|
|
private string $test_company_nif = 'A39200019';
|
|
|
|
private string $test_client_nif = 'A39200019';
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
$this->faker = Faker::create();
|
|
|
|
// $this->markTestSkipped('not now');
|
|
}
|
|
|
|
/**
|
|
* Helper to stub test data.
|
|
*
|
|
* @param mixed $settings
|
|
* @return Invoice $invoice
|
|
*/
|
|
private function buildData($settings = null)
|
|
{
|
|
/** @var Account $a */
|
|
$a = Account::factory()->create([
|
|
'hosted_client_count' => 1000,
|
|
'hosted_company_count' => 1000,
|
|
]);
|
|
|
|
$a->num_users = 3;
|
|
$a->save();
|
|
|
|
$this->account = $a;
|
|
|
|
/** @var User $u */
|
|
$u = User::factory()->create([
|
|
'account_id' => $this->account->id,
|
|
'confirmation_code' => 'xyz123',
|
|
'email' => $this->faker->unique()->safeEmail(),
|
|
]);
|
|
|
|
$this->user = $u;
|
|
|
|
if(!$settings) {
|
|
$settings = CompanySettings::defaults();
|
|
$settings->client_online_payment_notification = false;
|
|
$settings->client_manual_payment_notification = false;
|
|
$settings->country_id = 724;
|
|
$settings->currency_id = 3;
|
|
$settings->address1 = 'Calle Mayor 123'; // Main Street 123
|
|
$settings->city = 'Madrid';
|
|
$settings->state = 'Madrid';
|
|
$settings->postal_code = '28001';
|
|
$settings->vat_number = 'B12345678'; // Spanish VAT number format
|
|
$settings->payment_terms = '10';
|
|
$settings->vat_number = $this->test_company_nif;
|
|
$settings->name = $this->nombre_razon;
|
|
}
|
|
|
|
/** @var Company $company */
|
|
$company = Company::factory()->create([
|
|
'account_id' => $this->account->id,
|
|
'settings' => $settings,
|
|
]);
|
|
|
|
$this->company = $company;
|
|
$this->company->settings = $settings;
|
|
$this->company->save();
|
|
|
|
$this->cu = CompanyUserFactory::create($this->user->id, $this->company->id, $this->account->id);
|
|
$this->cu->is_owner = true;
|
|
$this->cu->is_admin = true;
|
|
$this->cu->is_locked = false;
|
|
$this->cu->save();
|
|
|
|
$this->token = \Illuminate\Support\Str::random(64);
|
|
|
|
$company_token = new CompanyToken();
|
|
$company_token->user_id = $this->user->id;
|
|
$company_token->company_id = $this->company->id;
|
|
$company_token->account_id = $this->account->id;
|
|
$company_token->name = 'test token';
|
|
$company_token->token = $this->token;
|
|
$company_token->is_system = true;
|
|
|
|
$company_token->save();
|
|
|
|
$client_settings = ClientSettings::defaults();
|
|
$client_settings->currency_id = '3';
|
|
|
|
/** @var Client $client */
|
|
$client = Client::factory()->create([
|
|
'user_id' => $this->user->id,
|
|
'company_id' => $this->company->id,
|
|
'is_deleted' => 0,
|
|
'name' => $this->nombre_razon,
|
|
'address1' => 'Calle Mayor 123',
|
|
'city' => 'Madrid',
|
|
'state' => 'Madrid',
|
|
'postal_code' => '28001',
|
|
'country_id' => 724,
|
|
'vat_number' => $this->test_client_nif,
|
|
'balance' => 0,
|
|
'paid_to_date' => 0,
|
|
'settings' => $client_settings,
|
|
]);
|
|
|
|
$this->client = $client;
|
|
|
|
ClientContact::factory()->create([
|
|
'user_id' => $this->user->id,
|
|
'client_id' => $this->client->id,
|
|
'company_id' => $this->company->id,
|
|
'is_primary' => 1,
|
|
'first_name' => 'john',
|
|
'last_name' => 'doe',
|
|
'email' => 'john@doe.com',
|
|
'send_email' => true,
|
|
]);
|
|
|
|
$line_items = [];
|
|
|
|
$item = new InvoiceItem();
|
|
$item->product_key = '1234567890';
|
|
$item->quantity = 1;
|
|
$item->cost = 100;
|
|
$item->notes = 'Test item';
|
|
$item->tax_name1 = 'IVA';
|
|
$item->tax_rate1 = 21;
|
|
$item->discount =0;
|
|
|
|
$line_items[] = $item;
|
|
|
|
$invoice = Invoice::factory()->create([
|
|
'user_id' => $this->user->id,
|
|
'company_id' => $this->company->id,
|
|
'client_id' => $this->client->id,
|
|
'date' => now()->format('Y-m-d'),
|
|
'next_send_date' => null,
|
|
'due_date' => now()->addDays(5)->format('Y-m-d'),
|
|
'last_sent_date' => now(),
|
|
'reminder_last_sent' => null,
|
|
'uses_inclusive_taxes' => false,
|
|
'discount' => 0,
|
|
'is_amount_discount' => false,
|
|
'status_id' => Invoice::STATUS_DRAFT,
|
|
'amount' => 10,
|
|
'balance' => 10,
|
|
'line_items' => $line_items,
|
|
'tax_rate1' => 0,
|
|
'tax_rate2' => 0,
|
|
'tax_rate3' => 0,
|
|
'tax_name1' => '',
|
|
'tax_name2' => '',
|
|
'tax_name3' => '',
|
|
]);
|
|
|
|
$invoice = $invoice->calc()
|
|
->getInvoice()
|
|
->service()
|
|
->markSent()
|
|
->save();
|
|
|
|
return $invoice;
|
|
}
|
|
|
|
/**
|
|
* test_construction_and_validation
|
|
*
|
|
* tests building / validating / sending a NEW invoice in a chain
|
|
* @return void
|
|
*/
|
|
public function test_construction_and_validation()
|
|
{
|
|
// - current previous hash - 10C643EDC7DC727FAC6BAEBAAC7BEA67B5C1369A5A5ED74E5AD3149FC30A3C8C
|
|
//BE95547AA8B973A3D6A860B36833FBDE3C8AB853F4B8F05872574A5DA7314A23
|
|
// - current previous invoice number - TEST0033343443
|
|
|
|
$invoice = $this->buildData();
|
|
|
|
$invoice->number = 'TEST0033343460';
|
|
$invoice->save();
|
|
|
|
$this->assertNotNull($invoice);
|
|
|
|
/** @var Invoice $_inv */
|
|
$_inv = Invoice::factory()->create([
|
|
'user_id' => $invoice->user_id,
|
|
'company_id' => $invoice->company_id,
|
|
'client_id' => $invoice->client_id,
|
|
'date' => '2025-08-10',
|
|
'status_id' => Invoice::STATUS_SENT,
|
|
'uses_inclusive_taxes' => false,
|
|
]);
|
|
|
|
$xx = VerifactuLog::create([
|
|
'invoice_id' => $_inv->id,
|
|
'company_id' => $invoice->company_id,
|
|
'invoice_number' => 'TEST0033343459',
|
|
'date' => '2025-08-10',
|
|
'hash' => 'E5A23515881D696FCD1CA8EE4902632BFC6D892BA8EB79CB656A5F84963079D3',
|
|
'nif' => 'A39200019',
|
|
'previous_hash' => 'E5A23515881D696FCD1CA8EE4902632BFC6D892BA8EB79CB656A5F84963079D3',
|
|
]);
|
|
|
|
$verifactu = new Verifactu($invoice);
|
|
$verifactu->run();
|
|
$verifactu->setTestMode()
|
|
->setPreviousHash('E5A23515881D696FCD1CA8EE4902632BFC6D892BA8EB79CB656A5F84963079D3');
|
|
|
|
$validator = new \App\Services\EDocument\Standards\Validation\VerifactuDocumentValidator($verifactu->getEnvelope());
|
|
$validator->validate();
|
|
$errors = $validator->getVerifactuErrors();
|
|
|
|
|
|
if (!empty($errors)) {
|
|
|
|
nlog('Verifactu Validation Errors:');
|
|
nlog($errors);
|
|
}
|
|
|
|
$this->assertCount(0, $errors);
|
|
|
|
$this->assertNotEmpty($verifactu->getEnvelope());
|
|
|
|
$envelope = $verifactu->getEnvelope();
|
|
|
|
$this->assertNotEmpty($envelope);
|
|
|
|
// In test mode, we don't actually send to the service
|
|
// The envelope generation and validation is what we're testing
|
|
$this->assertNotEmpty($envelope);
|
|
$this->assertStringContainsString('soapenv:Envelope', $envelope);
|
|
$this->assertStringContainsString('RegFactuSistemaFacturacion', $envelope);
|
|
|
|
// Test the send method (in test mode it should return a response structure)
|
|
$response = $verifactu->send($envelope);
|
|
nlog($response);
|
|
|
|
$this->assertNotNull($response);
|
|
$this->assertArrayHasKey('success', $response);
|
|
$this->assertTrue($response['success']);
|
|
// In test mode, the response might not be successful, but the structure should be correct
|
|
|
|
$xx->forceDelete();
|
|
}
|
|
|
|
/**
|
|
* testBuildInvoiceCancellation
|
|
*
|
|
* test cancellation of an invoice and sending to AEAT
|
|
*
|
|
* @return void
|
|
*/
|
|
public function testBuildInvoiceCancellation()
|
|
{
|
|
$invoice = $this->buildData();
|
|
|
|
$invoice->number = 'TEST0033343459';
|
|
$invoice->save();
|
|
|
|
/** @var Invoice $_inv */
|
|
$_inv = Invoice::factory()->create([
|
|
'user_id' => $invoice->user_id,
|
|
'company_id' => $invoice->company_id,
|
|
'client_id' => $invoice->client_id,
|
|
'date' => '2025-08-10',
|
|
'status_id' => Invoice::STATUS_SENT,
|
|
'uses_inclusive_taxes' => false,
|
|
]);
|
|
|
|
$xx = VerifactuLog::create([
|
|
'invoice_id' => $_inv->id,
|
|
'company_id' => $invoice->company_id,
|
|
'invoice_number' => 'TEST0033343459',
|
|
'date' => '2025-08-10',
|
|
'hash' => 'CEF610A3C24D4106ABE4A836C48B0F5251600F44EEE05A90EBD7185FA753553F',
|
|
'nif' => 'A39200019',
|
|
'previous_hash' => 'CEF610A3C24D4106ABE4A836C48B0F5251600F44EEE05A90EBD7185FA753553F',
|
|
]);
|
|
|
|
$verifactu = new Verifactu($invoice);
|
|
$document = (new RegistroAlta($invoice))->run()->getInvoice();
|
|
$huella = $this->cancellationHash($document, $xx->hash);
|
|
|
|
$cancellation = $document->createCancellation();
|
|
$cancellation->setHuella($huella);
|
|
|
|
$soapXml = $cancellation->toSoapEnvelope();
|
|
|
|
$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());
|
|
|
|
$r = new ResponseProcessor();
|
|
$rx = $r->processResponse($response->body());
|
|
$this->assertTrue($rx['success']);
|
|
|
|
$xx->forceDelete();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* test_invoice_modification_validation
|
|
*
|
|
* Test that the modified invoice passes the validation rules
|
|
* @return void
|
|
*/
|
|
public function test_invoice_modification_validation()
|
|
{
|
|
|
|
$invoice = $this->buildData();
|
|
|
|
/** @var Invoice $_invoice */
|
|
$_invoice = Invoice::factory()->create([
|
|
'user_id' => $invoice->user_id,
|
|
'company_id' => $invoice->company_id,
|
|
'client_id' => $invoice->client_id,
|
|
'date' => '2025-08-10',
|
|
'status_id' => Invoice::STATUS_SENT,
|
|
'uses_inclusive_taxes' => false,
|
|
'number' => 'Replaceable Invoice #'.rand(1000000000, 9999999999),
|
|
]);
|
|
|
|
$invoice->number = 'TEST0033343460-R4';
|
|
$invoice->status_id = Invoice::STATUS_DRAFT;
|
|
$invoice->backup->parent_invoice_id = $_invoice->hashed_id;
|
|
|
|
$items = $invoice->line_items;
|
|
|
|
foreach($items as &$item) {
|
|
$item->quantity = -1;
|
|
}
|
|
|
|
$invoice->line_items = $items;
|
|
|
|
$repo = new InvoiceRepository();
|
|
$invoice = $repo->save($invoice->toArray(), $invoice);
|
|
$invoice = $invoice->service()->markSent()->save();
|
|
|
|
$previous_huella = 'E5A23515881D696FCD1CA8EE4902632BFC6D892BA8EB79CB656A5F84963079D3';
|
|
|
|
$verifactu2 = new Verifactu($invoice);
|
|
$document2 = $verifactu2->setTestMode()
|
|
->setPreviousHash($previous_huella)
|
|
->run()
|
|
->getInvoice();
|
|
|
|
$soapXml = $document2->toSoapEnvelope();
|
|
|
|
$this->assertNotNull($document2->getHuella());
|
|
|
|
nlog("huella: " . $document2->getHuella());
|
|
|
|
nlog($soapXml);
|
|
|
|
$xslt = new VerifactuDocumentValidator($soapXml);
|
|
$xslt->validate();
|
|
$errors = $xslt->getVerifactuErrors();
|
|
|
|
if (count($errors) > 0) {
|
|
nlog('Errors:');
|
|
nlog($errors);
|
|
nlog('Errors:');
|
|
}
|
|
|
|
$this->assertCount(0, $errors);
|
|
|
|
}
|
|
|
|
/**
|
|
* test_invoice_invoice_modification
|
|
* Creates a new invoice and sends to AEAT, follows with a matching credit note that is then sent to AEAT
|
|
*
|
|
* @return void
|
|
*/
|
|
public function test_invoice_invoice_modification_and_create_cancellation_of_rectification_invoice()
|
|
{
|
|
// New Invoice
|
|
$invoice = $this->buildData();
|
|
$invoice->number = 'TEST0033343460-R13';
|
|
$invoice->save();
|
|
|
|
$previous_huella = 'FDC8D47AC4BE81237A6A2FC21F854C824618805DB684F6B28053AC62AB8C86EB';
|
|
|
|
$xx = VerifactuLog::create([
|
|
'invoice_id' => $invoice->id,
|
|
'company_id' => $invoice->company_id,
|
|
'invoice_number' => 'TEST0033343460-C9',
|
|
'date' => '2025-08-10',
|
|
'hash' => $previous_huella,
|
|
'nif' => 'A39200019',
|
|
'previous_hash' => $previous_huella,
|
|
]);
|
|
|
|
$verifactu = new Verifactu($invoice);
|
|
$document = $verifactu->setTestMode()
|
|
->setPreviousHash($previous_huella)
|
|
->run()
|
|
->getInvoice();
|
|
|
|
nlog($document->toSoapEnvelope());
|
|
|
|
$response = $verifactu->send($document->toSoapEnvelope());
|
|
|
|
$this->assertNotNull($response);
|
|
$this->assertArrayHasKey('success', $response);
|
|
$this->assertTrue($response['success']);
|
|
|
|
// Credit Note
|
|
$invoice2 = $invoice->replicate();
|
|
$invoice2->number = 'TEST0033343460-C10';
|
|
$invoice2->status_id = Invoice::STATUS_DRAFT;
|
|
$invoice2->backup->parent_invoice_id = $invoice->hashed_id;
|
|
$invoice2->backup->document_type = 'R2';
|
|
$items = $invoice2->line_items;
|
|
|
|
foreach($items as &$item) {
|
|
$item->quantity = -1;
|
|
}
|
|
|
|
$invoice2->line_items = $items;
|
|
|
|
$invoice2->save();
|
|
|
|
$data = $invoice2->toArray();
|
|
$data['client_id'] = $invoice->client_id;
|
|
unset($data['id']);
|
|
|
|
$repo = new InvoiceRepository();
|
|
$invoice2 = $repo->save($data, $invoice2);
|
|
$invoice2 = $invoice2->service()->markSent()->save();
|
|
|
|
$this->assertEquals(-121, $invoice2->amount);
|
|
|
|
$verifactu2 = new Verifactu($invoice2);
|
|
$document2 = $verifactu2->setTestMode()
|
|
->setPreviousHash($document->getHuella())
|
|
->run()
|
|
->getInvoice();
|
|
|
|
nlog($document2->toSoapEnvelope());
|
|
|
|
$response = $verifactu2->send($document2->toSoapEnvelope());
|
|
|
|
$this->assertNotNull($response);
|
|
$this->assertArrayHasKey('success', $response);
|
|
$this->assertTrue($response['success']);
|
|
|
|
//Lets try and cancel the credit note now - we should fail!!
|
|
$verifactu = new Verifactu($invoice2);
|
|
$document = (new RegistroAlta($invoice2))->run()->getInvoice();
|
|
$huella = $this->cancellationHash($document, $document2->getHuella());
|
|
|
|
$cancellation = $document->createCancellation();
|
|
$cancellation->setHuella($huella);
|
|
|
|
$soapXml = $cancellation->toSoapEnvelope();
|
|
|
|
nlog($soapXml);
|
|
|
|
$response = $verifactu->setTestMode()
|
|
->setInvoice($document)
|
|
->setHuella($huella)
|
|
->setPreviousHash($document2->getHuella())
|
|
->send($soapXml);
|
|
|
|
nlog("CANCELLATION RESPONSE");
|
|
nlog($response);
|
|
|
|
$this->assertNotNull($response);
|
|
$this->assertArrayHasKey('success', $response);
|
|
$this->assertTrue($response['success']);
|
|
|
|
$xx->forceDelete();
|
|
|
|
VerifactuLog::query()->where('id', $invoice2->id)->forceDelete();
|
|
VerifactuLog::query()->where('id', $invoice->id)->forceDelete();
|
|
|
|
}
|
|
|
|
public function test_rectification_invoice()
|
|
{
|
|
$soapXml = <<<XML
|
|
<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>
|
|
<sum1:ObligadoEmision>
|
|
<sum1:NombreRazon>CERTIFICADO FISICA PRUEBAS</sum1:NombreRazon>
|
|
<sum1:NIF>A39200019</sum1:NIF>
|
|
</sum1:ObligadoEmision>
|
|
</sum:Cabecera>
|
|
<sum:RegistroFactura>
|
|
<sum1:RegistroAlta>
|
|
<sum1:IDVersion>1.0</sum1:IDVersion>
|
|
<sum1:IDFactura>
|
|
<sum1:IDEmisorFactura>A39200019</sum1:IDEmisorFactura>
|
|
<sum1:NumSerieFactura>TEST0033343460-R1</sum1:NumSerieFactura>
|
|
<sum1:FechaExpedicionFactura>10-08-2025</sum1:FechaExpedicionFactura>
|
|
</sum1:IDFactura>
|
|
<sum1:NombreRazonEmisor>CERTIFICADO FISICA PRUEBAS</sum1:NombreRazonEmisor>
|
|
<sum1:TipoFactura>F3</sum1:TipoFactura>
|
|
<sum1:FacturasSustituidas>
|
|
<sum1:IDFacturaSustituida>
|
|
<sum1:IDEmisorFactura>A39200019</sum1:IDEmisorFactura>
|
|
<sum1:NumSerieFactura>TEST0033343460</sum1:NumSerieFactura>
|
|
<sum1:FechaExpedicionFactura>10-08-2025</sum1:FechaExpedicionFactura>
|
|
</sum1:IDFacturaSustituida>
|
|
</sum1:FacturasSustituidas>
|
|
<sum1:DescripcionOperacion>Alta</sum1:DescripcionOperacion>
|
|
<sum1:Destinatarios>
|
|
<sum1:IDDestinatario>
|
|
<sum1:NombreRazon>CERTIFICADO FISICA PRUEBAS</sum1:NombreRazon>
|
|
<sum1:NIF>A39200019</sum1:NIF>
|
|
</sum1:IDDestinatario>
|
|
</sum1:Destinatarios>
|
|
<sum1:Desglose>
|
|
<sum1:DetalleDesglose>
|
|
<sum1:Impuesto>01</sum1:Impuesto>
|
|
<sum1:ClaveRegimen>01</sum1:ClaveRegimen>
|
|
<sum1:CalificacionOperacion>S1</sum1:CalificacionOperacion>
|
|
<sum1:TipoImpositivo>21.00</sum1:TipoImpositivo>
|
|
<sum1:BaseImponibleOimporteNoSujeto>100.00</sum1:BaseImponibleOimporteNoSujeto>
|
|
<sum1:CuotaRepercutida>21.00</sum1:CuotaRepercutida>
|
|
</sum1:DetalleDesglose>
|
|
</sum1:Desglose>
|
|
<sum1:CuotaTotal>21</sum1:CuotaTotal>
|
|
<sum1:ImporteTotal>121</sum1:ImporteTotal>
|
|
<sum1:Encadenamiento>
|
|
<sum1:RegistroAnterior>
|
|
<sum1:IDEmisorFactura>A39200019</sum1:IDEmisorFactura>
|
|
<sum1:NumSerieFactura>TEST0033343459</sum1:NumSerieFactura>
|
|
<sum1:FechaExpedicionFactura>10-08-2025</sum1:FechaExpedicionFactura>
|
|
<sum1:Huella>1FB6B4EF72DD2A07CC23B3F9D74EE5749C8E86B34B9B1DFFFC8C3E46ACA87E21</sum1:Huella>
|
|
</sum1:RegistroAnterior>
|
|
</sum1:Encadenamiento>
|
|
<sum1:SistemaInformatico>
|
|
<sum1:NombreRazon>CERTIFICADO FISICA PRUEBAS</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>2025-08-10T05:02:18+00:00</sum1:FechaHoraHusoGenRegistro>
|
|
<sum1:TipoHuella>01</sum1:TipoHuella>
|
|
<sum1:Huella>BC61C7CB7CB09917C076CAE7D066B3E2CF521A3B8B501D0C83250B5EB4A4B40D</sum1:Huella>
|
|
</sum1:RegistroAlta>
|
|
</sum:RegistroFactura>
|
|
</sum:RegFactuSistemaFacturacion>
|
|
</soapenv:Body>
|
|
</soapenv:Envelope>
|
|
|
|
XML;
|
|
|
|
|
|
$xslt = new VerifactuDocumentValidator($soapXml);
|
|
$xslt->validate();
|
|
$errors = $xslt->getVerifactuErrors();
|
|
|
|
if(count($errors) > 0) {
|
|
nlog('Errors:');
|
|
nlog($errors);
|
|
nlog('Errors:');
|
|
}
|
|
|
|
$this->assertCount(0, $errors);
|
|
|
|
$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());
|
|
|
|
$r = new ResponseProcessor();
|
|
$rx = $r->processResponse($response->body());
|
|
|
|
nlog($rx);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Test that R1 invoice XML structure is exactly as expected with proper element order
|
|
*/
|
|
public function test_r1_invoice_xml_structure_exact_match(): void
|
|
{
|
|
// Create a complete R1 invoice with all required elements matching the exact XML structure
|
|
$invoice = new VerifactuInvoice();
|
|
|
|
// Set required properties using setter methods to match the expected XML exactly
|
|
$invoice->setIdVersion('1.0');
|
|
|
|
$idFactura = new IDFactura();
|
|
$idFactura->setIdEmisorFactura('A39200019');
|
|
$idFactura->setNumSerieFactura('TEST0033343444');
|
|
$idFactura->setFechaExpedicionFactura('09-08-2025');
|
|
$invoice->setIdFactura($idFactura);
|
|
|
|
$invoice->setNombreRazonEmisor('CERTIFICADO FISICA PRUEBAS');
|
|
$invoice->setTipoFactura(VerifactuInvoice::TIPO_FACTURA_RECTIFICATIVA);
|
|
$invoice->setTipoRectificativa('S');
|
|
$invoice->setDescripcionOperacion('Rectificación por error en factura anterior');
|
|
$invoice->setCuotaTotal(47.05);
|
|
$invoice->setImporteTotal(144.05);
|
|
$invoice->setFechaHoraHusoGenRegistro('2025-08-09T23:18:44+02:00');
|
|
$invoice->setTipoHuella('01');
|
|
$invoice->setHuella('E7558C33FE3496551F38FEB582F4879B1D9F6C073489628A8DC275E12298941F');
|
|
|
|
// Set up rectification details exactly as in the expected XML
|
|
$invoice->setRectifiedInvoice('A39200019', 'TEST0033343443', '09-08-2025');
|
|
|
|
|
|
$importeRectificacion = [
|
|
'BaseRectificada' => 100.00,
|
|
'CuotaRectificada' => 21.00,
|
|
'CuotaRecargoRectificado' => 0.00
|
|
];
|
|
|
|
$invoice->setRectificationAmounts($importeRectificacion);
|
|
|
|
// Set up desglose exactly as in the expected XML
|
|
$desglose = new Desglose();
|
|
$desglose->setDesgloseFactura([
|
|
'Impuesto' => '01',
|
|
'ClaveRegimen' => '01',
|
|
'CalificacionOperacion' => 'S1',
|
|
'TipoImpositivo' => 21.00,
|
|
'BaseImponible' => 97.00,
|
|
'Cuota' => 20.37
|
|
]);
|
|
$invoice->setDesglose($desglose);
|
|
|
|
// Generate SOAP envelope XML
|
|
$soapXml = $invoice->toSoapEnvelope();
|
|
|
|
// Verify the XML structure exactly matches the expected format
|
|
$this->assertStringContainsString('<?xml version="1.0" encoding="UTF-8"?>', $soapXml);
|
|
$this->assertStringContainsString('<soapenv:Envelope', $soapXml);
|
|
$this->assertStringContainsString('xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"', $soapXml);
|
|
$this->assertStringContainsString('xmlns:sum="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroLR.xsd"', $soapXml);
|
|
$this->assertStringContainsString('xmlns:sum1="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd"', $soapXml);
|
|
|
|
// Verify SOAP structure
|
|
$this->assertStringContainsString('<soapenv:Header/>', $soapXml);
|
|
$this->assertStringContainsString('<soapenv:Body>', $soapXml);
|
|
$this->assertStringContainsString('<sum:RegFactuSistemaFacturacion>', $soapXml);
|
|
$this->assertStringContainsString('<sum:Cabecera>', $soapXml);
|
|
$this->assertStringContainsString('<sum1:ObligadoEmision>', $soapXml);
|
|
$this->assertStringContainsString('<sum:RegistroFactura>', $soapXml);
|
|
$this->assertStringContainsString('<sum1:RegistroAlta>', $soapXml);
|
|
|
|
// Verify elements are in exact order as per the expected XML
|
|
$expectedOrder = [
|
|
'IDVersion',
|
|
'IDFactura',
|
|
'NombreRazonEmisor',
|
|
'TipoFactura',
|
|
'TipoRectificativa',
|
|
'FacturasRectificadas',
|
|
'ImporteRectificacion',
|
|
'DescripcionOperacion',
|
|
'Destinatarios',
|
|
'Desglose',
|
|
'CuotaTotal',
|
|
'ImporteTotal',
|
|
'Encadenamiento',
|
|
'SistemaInformatico',
|
|
'FechaHoraHusoGenRegistro',
|
|
'TipoHuella',
|
|
'Huella'
|
|
];
|
|
|
|
$xmlLines = explode("\n", $soapXml);
|
|
$currentIndex = 0;
|
|
|
|
foreach ($expectedOrder as $elementName) {
|
|
$found = false;
|
|
for ($i = $currentIndex; $i < count($xmlLines); $i++) {
|
|
if (strpos($xmlLines[$i], "<sum1:{$elementName}") !== false || strpos($xmlLines[$i], "</sum1:{$elementName}") !== false) {
|
|
$currentIndex = $i;
|
|
$found = true;
|
|
break;
|
|
}
|
|
}
|
|
$this->assertTrue($found, "Element {$elementName} not found in expected order");
|
|
}
|
|
|
|
// Verify specific structure for FacturasRectificadas
|
|
$this->assertStringContainsString('<sum1:FacturasRectificadas>', $soapXml);
|
|
$this->assertStringContainsString('<sum1:IDFacturaRectificada>', $soapXml);
|
|
$this->assertStringContainsString('<sum1:IDEmisorFactura>A39200019</sum1:IDEmisorFactura>', $soapXml);
|
|
$this->assertStringContainsString('<sum1:NumSerieFactura>TEST0033343443</sum1:NumSerieFactura>', $soapXml);
|
|
$this->assertStringContainsString('<sum1:FechaExpedicionFactura>09-08-2025</sum1:FechaExpedicionFactura>', $soapXml);
|
|
$this->assertStringContainsString('</sum1:IDFacturaRectificada>', $soapXml);
|
|
$this->assertStringContainsString('</sum1:FacturasRectificadas>', $soapXml);
|
|
|
|
// Verify ImporteRectificacion structure
|
|
$this->assertStringContainsString('<sum1:ImporteRectificacion>', $soapXml);
|
|
$this->assertStringContainsString('<sum1:BaseRectificada>100.00</sum1:BaseRectificada>', $soapXml);
|
|
$this->assertStringContainsString('<sum1:CuotaRectificada>21.00</sum1:CuotaRectificada>', $soapXml);
|
|
$this->assertStringContainsString('<sum1:CuotaRecargoRectificado>0.00</sum1:CuotaRecargoRectificado>', $soapXml);
|
|
$this->assertStringContainsString('</sum1:ImporteRectificacion>', $soapXml);
|
|
|
|
// Verify Destinatarios structure
|
|
$this->assertStringContainsString('<sum1:Destinatarios>', $soapXml);
|
|
$this->assertStringContainsString('<sum1:IDDestinatario>', $soapXml);
|
|
$this->assertStringContainsString('<sum1:NombreRazon>Test Recipient Company</sum1:NombreRazon>', $soapXml);
|
|
$this->assertStringContainsString('<sum1:NIF>A39200019</sum1:NIF>', $soapXml);
|
|
$this->assertStringContainsString('</sum1:IDDestinatario>', $soapXml);
|
|
$this->assertStringContainsString('</sum1:Destinatarios>', $soapXml);
|
|
|
|
// Verify Desglose structure
|
|
$this->assertStringContainsString('<sum1:Desglose>', $soapXml);
|
|
$this->assertStringContainsString('<sum1:DetalleDesglose>', $soapXml);
|
|
$this->assertStringContainsString('<sum1:Impuesto>01</sum1:Impuesto>', $soapXml);
|
|
$this->assertStringContainsString('<sum1:ClaveRegimen>01</sum1:ClaveRegimen>', $soapXml);
|
|
$this->assertStringContainsString('<sum1:CalificacionOperacion>S1</sum1:CalificacionOperacion>', $soapXml);
|
|
$this->assertStringContainsString('<sum1:TipoImpositivo>21.00</sum1:TipoImpositivo>', $soapXml);
|
|
$this->assertStringContainsString('<sum1:BaseImponibleOimporteNoSujeto>97.00</sum1:BaseImponibleOimporteNoSujeto>', $soapXml);
|
|
$this->assertStringContainsString('<sum1:CuotaRepercutida>20.37</sum1:CuotaRepercutida>', $soapXml);
|
|
$this->assertStringContainsString('</sum1:DetalleDesglose>', $soapXml);
|
|
$this->assertStringContainsString('</sum1:Desglose>', $soapXml);
|
|
|
|
// Verify Encadenamiento structure
|
|
$this->assertStringContainsString('<sum1:Encadenamiento>', $soapXml);
|
|
$this->assertStringContainsString('<sum1:PrimerRegistro>S</sum1:PrimerRegistro>', $soapXml);
|
|
$this->assertStringContainsString('</sum1:Encadenamiento>', $soapXml);
|
|
|
|
// Verify SistemaInformatico structure
|
|
$this->assertStringContainsString('<sum1:SistemaInformatico>', $soapXml);
|
|
$this->assertStringContainsString('<sum1:NombreRazon>CERTIFICADO FISICA PRUEBAS</sum1:NombreRazon>', $soapXml);
|
|
$this->assertStringContainsString('<sum1:NIF>A39200019</sum1:NIF>', $soapXml);
|
|
$this->assertStringContainsString('<sum1:NombreSistemaInformatico>InvoiceNinja</sum1:NombreSistemaInformatico>', $soapXml);
|
|
$this->assertStringContainsString('<sum1:IdSistemaInformatico>77</sum1:IdSistemaInformatico>', $soapXml);
|
|
$this->assertStringContainsString('<sum1:Version>1.0.03</sum1:Version>', $soapXml);
|
|
$this->assertStringContainsString('<sum1:NumeroInstalacion>383</sum1:NumeroInstalacion>', $soapXml);
|
|
$this->assertStringContainsString('<sum1:TipoUsoPosibleSoloVerifactu>N</sum1:TipoUsoPosibleSoloVerifactu>', $soapXml);
|
|
$this->assertStringContainsString('<sum1:TipoUsoPosibleMultiOT>S</sum1:TipoUsoPosibleMultiOT>', $soapXml);
|
|
$this->assertStringContainsString('<sum1:IndicadorMultiplesOT>S</sum1:IndicadorMultiplesOT>', $soapXml);
|
|
$this->assertStringContainsString('</sum1:SistemaInformatico>', $soapXml);
|
|
|
|
// Verify closing tags
|
|
$this->assertStringContainsString('</sum1:RegistroAlta>', $soapXml);
|
|
$this->assertStringContainsString('</sum:RegistroFactura>', $soapXml);
|
|
$this->assertStringContainsString('</sum:RegFactuSistemaFacturacion>', $soapXml);
|
|
$this->assertStringContainsString('</soapenv:Body>', $soapXml);
|
|
$this->assertStringContainsString('</soapenv:Envelope>', $soapXml);
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////
|
|
private function cancellationHash($document, $huella)
|
|
{
|
|
|
|
$idEmisorFacturaAnulada = $document->getIdFactura()->getIdEmisorFactura();
|
|
$numSerieFacturaAnulada = $document->getIdFactura()->getNumSerieFactura();
|
|
$fechaExpedicionFacturaAnulada = $document->getIdFactura()->getFechaExpedicionFactura();
|
|
$fechaHoraHusoGenRegistro = $document->getFechaHoraHusoGenRegistro();
|
|
|
|
$hashInput = "IDEmisorFacturaAnulada={$idEmisorFacturaAnulada}&" .
|
|
"NumSerieFacturaAnulada={$numSerieFacturaAnulada}&" .
|
|
"FechaExpedicionFacturaAnulada={$fechaExpedicionFacturaAnulada}&" .
|
|
"Huella={$huella}&" .
|
|
"FechaHoraHusoGenRegistro={$fechaHoraHusoGenRegistro}";
|
|
|
|
nlog("Cancellation Huella: " . $hashInput);
|
|
|
|
return strtoupper(hash('sha256', $hashInput));
|
|
|
|
}
|
|
} |