diff --git a/src/Models/Records/IssuedBy.php b/src/Models/Records/IssuedBy.php new file mode 100644 index 0000000..ac69723 --- /dev/null +++ b/src/Models/Records/IssuedBy.php @@ -0,0 +1,15 @@ +issuedBy === IssuedBy::ThirdParty && $this->thirdParty === null) { + $context->buildViolation('An invoice issued by a third party requires the third party') + ->atPath('thirdParty') + ->addViolation(); + } + if ($this->issuedBy !== IssuedBy::ThirdParty && $this->thirdParty !== null) { + $context->buildViolation('Only an invoice issued by a third party can carry the third party') + ->atPath('thirdParty') + ->addViolation(); + } + if ($this->issuedBy === IssuedBy::Recipient && count($this->recipients) === 0) { + $context->buildViolation('A self-billed invoice requires its recipient, who issues it') + ->atPath('recipients') + ->addViolation(); + } + } + #[Assert\Callback] final public function validateRecipients(ExecutionContextInterface $context): void { if (!isset($this->invoiceType)) { @@ -418,6 +452,19 @@ protected function importCustomProperties(UXML $recordElement): void { } $this->description = $description; + // Issued by a third party or by the recipient + $rawIssuedBy = $recordElement->get('sum1:EmitidaPorTerceroODestinatario')?->asText(); + if ($rawIssuedBy !== null) { + $this->issuedBy = IssuedBy::tryFrom($rawIssuedBy) ?? throw new ImportException('Invalid value'); + } + $terceroElement = $recordElement->get('sum1:Tercero'); + if ($terceroElement !== null) { + $this->thirdParty = new FiscalIdentifier( + $terceroElement->get('sum1:NombreRazon')?->asText() ?? throw new ImportException('Missing from element'), + $terceroElement->get('sum1:NIF')?->asText() ?? throw new ImportException('Missing from element'), + ); + } + // Recipients foreach ($recordElement->getAll('sum1:Destinatarios/sum1:IDDestinatario') as $destinatarioElement) { $recipientName = $destinatarioElement->get('sum1:NombreRazon')?->asText(); @@ -526,6 +573,16 @@ protected function exportCustomProperties(UXML $recordElement): void { // Description $recordElement->add('sum1:DescripcionOperacion', $this->description); + // Issued by a third party or by the recipient + if ($this->issuedBy !== null) { + $recordElement->add('sum1:EmitidaPorTerceroODestinatario', $this->issuedBy->value); + } + if ($this->thirdParty !== null) { + $terceroElement = $recordElement->add('sum1:Tercero'); + $terceroElement->add('sum1:NombreRazon', $this->thirdParty->name); + $terceroElement->add('sum1:NIF', $this->thirdParty->nif); + } + // Recipients if (count($this->recipients) > 0) { $destinatariosElement = $recordElement->add('sum1:Destinatarios'); diff --git a/tests/Models/Records/RegistrationRecordTest.php b/tests/Models/Records/RegistrationRecordTest.php index 1e789e9..164e9d3 100644 --- a/tests/Models/Records/RegistrationRecordTest.php +++ b/tests/Models/Records/RegistrationRecordTest.php @@ -11,6 +11,7 @@ use Komma\Verifactu\Models\Records\ForeignIdType; use Komma\Verifactu\Models\Records\InvoiceIdentifier; use Komma\Verifactu\Models\Records\InvoiceType; +use Komma\Verifactu\Models\Records\IssuedBy; use Komma\Verifactu\Models\Records\OperationType; use Komma\Verifactu\Models\Records\Record; use Komma\Verifactu\Models\Records\RegimeType; @@ -410,6 +411,54 @@ public function testValidatesReplacedInvoices(): void { $record->validate(); } + public function testValidatesIssuedBy(): void { + $record = Record::fromXml(TestUtils::getXmlFile(__DIR__ . '/registration-record-f1.xml')); + $this->assertInstanceOf(RegistrationRecord::class, $record); + + $record->issuedBy = IssuedBy::ThirdParty; + try { + $record->validate(); + $this->fail('An invoice issued by a third party without the third party must not validate'); + } catch (InvalidModelException $e) { + $this->assertStringContainsString('requires the third party', $e->getMessage()); + } + + $record->thirdParty = new FiscalIdentifier('Gestoría Rías SL', 'B70000021'); + $record->validate(); + + $record->issuedBy = IssuedBy::Recipient; + try { + $record->validate(); + $this->fail('A self-billed invoice must not carry a third party'); + } catch (InvalidModelException $e) { + $this->assertStringContainsString('Only an invoice issued by a third party', $e->getMessage()); + } + + $record->thirdParty = null; + $record->validate(); + $this->assertSame(IssuedBy::Recipient, $record->issuedBy); + } + + public function testExportsAndImportsIssuedByRecipient(): void { + $modelXml = TestUtils::getXmlFile(__DIR__ . '/registration-record-f1.xml'); + $record = Record::fromXml($modelXml); + $this->assertInstanceOf(RegistrationRecord::class, $record); + $record->issuedBy = IssuedBy::Recipient; + + $computerSystemXml = $modelXml->get('sum1:SistemaInformatico'); + $this->assertNotNull($computerSystemXml); + $exportedXml = UXML::newInstance('container', null, ['xmlns:sum1' => Record::NS]); + $record->export($exportedXml, ComputerSystem::fromXml($computerSystemXml)); + + $xml = $exportedXml->get('sum1:RegistroAlta')?->asXML() ?? ''; + $this->assertStringContainsString('D', $xml); + $this->assertLessThan(strpos($xml, ''), strpos($xml, '')); + + $reimported = Record::fromXml($exportedXml->get('sum1:RegistroAlta')); + $this->assertInstanceOf(RegistrationRecord::class, $reimported); + $this->assertSame(IssuedBy::Recipient, $reimported->issuedBy); + } + #[DataProvider('xmlPathsProvider')] public function testImportsAndExportsXmlElement(string $xmlPath): void { // Import model