From f662880b316ed02120436a47204ae7e83846ae21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Palacios?= Date: Sun, 19 Oct 2025 14:14:21 +0200 Subject: [PATCH 1/7] added voluntary discontinuation of VERI*FACTU registries for a company --- .../Records/VoluntaryDiscontinuation.php | 48 ++++++++++++++ src/Services/AeatClient.php | 9 +++ .../Records/VoluntaryDiscontinuationTest.php | 64 +++++++++++++++++++ 3 files changed, 121 insertions(+) create mode 100644 src/Models/Records/VoluntaryDiscontinuation.php create mode 100644 tests/Models/Records/VoluntaryDiscontinuationTest.php diff --git a/src/Models/Records/VoluntaryDiscontinuation.php b/src/Models/Records/VoluntaryDiscontinuation.php new file mode 100644 index 0000000..26e3fac --- /dev/null +++ b/src/Models/Records/VoluntaryDiscontinuation.php @@ -0,0 +1,48 @@ +endDate = $endDate; + } + if ($incident !== null) { + $this->incident = $incident; + } + } + + /** + * Fecha fin de VERI*FACTU. Debe ser a finales del año contable actual. + * + * @field Caberecera/RemisionVoluntaria/FechaFinVeriFactu + */ + #[Assert\NotBlank] + public ?DateTimeImmutable $endDate = null; + + /** + * Incidencia + * + * @field Caberecera/RemisionVoluntaria/Incidencia + */ + #[Assert\Type('boolean')] + public bool $incident = false; +} diff --git a/src/Services/AeatClient.php b/src/Services/AeatClient.php index dc59546..a193ee5 100644 --- a/src/Services/AeatClient.php +++ b/src/Services/AeatClient.php @@ -9,6 +9,7 @@ use josemmo\Verifactu\Models\Records\CancellationRecord; use josemmo\Verifactu\Models\Records\FiscalIdentifier; use josemmo\Verifactu\Models\Records\RegistrationRecord; +use josemmo\Verifactu\Models\Records\VoluntaryDiscontinuation; use josemmo\Verifactu\Models\Responses\AeatResponse; use Psr\Http\Message\ResponseInterface; use SensitiveParameter; @@ -28,6 +29,7 @@ class AeatClient { private ?string $certificatePath = null; private ?string $certificatePassword = null; private ?FiscalIdentifier $representative = null; + private ?VoluntaryDiscontinuation $voluntaryDiscontinuation = null; private bool $isProduction = true; /** @@ -41,10 +43,12 @@ public function __construct( ComputerSystem $system, FiscalIdentifier $taxpayer, ?Client $httpClient = null, + ?VoluntaryDiscontinuation $voluntaryDiscontinuation = null, ) { $this->system = $system; $this->taxpayer = $taxpayer; $this->client = $httpClient ?? new Client(); + $this->voluntaryDiscontinuation = $voluntaryDiscontinuation; } /** @@ -122,6 +126,11 @@ public function send(array $records): PromiseInterface { /** @phpstan-ignore gen $representanteElement->add('sum1:NombreRazon', $this->representative->name); $representanteElement->add('sum1:NIF', $this->representative->nif); } + if($this->voluntaryDiscontinuation !== null && $this->voluntaryDiscontinuation->endDate !== null) { + $remisionVoluntariaElement = $cabeceraElement->add('sum1:RemisionVoluntaria'); + $remisionVoluntariaElement->add('sum1:FechaFinVeriFactu', $this->voluntaryDiscontinuation->endDate->format('d-m-Y')); + $remisionVoluntariaElement->add('sum1:Incidencia', $this->voluntaryDiscontinuation->incident ? 'S' : 'N'); + } // Add registration records foreach ($records as $record) { diff --git a/tests/Models/Records/VoluntaryDiscontinuationTest.php b/tests/Models/Records/VoluntaryDiscontinuationTest.php new file mode 100644 index 0000000..71e236c --- /dev/null +++ b/tests/Models/Records/VoluntaryDiscontinuationTest.php @@ -0,0 +1,64 @@ +assertNull($discontinuation->endDate); + $this->assertFalse($discontinuation->incident); + } + + public function testConstructorWithValues(): void { + $endDate = new DateTimeImmutable('2024-12-31'); + $discontinuation = new VoluntaryDiscontinuation($endDate, true); + $this->assertEquals($endDate, $discontinuation->endDate); + $this->assertTrue($discontinuation->incident); + } + + public function testConstructorWithNullValues(): void { + $discontinuation = new VoluntaryDiscontinuation(null, null); + $this->assertNull($discontinuation->endDate); + $this->assertFalse($discontinuation->incident); + } + + public function testValidationRequiresEndDate(): void { + $discontinuation = new VoluntaryDiscontinuation(); + $discontinuation->incident = false; + + try { + $discontinuation->validate(); + $this->fail('Did not throw exception for missing end date'); + } catch (InvalidModelException $e) { + $this->assertStringContainsString('endDate', $e->getMessage()); + } + } + + public function testValidationAcceptsValidData(): void { + $discontinuation = new VoluntaryDiscontinuation(); + $discontinuation->endDate = new DateTimeImmutable('2024-12-31'); + $discontinuation->incident = true; + + // Should not throw exception + $discontinuation->validate(); + $this->assertTrue(true); + } + + public function testValidationAcceptsBooleanIncident(): void { + $discontinuation = new VoluntaryDiscontinuation(); + $discontinuation->endDate = new DateTimeImmutable('2024-12-31'); + $discontinuation->incident = false; + + // Should not throw exception + $discontinuation->validate(); + $this->assertFalse($discontinuation->incident); + + $discontinuation->incident = true; + $discontinuation->validate(); + $this->assertTrue($discontinuation->incident); + } +} From 15bcce9c0daf79567f8bfd6a9259ebc1615444ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Palacios?= Date: Sun, 19 Oct 2025 14:19:56 +0200 Subject: [PATCH 2/7] updated README --- README.md | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/README.md b/README.md index bbd31cb..124d204 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ use josemmo\Verifactu\Models\Records\OperationType; use josemmo\Verifactu\Models\Records\RegimeType; use josemmo\Verifactu\Models\Records\RegistrationRecord; use josemmo\Verifactu\Models\Records\TaxType; +use josemmo\Verifactu\Models\Records\VoluntaryDiscontinuation; use josemmo\Verifactu\Services\AeatClient; require __DIR__ . '/vendor/autoload.php'; @@ -80,6 +81,44 @@ $res = $client->send([$record])->wait(); echo $res->asXML() . "\n"; ``` +## Remisión Voluntaria (Voluntary Discontinuation) + +La librería también soporta la funcionalidad de **Remisión Voluntaria** que permite notificar a la AEAT cuando una empresa decide dejar de usar VERI*FACTU. + +### Ejemplo de uso de Remisión Voluntaria + +```php +endDate = new DateTimeImmutable('2025-12-31'); // Debe ser a finales del año contable +$discontinuation->incident = false; // true si el motivo del cese voluntario es por una incidencia +$discontinuation->validate(); + +// Configura el cliente con la remisión voluntaria +$client = new AeatClient($system, $taxpayer, null, $discontinuation); +$client->setCertificate(__DIR__ . '/certificado.pfx', 'contraseña'); +$client->setProduction(false); + +// Envía la remisión (no se requieren registros de facturación) +$res = $client->send([])->wait(); +echo $res->asXML() . "\n"; +``` + +### Parámetros de Remisión Voluntaria + +- **`endDate`**: Fecha en la que la empresa dejará de usar VERI*FACTU. **Debe ser a finales del año contable actual** según indica la AEAT. +- **`incident`**: Indica si la razón de la remisión es una incidencia (`true`) o es voluntaria (`false`). + +### Notas importantes + +- La remisión voluntaria debe enviarse **antes** de la fecha de fin especificada +- Una vez llega la fecha de remisión, el SIF no debería de volver a enviar facturas a VERI*FACTU a no ser que el cliente vuelva a activarlo de forma voluntaria. +- La fecha de fin debe ser a finales del año contable actual (31 de diciembre) + ## Exención de responsabilidad Esta librería se proporciona sin una declaración responsable al no ser un Sistema Informático de Facturación (SIF). Verifactu-PHP es una herramienta para crear SIFs, es tu responsabilidad auditar su código y usarlo de acuerdo a la normativa vigente. From d1c0e92702b444abef30a25232233aae028a989e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Palacios?= Date: Sun, 19 Oct 2025 14:32:50 +0200 Subject: [PATCH 3/7] ran pint --- src/Models/Records/VoluntaryDiscontinuation.php | 2 +- src/Services/AeatClient.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Models/Records/VoluntaryDiscontinuation.php b/src/Models/Records/VoluntaryDiscontinuation.php index 26e3fac..52357a1 100644 --- a/src/Models/Records/VoluntaryDiscontinuation.php +++ b/src/Models/Records/VoluntaryDiscontinuation.php @@ -15,7 +15,7 @@ class VoluntaryDiscontinuation extends Model { /** * Class constructor * - * @param DateTimeImmutable|null $endDate When the company will stop using VERI*FACTU. As the AEAT says, this date should be at the end of the year. + * @param DateTimeImmutable|null $endDate When the company will stop using VERI*FACTU. As the AEAT says, this date should be at the end of the year. * @param bool|null $incident Whether the reason of the voluntary discontinuation is an incident or not (Default: false) */ public function __construct( diff --git a/src/Services/AeatClient.php b/src/Services/AeatClient.php index a193ee5..56d301d 100644 --- a/src/Services/AeatClient.php +++ b/src/Services/AeatClient.php @@ -126,7 +126,7 @@ public function send(array $records): PromiseInterface { /** @phpstan-ignore gen $representanteElement->add('sum1:NombreRazon', $this->representative->name); $representanteElement->add('sum1:NIF', $this->representative->nif); } - if($this->voluntaryDiscontinuation !== null && $this->voluntaryDiscontinuation->endDate !== null) { + if ($this->voluntaryDiscontinuation !== null && $this->voluntaryDiscontinuation->endDate !== null) { $remisionVoluntariaElement = $cabeceraElement->add('sum1:RemisionVoluntaria'); $remisionVoluntariaElement->add('sum1:FechaFinVeriFactu', $this->voluntaryDiscontinuation->endDate->format('d-m-Y')); $remisionVoluntariaElement->add('sum1:Incidencia', $this->voluntaryDiscontinuation->incident ? 'S' : 'N'); From a4d79a863bd7b7f2b3b8364e36481333968215fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Palacios?= Date: Sun, 19 Oct 2025 14:34:47 +0200 Subject: [PATCH 4/7] fixed test --- tests/Models/Records/VoluntaryDiscontinuationTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/Models/Records/VoluntaryDiscontinuationTest.php b/tests/Models/Records/VoluntaryDiscontinuationTest.php index 71e236c..21e6854 100644 --- a/tests/Models/Records/VoluntaryDiscontinuationTest.php +++ b/tests/Models/Records/VoluntaryDiscontinuationTest.php @@ -45,7 +45,8 @@ public function testValidationAcceptsValidData(): void { // Should not throw exception $discontinuation->validate(); - $this->assertTrue(true); + $this->assertEquals(new DateTimeImmutable('2024-12-31'), $discontinuation->endDate); + $this->assertTrue($discontinuation->incident); } public function testValidationAcceptsBooleanIncident(): void { From de619c6fc4b555a6017d91f038077423618ec81c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Palacios?= Date: Sat, 6 Dec 2025 10:20:06 +0100 Subject: [PATCH 5/7] changed the way the voluntary discontinuation is set --- src/Services/AeatClient.php | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/Services/AeatClient.php b/src/Services/AeatClient.php index 56d301d..be49b05 100644 --- a/src/Services/AeatClient.php +++ b/src/Services/AeatClient.php @@ -42,13 +42,11 @@ class AeatClient { public function __construct( ComputerSystem $system, FiscalIdentifier $taxpayer, - ?Client $httpClient = null, - ?VoluntaryDiscontinuation $voluntaryDiscontinuation = null, + ?Client $httpClient = null ) { $this->system = $system; $this->taxpayer = $taxpayer; $this->client = $httpClient ?? new Client(); - $this->voluntaryDiscontinuation = $voluntaryDiscontinuation; } /** @@ -96,6 +94,18 @@ public function setProduction(bool $production): static { return $this; } + /** + * Set voluntary discontinuation + * + * @param VoluntaryDiscontinuation|null $voluntaryDiscontinuation Sends the date of which the company will stop using VERI*FACTU. + * + * @return $this This instance + */ + public function setVoluntaryDiscontinuation(?VoluntaryDiscontinuation $voluntaryDiscontinuation): static { + $this->voluntaryDiscontinuation = $voluntaryDiscontinuation; + return $this; + } + /** * Send invoicing records * From 7b9cb9f37baa336a99196e7b2596f01c4fb499b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Palacios?= Date: Sat, 6 Dec 2025 10:22:20 +0100 Subject: [PATCH 6/7] removed tests --- .../Records/VoluntaryDiscontinuationTest.php | 65 ------------------- 1 file changed, 65 deletions(-) delete mode 100644 tests/Models/Records/VoluntaryDiscontinuationTest.php diff --git a/tests/Models/Records/VoluntaryDiscontinuationTest.php b/tests/Models/Records/VoluntaryDiscontinuationTest.php deleted file mode 100644 index 21e6854..0000000 --- a/tests/Models/Records/VoluntaryDiscontinuationTest.php +++ /dev/null @@ -1,65 +0,0 @@ -assertNull($discontinuation->endDate); - $this->assertFalse($discontinuation->incident); - } - - public function testConstructorWithValues(): void { - $endDate = new DateTimeImmutable('2024-12-31'); - $discontinuation = new VoluntaryDiscontinuation($endDate, true); - $this->assertEquals($endDate, $discontinuation->endDate); - $this->assertTrue($discontinuation->incident); - } - - public function testConstructorWithNullValues(): void { - $discontinuation = new VoluntaryDiscontinuation(null, null); - $this->assertNull($discontinuation->endDate); - $this->assertFalse($discontinuation->incident); - } - - public function testValidationRequiresEndDate(): void { - $discontinuation = new VoluntaryDiscontinuation(); - $discontinuation->incident = false; - - try { - $discontinuation->validate(); - $this->fail('Did not throw exception for missing end date'); - } catch (InvalidModelException $e) { - $this->assertStringContainsString('endDate', $e->getMessage()); - } - } - - public function testValidationAcceptsValidData(): void { - $discontinuation = new VoluntaryDiscontinuation(); - $discontinuation->endDate = new DateTimeImmutable('2024-12-31'); - $discontinuation->incident = true; - - // Should not throw exception - $discontinuation->validate(); - $this->assertEquals(new DateTimeImmutable('2024-12-31'), $discontinuation->endDate); - $this->assertTrue($discontinuation->incident); - } - - public function testValidationAcceptsBooleanIncident(): void { - $discontinuation = new VoluntaryDiscontinuation(); - $discontinuation->endDate = new DateTimeImmutable('2024-12-31'); - $discontinuation->incident = false; - - // Should not throw exception - $discontinuation->validate(); - $this->assertFalse($discontinuation->incident); - - $discontinuation->incident = true; - $discontinuation->validate(); - $this->assertTrue($discontinuation->incident); - } -} From d3e73ff0bbb66c799cde9a78e14d7995ec884ed7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Palacios?= Date: Sat, 31 Jan 2026 11:06:46 +0100 Subject: [PATCH 7/7] removed not blank attribute --- src/Models/Records/VoluntaryDiscontinuation.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Models/Records/VoluntaryDiscontinuation.php b/src/Models/Records/VoluntaryDiscontinuation.php index 52357a1..6067517 100644 --- a/src/Models/Records/VoluntaryDiscontinuation.php +++ b/src/Models/Records/VoluntaryDiscontinuation.php @@ -35,7 +35,6 @@ public function __construct( * * @field Caberecera/RemisionVoluntaria/FechaFinVeriFactu */ - #[Assert\NotBlank] public ?DateTimeImmutable $endDate = null; /**